@@ -0,0 +1,304 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2021 SonarSource SA | |||
* mailto:info AT sonarsource DOT com | |||
* | |||
* This program is free software; you can redistribute it and/or | |||
* modify it under the terms of the GNU Lesser General Public | |||
* License as published by the Free Software Foundation; either | |||
* version 3 of the License, or (at your option) any later version. | |||
* | |||
* This program is distributed in the hope that it will be useful, | |||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | |||
* Lesser General Public License for more details. | |||
* | |||
* You should have received a copy of the GNU Lesser General Public License | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
package org.sonar.alm.client.github; | |||
import com.google.gson.annotations.SerializedName; | |||
import java.util.List; | |||
import java.util.Objects; | |||
import java.util.Optional; | |||
import javax.annotation.CheckForNull; | |||
import javax.annotation.Nullable; | |||
import javax.annotation.concurrent.Immutable; | |||
import org.sonar.alm.client.github.security.AccessToken; | |||
import org.sonar.alm.client.github.security.UserAccessToken; | |||
import org.sonar.api.server.ServerSide; | |||
@ServerSide | |||
public interface GithubApplicationClient { | |||
/** | |||
* Create a user access token for the enterprise app installation. | |||
* | |||
* See https://developer.github.com/enterprise/2.20/apps/building-github-apps/identifying-and-authorizing-users-for-github-apps/#identifying-users-on-your-site | |||
* | |||
* @throws IllegalStateException if an internal error occured: network issue, invalid response, etc | |||
* @throws IllegalArgumentException if the request failed due to one of the parameters being invalid. | |||
*/ | |||
UserAccessToken createUserAccessToken(String appUrl, String clientId, String clientSecret, String code); | |||
/** | |||
* Lists all the organizations accessible to the access token provided. | |||
*/ | |||
Organizations listOrganizations(String appUrl, AccessToken accessToken, int page, int pageSize); | |||
/** | |||
* Lists all the repositories of the provided organization accessible to the access token provided. | |||
*/ | |||
Repositories listRepositories(String appUrl, AccessToken accessToken, String organization, @Nullable String query, int page, int pageSize); | |||
/** | |||
* Returns the repository identified by the repositoryKey owned by the provided organization. | |||
*/ | |||
Optional<Repository> getRepository(String appUrl, AccessToken accessToken, String organization, String repositoryKey); | |||
class Repositories { | |||
private int total; | |||
private List<Repository> repositories; | |||
public Repositories() { | |||
//nothing to do | |||
} | |||
public int getTotal() { | |||
return total; | |||
} | |||
public Repositories setTotal(int total) { | |||
this.total = total; | |||
return this; | |||
} | |||
@CheckForNull | |||
public List<Repository> getRepositories() { | |||
return repositories; | |||
} | |||
public Repositories setRepositories(List<Repository> repositories) { | |||
this.repositories = repositories; | |||
return this; | |||
} | |||
} | |||
@Immutable | |||
final class Repository { | |||
private final long id; | |||
private final String name; | |||
private final boolean isPrivate; | |||
private final String fullName; | |||
private final String url; | |||
public Repository(long id, String name, boolean isPrivate, String fullName, String url) { | |||
this.id = id; | |||
this.name = name; | |||
this.isPrivate = isPrivate; | |||
this.fullName = fullName; | |||
this.url = url; | |||
} | |||
public long getId() { | |||
return id; | |||
} | |||
public String getName() { | |||
return name; | |||
} | |||
public boolean isPrivate() { | |||
return isPrivate; | |||
} | |||
public String getFullName() { | |||
return fullName; | |||
} | |||
public String getUrl() { | |||
return url; | |||
} | |||
@Override | |||
public String toString() { | |||
return "Repository{" + | |||
"id=" + id + | |||
", name='" + name + '\'' + | |||
", isPrivate='" + isPrivate + '\'' + | |||
", fullName='" + fullName + '\'' + | |||
", url='" + url + '\'' + | |||
'}'; | |||
} | |||
@Override | |||
public boolean equals(Object o) { | |||
if (this == o) { | |||
return true; | |||
} | |||
if (o == null || getClass() != o.getClass()) { | |||
return false; | |||
} | |||
Repository that = (Repository) o; | |||
return id == that.id; | |||
} | |||
@Override | |||
public int hashCode() { | |||
return Long.hashCode(id); | |||
} | |||
} | |||
@Immutable | |||
final class RepositoryDetails { | |||
private final Repository repository; | |||
private final String description; | |||
private final String mainBranchName; | |||
private final String url; | |||
public RepositoryDetails(Repository repository, String description, String mainBranchName, String url) { | |||
this.repository = repository; | |||
this.description = description; | |||
this.mainBranchName = mainBranchName; | |||
this.url = url; | |||
} | |||
public Repository getRepository() { | |||
return repository; | |||
} | |||
public String getDescription() { | |||
return description; | |||
} | |||
public String getMainBranchName() { | |||
return mainBranchName; | |||
} | |||
public String getUrl() { | |||
return url; | |||
} | |||
@Override | |||
public boolean equals(Object o) { | |||
if (this == o) { | |||
return true; | |||
} | |||
if (o == null || getClass() != o.getClass()) { | |||
return false; | |||
} | |||
RepositoryDetails that = (RepositoryDetails) o; | |||
return Objects.equals(repository, that.repository) && | |||
Objects.equals(description, that.description) && | |||
Objects.equals(mainBranchName, that.mainBranchName) && | |||
Objects.equals(url, that.url); | |||
} | |||
@Override | |||
public int hashCode() { | |||
return Objects.hash(repository, description, mainBranchName, url); | |||
} | |||
@Override | |||
public String toString() { | |||
return "RepositoryDetails{" + | |||
"repository=" + repository + | |||
", description='" + description + '\'' + | |||
", mainBranchName='" + mainBranchName + '\'' + | |||
", url='" + url + '\'' + | |||
'}'; | |||
} | |||
} | |||
class Organizations { | |||
private int total; | |||
private List<Organization> organizations; | |||
public Organizations() { | |||
//nothing to do | |||
} | |||
public int getTotal() { | |||
return total; | |||
} | |||
public Organizations setTotal(int total) { | |||
this.total = total; | |||
return this; | |||
} | |||
@CheckForNull | |||
public List<Organization> getOrganizations() { | |||
return organizations; | |||
} | |||
public Organizations setOrganizations(List<Organization> organizations) { | |||
this.organizations = organizations; | |||
return this; | |||
} | |||
} | |||
class Organization { | |||
private final long id; | |||
private final String login; | |||
private final String name; | |||
private final String bio; | |||
private final String blog; | |||
@SerializedName("html_url") | |||
private final String htmlUrl; | |||
@SerializedName("avatar_url") | |||
private final String avatarUrl; | |||
private final String type; | |||
public Organization(long id, String login, @Nullable String name, @Nullable String bio, @Nullable String blog, @Nullable String htmlUrl, @Nullable String avatarUrl, | |||
String type) { | |||
this.id = id; | |||
this.login = login; | |||
this.name = name; | |||
this.bio = bio; | |||
this.blog = blog; | |||
this.htmlUrl = htmlUrl; | |||
this.avatarUrl = avatarUrl; | |||
this.type = type; | |||
} | |||
public long getId() { | |||
return id; | |||
} | |||
public String getLogin() { | |||
return login; | |||
} | |||
@CheckForNull | |||
public String getName() { | |||
return name; | |||
} | |||
@CheckForNull | |||
public String getBio() { | |||
return bio; | |||
} | |||
@CheckForNull | |||
public String getBlog() { | |||
return blog; | |||
} | |||
public String getHtmlUrl() { | |||
return htmlUrl; | |||
} | |||
@CheckForNull | |||
public String getAvatarUrl() { | |||
return avatarUrl; | |||
} | |||
public String getType() { | |||
return type; | |||
} | |||
} | |||
} |
@@ -0,0 +1,164 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2021 SonarSource SA | |||
* mailto:info AT sonarsource DOT com | |||
* | |||
* This program is free software; you can redistribute it and/or | |||
* modify it under the terms of the GNU Lesser General Public | |||
* License as published by the Free Software Foundation; either | |||
* version 3 of the License, or (at your option) any later version. | |||
* | |||
* This program is distributed in the hope that it will be useful, | |||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | |||
* Lesser General Public License for more details. | |||
* | |||
* You should have received a copy of the GNU Lesser General Public License | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
package org.sonar.alm.client.github; | |||
import com.google.gson.Gson; | |||
import java.io.IOException; | |||
import java.util.Arrays; | |||
import java.util.Optional; | |||
import java.util.stream.Collectors; | |||
import javax.annotation.Nullable; | |||
import org.sonar.alm.client.github.GithubApplicationHttpClient.GetResponse; | |||
import org.sonar.alm.client.github.GithubBinding.GsonGithubRepository; | |||
import org.sonar.alm.client.github.GithubBinding.GsonInstallations; | |||
import org.sonar.alm.client.github.GithubBinding.GsonRepositorySearch; | |||
import org.sonar.alm.client.github.security.AccessToken; | |||
import org.sonar.alm.client.github.security.UserAccessToken; | |||
import org.sonar.api.utils.log.Logger; | |||
import org.sonar.api.utils.log.Loggers; | |||
import static com.google.common.base.Preconditions.checkArgument; | |||
import static java.lang.String.format; | |||
import static java.net.HttpURLConnection.HTTP_OK; | |||
public class GithubApplicationClientImpl implements GithubApplicationClient { | |||
private static final Logger LOG = Loggers.get(GithubApplicationClientImpl.class); | |||
private static final Gson GSON = new Gson(); | |||
private final GithubApplicationHttpClient appHttpClient; | |||
public GithubApplicationClientImpl(GithubApplicationHttpClient appHttpClient) { | |||
this.appHttpClient = appHttpClient; | |||
} | |||
private static void checkPageArgs(int page, int pageSize) { | |||
checkArgument(page > 0, "'page' must be larger than 0."); | |||
checkArgument(pageSize > 0 && pageSize <= 100, "'pageSize' must be a value larger than 0 and smaller or equal to 100."); | |||
} | |||
@Override | |||
public Organizations listOrganizations(String appUrl, AccessToken accessToken, int page, int pageSize) { | |||
checkPageArgs(page, pageSize); | |||
try { | |||
Organizations organizations = new Organizations(); | |||
GetResponse response = appHttpClient.get(appUrl, accessToken, String.format("/user/installations?page=%s&per_page=%s", page, pageSize)); | |||
Optional<GsonInstallations> gsonInstallations = response.getContent().map(content -> GSON.fromJson(content, GsonInstallations.class)); | |||
if (!gsonInstallations.isPresent()) { | |||
return organizations; | |||
} | |||
organizations.setTotal(gsonInstallations.get().totalCount); | |||
if (gsonInstallations.get().installations != null) { | |||
organizations.setOrganizations(gsonInstallations.get().installations.stream() | |||
.map(gsonInstallation -> new Organization(gsonInstallation.account.id, gsonInstallation.account.login, null, null, null, null, null, | |||
gsonInstallation.targetType)) | |||
.collect(Collectors.toList())); | |||
} | |||
return organizations; | |||
} catch (IOException e) { | |||
throw new IllegalStateException(format("Failed to list all organizations accessible by user access token on %s", appUrl), e); | |||
} | |||
} | |||
@Override | |||
public Repositories listRepositories(String appUrl, AccessToken accessToken, String organization, @Nullable String query, int page, int pageSize) { | |||
checkPageArgs(page, pageSize); | |||
String searchQuery = "org:" + organization; | |||
if (query != null) { | |||
searchQuery = query.replace(" ", "+") + "+" + searchQuery; | |||
} | |||
try { | |||
Repositories repositories = new Repositories(); | |||
GetResponse response = appHttpClient.get(appUrl, accessToken, String.format("/search/repositories?q=%s&page=%s&per_page=%s", searchQuery, page, pageSize)); | |||
Optional<GsonRepositorySearch> gsonRepositories = response.getContent().map(content -> GSON.fromJson(content, GsonRepositorySearch.class)); | |||
if (!gsonRepositories.isPresent()) { | |||
return repositories; | |||
} | |||
repositories.setTotal(gsonRepositories.get().totalCount); | |||
if (gsonRepositories.get().items != null) { | |||
repositories.setRepositories(gsonRepositories.get().items.stream() | |||
.map(gsonRepository -> new Repository(gsonRepository.id, gsonRepository.name, gsonRepository.isPrivate, gsonRepository.fullName, gsonRepository.url)) | |||
.collect(Collectors.toList())); | |||
} | |||
return repositories; | |||
} catch (Exception e) { | |||
throw new IllegalStateException(format("Failed to list all repositories of '%s' accessible by user access token on '%s' using query '%s'", organization, appUrl, searchQuery), | |||
e); | |||
} | |||
} | |||
@Override | |||
public Optional<Repository> getRepository(String appUrl, AccessToken accessToken, String organization, String repositoryKey) { | |||
try { | |||
GetResponse response = appHttpClient.get(appUrl, accessToken, String.format("/repos/%s", repositoryKey)); | |||
return response.getContent() | |||
.map(content -> GSON.fromJson(content, GsonGithubRepository.class)) | |||
.map(repository -> new Repository(repository.id, repository.name, repository.isPrivate, repository.fullName, repository.url)); | |||
} 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); | |||
} | |||
} | |||
@Override | |||
public UserAccessToken createUserAccessToken(String appUrl, String clientId, String clientSecret, String code) { | |||
try { | |||
String endpoint = "/login/oauth/access_token?client_id=" + clientId + "&client_secret=" + clientSecret + "&code=" + code; | |||
String baseAppUrl; | |||
int apiIndex = appUrl.indexOf("/api/v3"); | |||
if (apiIndex > 0) { | |||
baseAppUrl = appUrl.substring(0, apiIndex); | |||
} else if (appUrl.startsWith("https://api.github.com")) { | |||
baseAppUrl = "https://github.com"; | |||
} else { | |||
baseAppUrl = appUrl; | |||
} | |||
GithubApplicationHttpClient.Response response = appHttpClient.post(baseAppUrl, null, endpoint); | |||
if (response.getCode() != HTTP_OK) { | |||
throw new IllegalStateException("Failed to create GitHub's user access token. GitHub returned code " + code + ". " + response.getContent().orElse("")); | |||
} | |||
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()) | |||
.map(UserAccessToken::new); | |||
if (accessToken.isPresent()) { | |||
return accessToken.get(); | |||
} | |||
// If token is not in the 200's body, it's because the client ID or client secret are incorrect | |||
LOG.error("Failed to create GitHub's user access token. GitHub's response: " + content); | |||
throw new IllegalArgumentException(); | |||
} catch (IOException e) { | |||
throw new IllegalStateException("Failed to create GitHub's user access token", e); | |||
} | |||
} | |||
} |
@@ -0,0 +1,81 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2021 SonarSource SA | |||
* mailto:info AT sonarsource DOT com | |||
* | |||
* This program is free software; you can redistribute it and/or | |||
* modify it under the terms of the GNU Lesser General Public | |||
* License as published by the Free Software Foundation; either | |||
* version 3 of the License, or (at your option) any later version. | |||
* | |||
* This program is distributed in the hope that it will be useful, | |||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | |||
* Lesser General Public License for more details. | |||
* | |||
* You should have received a copy of the GNU Lesser General Public License | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
package org.sonar.alm.client.github; | |||
import java.io.IOException; | |||
import java.util.Optional; | |||
import org.sonar.alm.client.github.security.AccessToken; | |||
import org.sonar.api.ce.ComputeEngineSide; | |||
import org.sonar.api.server.ServerSide; | |||
@ServerSide | |||
@ComputeEngineSide | |||
public interface GithubApplicationHttpClient { | |||
/** | |||
* Content of the response is populated if response's HTTP code is {@link java.net.HttpURLConnection#HTTP_OK OK}. | |||
*/ | |||
GetResponse get(String appUrl, AccessToken token, String endPoint) throws IOException; | |||
/** | |||
* Content of the response is populated if response's HTTP code is {@link java.net.HttpURLConnection#HTTP_OK OK} or | |||
* {@link java.net.HttpURLConnection#HTTP_CREATED CREATED}. | |||
*/ | |||
Response post(String appUrl, AccessToken token, String endPoint) throws IOException; | |||
/** | |||
* Content of the response is populated if response's HTTP code is {@link java.net.HttpURLConnection#HTTP_OK OK} or | |||
* {@link java.net.HttpURLConnection#HTTP_CREATED CREATED}. | |||
* | |||
* Content type will be application/json; charset=utf-8 | |||
*/ | |||
Response post(String appUrl, AccessToken token, String endPoint, String json) throws IOException; | |||
/** | |||
* Content of the response is populated if response's HTTP code is {@link java.net.HttpURLConnection#HTTP_OK OK}. | |||
* | |||
* Content type will be application/json; charset=utf-8 | |||
*/ | |||
Response patch(String appUrl, AccessToken token, String endPoint, String json) throws IOException; | |||
/** | |||
* Content of the response is populated if response's HTTP code is {@link java.net.HttpURLConnection#HTTP_OK OK}. | |||
* | |||
* Content type will be application/json; charset=utf-8 | |||
* | |||
*/ | |||
Response delete(String appUrl, AccessToken token, String endPoint) throws IOException; | |||
interface Response { | |||
/** | |||
* @return the HTTP code of the response. | |||
*/ | |||
int getCode(); | |||
/** | |||
* @return the content of the response if the response had an HTTP code for which we expect a content for the current | |||
* HTTP method (see {@link #get(String, AccessToken, String)} and {@link #post(String, AccessToken, String)}). | |||
*/ | |||
Optional<String> getContent(); | |||
} | |||
interface GetResponse extends Response { | |||
Optional<String> getNextEndPoint(); | |||
} | |||
} |
@@ -0,0 +1,254 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2021 SonarSource SA | |||
* mailto:info AT sonarsource DOT com | |||
* | |||
* This program is free software; you can redistribute it and/or | |||
* modify it under the terms of the GNU Lesser General Public | |||
* License as published by the Free Software Foundation; either | |||
* version 3 of the License, or (at your option) any later version. | |||
* | |||
* This program is distributed in the hope that it will be useful, | |||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | |||
* Lesser General Public License for more details. | |||
* | |||
* You should have received a copy of the GNU Lesser General Public License | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
package org.sonar.alm.client.github; | |||
import java.io.IOException; | |||
import java.net.MalformedURLException; | |||
import java.net.URL; | |||
import java.util.Optional; | |||
import java.util.regex.Matcher; | |||
import java.util.regex.Pattern; | |||
import javax.annotation.CheckForNull; | |||
import javax.annotation.Nullable; | |||
import okhttp3.FormBody; | |||
import okhttp3.MediaType; | |||
import okhttp3.OkHttpClient; | |||
import okhttp3.Request; | |||
import okhttp3.RequestBody; | |||
import okhttp3.ResponseBody; | |||
import org.sonar.alm.client.TimeoutConfiguration; | |||
import org.sonar.alm.client.github.security.AccessToken; | |||
import org.sonar.api.utils.log.Logger; | |||
import org.sonar.api.utils.log.Loggers; | |||
import org.sonarqube.ws.client.OkHttpClientBuilder; | |||
import static com.google.common.base.Preconditions.checkArgument; | |||
import static java.net.HttpURLConnection.HTTP_CREATED; | |||
import static java.net.HttpURLConnection.HTTP_NO_CONTENT; | |||
import static java.net.HttpURLConnection.HTTP_OK; | |||
import static java.util.Optional.empty; | |||
import static java.util.Optional.of; | |||
import static java.util.Optional.ofNullable; | |||
public class GithubApplicationHttpClientImpl implements GithubApplicationHttpClient { | |||
private static final Logger LOG = Loggers.get(GithubApplicationHttpClientImpl.class); | |||
private static final Pattern NEXT_LINK_PATTERN = Pattern.compile(".*<(.*)>; rel=\"next\""); | |||
private static final String MACHINE_MAN_PREVIEW_JSON = "application/vnd.github.machine-man-preview+json"; | |||
private static final String ANTIOPE_PREVIEW_JSON = "application/vnd.github.antiope-preview+json"; | |||
private final OkHttpClient client; | |||
public GithubApplicationHttpClientImpl(TimeoutConfiguration timeoutConfiguration) { | |||
client = new OkHttpClientBuilder() | |||
.setConnectTimeoutMs(timeoutConfiguration.getConnectTimeout()) | |||
.setReadTimeoutMs(timeoutConfiguration.getReadTimeout()) | |||
.build(); | |||
} | |||
@Override | |||
public GetResponse get(String appUrl, AccessToken token, String endPoint) throws IOException { | |||
validateEndPoint(endPoint); | |||
try (okhttp3.Response response = client.newCall(newGetRequest(appUrl, token, endPoint)).execute()) { | |||
int responseCode = response.code(); | |||
if (responseCode != HTTP_OK) { | |||
LOG.warn("GET response did not have expected HTTP code (was {}): {}", responseCode, attemptReadContent(response)); | |||
return new GetResponseImpl(responseCode, null, null); | |||
} | |||
return new GetResponseImpl(responseCode, readContent(response.body()).orElse(null), readNextEndPoint(response)); | |||
} | |||
} | |||
private static void validateEndPoint(String endPoint) { | |||
checkArgument(endPoint.startsWith("/") || endPoint.startsWith("http"), "endpoint must start with '/' or 'http'"); | |||
} | |||
private static Request newGetRequest(String appUrl, AccessToken token, String endPoint) { | |||
return newRequestBuilder(appUrl, token, endPoint).get().build(); | |||
} | |||
@Override | |||
public Response post(String appUrl, AccessToken token, String endPoint) throws IOException { | |||
return doPost(appUrl, token, endPoint, new FormBody.Builder().build()); | |||
} | |||
@Override | |||
public Response post(String appUrl, AccessToken token, String endPoint, String json) throws IOException { | |||
RequestBody body = RequestBody.create(json, MediaType.parse("application/json; charset=utf-8")); | |||
return doPost(appUrl, token, endPoint, body); | |||
} | |||
@Override | |||
public Response patch(String appUrl, AccessToken token, String endPoint, String json) throws IOException { | |||
RequestBody body = RequestBody.create(json, MediaType.parse("application/json; charset=utf-8")); | |||
return doPatch(appUrl, token, endPoint, body); | |||
} | |||
@Override | |||
public Response delete(String appUrl, AccessToken token, String endPoint) throws IOException { | |||
validateEndPoint(endPoint); | |||
try (okhttp3.Response response = client.newCall(newDeleteRequest(appUrl, token, endPoint)).execute()) { | |||
int responseCode = response.code(); | |||
if (responseCode != HTTP_NO_CONTENT) { | |||
String content = attemptReadContent(response); | |||
LOG.warn("DELETE response did not have expected HTTP code (was {}): {}", responseCode, content); | |||
return new ResponseImpl(responseCode, content); | |||
} | |||
return new ResponseImpl(responseCode, null); | |||
} | |||
} | |||
private static Request newDeleteRequest(String appUrl, AccessToken token, String endPoint) { | |||
return newRequestBuilder(appUrl, token, endPoint).delete().build(); | |||
} | |||
private Response doPost(String appUrl, @Nullable AccessToken token, String endPoint, RequestBody body) throws IOException { | |||
validateEndPoint(endPoint); | |||
try (okhttp3.Response response = client.newCall(newPostRequest(appUrl, token, endPoint, body)).execute()) { | |||
int responseCode = response.code(); | |||
if (responseCode == HTTP_OK || responseCode == HTTP_CREATED) { | |||
return new ResponseImpl(responseCode, readContent(response.body()).orElse(null)); | |||
} else if (responseCode == HTTP_NO_CONTENT) { | |||
return new ResponseImpl(responseCode, null); | |||
} | |||
String content = attemptReadContent(response); | |||
LOG.warn("POST response did not have expected HTTP code (was {}): {}", responseCode, content); | |||
return new ResponseImpl(responseCode, content); | |||
} | |||
} | |||
private Response doPatch(String appUrl, AccessToken token, String endPoint, RequestBody body) throws IOException { | |||
validateEndPoint(endPoint); | |||
try (okhttp3.Response response = client.newCall(newPatchRequest(token, appUrl, endPoint, body)).execute()) { | |||
int responseCode = response.code(); | |||
if (responseCode == HTTP_OK) { | |||
return new ResponseImpl(responseCode, readContent(response.body()).orElse(null)); | |||
} else if (responseCode == HTTP_NO_CONTENT) { | |||
return new ResponseImpl(responseCode, null); | |||
} | |||
String content = attemptReadContent(response); | |||
LOG.warn("PATCH response did not have expected HTTP code (was {}): {}", responseCode, content); | |||
return new ResponseImpl(responseCode, content); | |||
} | |||
} | |||
private static Request newPostRequest(String appUrl, @Nullable AccessToken token, String endPoint, RequestBody body) { | |||
return newRequestBuilder(appUrl, token, endPoint).post(body).build(); | |||
} | |||
private static Request newPatchRequest(AccessToken token, String appUrl, String endPoint, RequestBody body) { | |||
return newRequestBuilder(appUrl, token, endPoint).patch(body).build(); | |||
} | |||
private static Request.Builder newRequestBuilder(String appUrl, @Nullable AccessToken token, String endPoint) { | |||
Request.Builder url = new Request.Builder() | |||
.url(toAbsoluteEndPoint(appUrl, endPoint)); | |||
if (token != null) { | |||
url | |||
.addHeader("Authorization", token.getAuthorizationHeaderPrefix() + " " + token) | |||
// TODO: Remove when CheckAPI is no longer in beta | |||
.addHeader("Accept", ANTIOPE_PREVIEW_JSON + ", " + MACHINE_MAN_PREVIEW_JSON); | |||
} | |||
return url; | |||
} | |||
private static String toAbsoluteEndPoint(String host, String endPoint) { | |||
if (endPoint.startsWith("http")) { | |||
return endPoint; | |||
} | |||
try { | |||
return new URL(host + endPoint).toExternalForm(); | |||
} catch (MalformedURLException e) { | |||
throw new IllegalArgumentException(String.format("%s is not a valid url", host + endPoint)); | |||
} | |||
} | |||
private static String attemptReadContent(okhttp3.Response response) { | |||
try { | |||
return readContent(response.body()).orElse(null); | |||
} catch (IOException e) { | |||
return null; | |||
} | |||
} | |||
private static Optional<String> readContent(@Nullable ResponseBody body) throws IOException { | |||
if (body == null) { | |||
return empty(); | |||
} | |||
try { | |||
return of(body.string()); | |||
} finally { | |||
body.close(); | |||
} | |||
} | |||
@CheckForNull | |||
private static String readNextEndPoint(okhttp3.Response response) { | |||
String links = response.header("link"); | |||
if (links == null || links.isEmpty() || !links.contains("rel=\"next\"")) { | |||
return null; | |||
} | |||
Matcher nextLinkMatcher = NEXT_LINK_PATTERN.matcher(links); | |||
if (!nextLinkMatcher.find()) { | |||
return null; | |||
} | |||
return nextLinkMatcher.group(1); | |||
} | |||
private static class ResponseImpl implements Response { | |||
private final int code; | |||
private final String content; | |||
private ResponseImpl(int code, @Nullable String content) { | |||
this.code = code; | |||
this.content = content; | |||
} | |||
@Override | |||
public int getCode() { | |||
return code; | |||
} | |||
@Override | |||
public Optional<String> getContent() { | |||
return ofNullable(content); | |||
} | |||
} | |||
private static final class GetResponseImpl extends ResponseImpl implements GetResponse { | |||
private final String nextEndPoint; | |||
private GetResponseImpl(int code, @Nullable String content, @Nullable String nextEndPoint) { | |||
super(code, content); | |||
this.nextEndPoint = nextEndPoint; | |||
} | |||
@Override | |||
public Optional<String> getNextEndPoint() { | |||
return ofNullable(nextEndPoint); | |||
} | |||
} | |||
} |
@@ -0,0 +1,148 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2021 SonarSource SA | |||
* mailto:info AT sonarsource DOT com | |||
* | |||
* This program is free software; you can redistribute it and/or | |||
* modify it under the terms of the GNU Lesser General Public | |||
* License as published by the Free Software Foundation; either | |||
* version 3 of the License, or (at your option) any later version. | |||
* | |||
* This program is distributed in the hope that it will be useful, | |||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | |||
* Lesser General Public License for more details. | |||
* | |||
* You should have received a copy of the GNU Lesser General Public License | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
package org.sonar.alm.client.github; | |||
import com.google.gson.annotations.SerializedName; | |||
import java.util.List; | |||
public class GithubBinding { | |||
private GithubBinding() { | |||
//nothing to do | |||
} | |||
public static class GsonInstallations { | |||
@SerializedName("total_count") | |||
int totalCount; | |||
@SerializedName("installations") | |||
List<GsonInstallation> installations; | |||
public GsonInstallations() { | |||
// even if empty constructor is not required for Gson, it is strongly | |||
// recommended: | |||
// http://stackoverflow.com/a/18645370/229031 | |||
} | |||
} | |||
public static class GsonInstallation { | |||
@SerializedName("id") | |||
long id; | |||
@SerializedName("target_type") | |||
String targetType; | |||
@SerializedName("permissions") | |||
Permissions permissions; | |||
@SerializedName("account") | |||
GsonAccount account; | |||
public GsonInstallation(long id, String targetType, Permissions permissions, GsonAccount account) { | |||
this.id = id; | |||
this.targetType = targetType; | |||
this.permissions = permissions; | |||
this.account = account; | |||
} | |||
public GsonInstallation() { | |||
// even if empty constructor is not required for Gson, it is strongly | |||
// recommended: | |||
// http://stackoverflow.com/a/18645370/229031 | |||
} | |||
public long getId() { | |||
return id; | |||
} | |||
public String getTargetType() { | |||
return targetType; | |||
} | |||
public Permissions getPermissions() { | |||
return permissions; | |||
} | |||
public GsonAccount getAccount() { | |||
return account; | |||
} | |||
public static class Permissions { | |||
@SerializedName("checks") | |||
String checks; | |||
public Permissions(String checks) { | |||
this.checks = checks; | |||
} | |||
public Permissions() { | |||
// even if empty constructor is not required for Gson, it is strongly | |||
// recommended: | |||
// http://stackoverflow.com/a/18645370/229031 | |||
} | |||
public String getChecks() { | |||
return checks; | |||
} | |||
} | |||
public static class GsonAccount { | |||
@SerializedName("id") | |||
long id; | |||
@SerializedName("login") | |||
String login; | |||
public GsonAccount() { | |||
// even if empty constructor is not required for Gson, it is strongly | |||
// recommended: | |||
// http://stackoverflow.com/a/18645370/229031 | |||
} | |||
} | |||
} | |||
public static class GsonRepositorySearch { | |||
@SerializedName("total_count") | |||
int totalCount; | |||
@SerializedName("items") | |||
List<GsonGithubRepository> items; | |||
public GsonRepositorySearch() { | |||
// even if empty constructor is not required for Gson, it is strongly | |||
// recommended: | |||
// http://stackoverflow.com/a/18645370/229031 | |||
} | |||
} | |||
public static class GsonGithubRepository { | |||
@SerializedName("id") | |||
long id; | |||
@SerializedName("name") | |||
String name; | |||
@SerializedName("full_name") | |||
String fullName; | |||
@SerializedName("private") | |||
boolean isPrivate; | |||
@SerializedName("url") | |||
String url; | |||
public GsonGithubRepository() { | |||
// even if empty constructor is not required for Gson, it is strongly | |||
// recommended: | |||
// http://stackoverflow.com/a/18645370/229031 | |||
} | |||
} | |||
} |
@@ -0,0 +1,23 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2021 SonarSource SA | |||
* mailto:info AT sonarsource DOT com | |||
* | |||
* This program is free software; you can redistribute it and/or | |||
* modify it under the terms of the GNU Lesser General Public | |||
* License as published by the Free Software Foundation; either | |||
* version 3 of the License, or (at your option) any later version. | |||
* | |||
* This program is distributed in the hope that it will be useful, | |||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | |||
* Lesser General Public License for more details. | |||
* | |||
* You should have received a copy of the GNU Lesser General Public License | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
@ParametersAreNonnullByDefault | |||
package org.sonar.alm.client.github; | |||
import javax.annotation.ParametersAreNonnullByDefault; |
@@ -0,0 +1,35 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2021 SonarSource SA | |||
* mailto:info AT sonarsource DOT com | |||
* | |||
* This program is free software; you can redistribute it and/or | |||
* modify it under the terms of the GNU Lesser General Public | |||
* License as published by the Free Software Foundation; either | |||
* version 3 of the License, or (at your option) any later version. | |||
* | |||
* This program is distributed in the hope that it will be useful, | |||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | |||
* Lesser General Public License for more details. | |||
* | |||
* You should have received a copy of the GNU Lesser General Public License | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
package org.sonar.alm.client.github.security; | |||
/** | |||
* Token used to authenticate requests to Github API | |||
* | |||
*/ | |||
public interface AccessToken { | |||
String getValue(); | |||
/** | |||
* Value of the HTTP header "Authorization" | |||
*/ | |||
String getAuthorizationHeaderPrefix(); | |||
} |
@@ -0,0 +1,44 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2021 SonarSource SA | |||
* mailto:info AT sonarsource DOT com | |||
* | |||
* This program is free software; you can redistribute it and/or | |||
* modify it under the terms of the GNU Lesser General Public | |||
* License as published by the Free Software Foundation; either | |||
* version 3 of the License, or (at your option) any later version. | |||
* | |||
* This program is distributed in the hope that it will be useful, | |||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | |||
* Lesser General Public License for more details. | |||
* | |||
* You should have received a copy of the GNU Lesser General Public License | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
package org.sonar.alm.client.github.security; | |||
public class UserAccessToken implements AccessToken { | |||
private final String token; | |||
public UserAccessToken(String token) { | |||
this.token = token; | |||
} | |||
@Override | |||
public String getValue() { | |||
return token; | |||
} | |||
@Override | |||
public String getAuthorizationHeaderPrefix() { | |||
return "token"; | |||
} | |||
@Override | |||
public String toString() { | |||
return getValue(); | |||
} | |||
} |
@@ -0,0 +1,23 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2021 SonarSource SA | |||
* mailto:info AT sonarsource DOT com | |||
* | |||
* This program is free software; you can redistribute it and/or | |||
* modify it under the terms of the GNU Lesser General Public | |||
* License as published by the Free Software Foundation; either | |||
* version 3 of the License, or (at your option) any later version. | |||
* | |||
* This program is distributed in the hope that it will be useful, | |||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | |||
* Lesser General Public License for more details. | |||
* | |||
* You should have received a copy of the GNU Lesser General Public License | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
@ParametersAreNonnullByDefault | |||
package org.sonar.alm.client.github.security; | |||
import javax.annotation.ParametersAreNonnullByDefault; |
@@ -0,0 +1,703 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2021 SonarSource SA | |||
* mailto:info AT sonarsource DOT com | |||
* | |||
* This program is free software; you can redistribute it and/or | |||
* modify it under the terms of the GNU Lesser General Public | |||
* License as published by the Free Software Foundation; either | |||
* version 3 of the License, or (at your option) any later version. | |||
* | |||
* This program is distributed in the hope that it will be useful, | |||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | |||
* Lesser General Public License for more details. | |||
* | |||
* You should have received a copy of the GNU Lesser General Public License | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
package org.sonar.alm.client.github; | |||
import com.tngtech.java.junit.dataprovider.DataProvider; | |||
import com.tngtech.java.junit.dataprovider.DataProviderRunner; | |||
import com.tngtech.java.junit.dataprovider.UseDataProvider; | |||
import java.io.IOException; | |||
import java.util.Optional; | |||
import javax.annotation.Nullable; | |||
import org.junit.Before; | |||
import org.junit.ClassRule; | |||
import org.junit.Test; | |||
import org.junit.runner.RunWith; | |||
import org.sonar.alm.client.github.security.AccessToken; | |||
import org.sonar.alm.client.github.security.UserAccessToken; | |||
import org.sonar.api.utils.log.LogTester; | |||
import org.sonar.api.utils.log.LoggerLevel; | |||
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.assertj.core.groups.Tuple.tuple; | |||
import static org.mockito.ArgumentMatchers.any; | |||
import static org.mockito.Mockito.mock; | |||
import static org.mockito.Mockito.verify; | |||
import static org.mockito.Mockito.when; | |||
@RunWith(DataProviderRunner.class) | |||
public class GithubApplicationClientImplTest { | |||
@ClassRule | |||
public static LogTester logTester = new LogTester().setLevel(LoggerLevel.WARN); | |||
private GithubApplicationHttpClientImpl httpClient = mock(GithubApplicationHttpClientImpl.class); | |||
private GithubApplicationClient underTest; | |||
private String appUrl = "Any URL"; | |||
@Before | |||
public void setup() { | |||
underTest = new GithubApplicationClientImpl(httpClient); | |||
logTester.clear(); | |||
} | |||
@Test | |||
@UseDataProvider("githubServers") | |||
public void createUserAccessToken_returns_empty_if_access_token_cant_be_created(String apiUrl, String appUrl) throws IOException { | |||
when(httpClient.post(appUrl, null, "/login/oauth/access_token?client_id=clientId&client_secret=clientSecret&code=code")) | |||
.thenReturn(new Response(400, null)); | |||
assertThatThrownBy(() -> underTest.createUserAccessToken(appUrl, "clientId", "clientSecret", "code")) | |||
.isInstanceOf(IllegalStateException.class); | |||
verify(httpClient).post(appUrl, null, "/login/oauth/access_token?client_id=clientId&client_secret=clientSecret&code=code"); | |||
} | |||
@Test | |||
@UseDataProvider("githubServers") | |||
public void createUserAccessToken_fail_if_access_token_request_fails(String apiUrl, String appUrl) throws IOException { | |||
when(httpClient.post(appUrl, null, "/login/oauth/access_token?client_id=clientId&client_secret=clientSecret&code=code")) | |||
.thenThrow(new IOException("OOPS")); | |||
assertThatThrownBy(() -> underTest.createUserAccessToken(apiUrl, "clientId", "clientSecret", "code")) | |||
.isInstanceOf(IllegalStateException.class) | |||
.hasMessage("Failed to create GitHub's user access token"); | |||
verify(httpClient).post(appUrl, null, "/login/oauth/access_token?client_id=clientId&client_secret=clientSecret&code=code"); | |||
} | |||
@Test | |||
@UseDataProvider("githubServers") | |||
public void createUserAccessToken_throws_illegal_argument_exception_if_access_token_code_is_expired(String apiUrl, String appUrl) throws IOException { | |||
when(httpClient.post(appUrl, null, "/login/oauth/access_token?client_id=clientId&client_secret=clientSecret&code=code")) | |||
.thenReturn(new OkGetResponse("error_code=100&error=expired_or_invalid")); | |||
assertThatThrownBy(() -> underTest.createUserAccessToken(apiUrl, "clientId", "clientSecret", "code")) | |||
.isInstanceOf(IllegalArgumentException.class); | |||
verify(httpClient).post(appUrl, null, "/login/oauth/access_token?client_id=clientId&client_secret=clientSecret&code=code"); | |||
} | |||
@Test | |||
@UseDataProvider("githubServers") | |||
public void createUserAccessToken_from_authorization_code_returns_access_token(String apiUrl, String appUrl) throws IOException { | |||
String token = randomAlphanumeric(10); | |||
when(httpClient.post(appUrl, null, "/login/oauth/access_token?client_id=clientId&client_secret=clientSecret&code=code")) | |||
.thenReturn(new OkGetResponse("access_token=" + token + "&status=")); | |||
UserAccessToken userAccessToken = underTest.createUserAccessToken(apiUrl, "clientId", "clientSecret", "code"); | |||
assertThat(userAccessToken) | |||
.extracting(UserAccessToken::getValue, UserAccessToken::getAuthorizationHeaderPrefix) | |||
.containsOnly(token, "token"); | |||
verify(httpClient).post(appUrl, null, "/login/oauth/access_token?client_id=clientId&client_secret=clientSecret&code=code"); | |||
} | |||
@DataProvider | |||
public static Object[][] githubServers() { | |||
return new Object[][] { | |||
{"https://github.sonarsource.com/api/v3", "https://github.sonarsource.com"}, | |||
{"https://api.github.com", "https://github.com"}, | |||
{"https://github.sonarsource.com/api/v3/", "https://github.sonarsource.com"}, | |||
{"https://api.github.com/", "https://github.com"}, | |||
}; | |||
} | |||
@Test | |||
public void listOrganizations_fail_on_failure() throws IOException { | |||
String appUrl = "https://github.sonarsource.com"; | |||
AccessToken accessToken = new UserAccessToken(randomAlphanumeric(10)); | |||
when(httpClient.get(appUrl, accessToken, String.format("/user/installations?page=%s&per_page=%s", 1, 100))) | |||
.thenThrow(new IOException("OOPS")); | |||
assertThatThrownBy(() -> underTest.listOrganizations(appUrl, accessToken, 1, 100)) | |||
.isInstanceOf(IllegalStateException.class) | |||
.hasMessage("Failed to list all organizations accessible by user access token on %s", appUrl); | |||
} | |||
@Test | |||
public void listOrganizations_fail_if_pageIndex_out_of_bounds() { | |||
UserAccessToken token = new UserAccessToken("token"); | |||
assertThatThrownBy(() -> underTest.listOrganizations(appUrl, token, 0, 100)) | |||
.isInstanceOf(IllegalArgumentException.class) | |||
.hasMessage("'page' must be larger than 0."); | |||
} | |||
@Test | |||
public void listOrganizations_fail_if_pageSize_out_of_bounds() { | |||
UserAccessToken token = new UserAccessToken("token"); | |||
assertThatThrownBy(() -> underTest.listOrganizations(appUrl, token, 1, 0)) | |||
.isInstanceOf(IllegalArgumentException.class) | |||
.hasMessage("'pageSize' must be a value larger than 0 and smaller or equal to 100."); | |||
assertThatThrownBy(() -> underTest.listOrganizations("", token, 1, 101)) | |||
.isInstanceOf(IllegalArgumentException.class) | |||
.hasMessage("'pageSize' must be a value larger than 0 and smaller or equal to 100."); | |||
} | |||
@Test | |||
public void listOrganizations_returns_no_installations() throws IOException { | |||
String appUrl = "https://github.sonarsource.com"; | |||
AccessToken accessToken = new UserAccessToken(randomAlphanumeric(10)); | |||
String responseJson = "{\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)); | |||
GithubApplicationClient.Organizations organizations = underTest.listOrganizations(appUrl, accessToken, 1, 100); | |||
assertThat(organizations.getTotal()).isZero(); | |||
assertThat(organizations.getOrganizations()).isNull(); | |||
} | |||
@Test | |||
public void listOrganizations_returns_pages_results() throws IOException { | |||
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" | |||
+ "} "; | |||
when(httpClient.get(appUrl, accessToken, String.format("/user/installations?page=%s&per_page=%s", 1, 100))) | |||
.thenReturn(new OkGetResponse(responseJson)); | |||
GithubApplicationClient.Organizations organizations = underTest.listOrganizations(appUrl, accessToken, 1, 100); | |||
assertThat(organizations.getTotal()).isEqualTo(2); | |||
assertThat(organizations.getOrganizations()).extracting(GithubApplicationClient.Organization::getLogin).containsOnly("github", "octocat"); | |||
} | |||
@Test | |||
public void listRepositories_fail_on_failure() throws IOException { | |||
String appUrl = "https://github.sonarsource.com"; | |||
AccessToken accessToken = new UserAccessToken(randomAlphanumeric(10)); | |||
when(httpClient.get(appUrl, accessToken, String.format("/search/repositories?q=%s&page=%s&per_page=%s", "org:test", 1, 100))) | |||
.thenThrow(new IOException("OOPS")); | |||
assertThatThrownBy(() -> underTest.listRepositories(appUrl, accessToken, "test", null, 1, 100)) | |||
.isInstanceOf(IllegalStateException.class) | |||
.hasMessage("Failed to list all repositories of 'test' accessible by user access token on 'https://github.sonarsource.com' using query 'org:test'"); | |||
} | |||
@Test | |||
public void listRepositories_fail_if_pageIndex_out_of_bounds() { | |||
UserAccessToken token = new UserAccessToken("token"); | |||
assertThatThrownBy(() -> underTest.listRepositories(appUrl, token, "test", null, 0, 100)) | |||
.isInstanceOf(IllegalArgumentException.class) | |||
.hasMessage("'page' must be larger than 0."); | |||
} | |||
@Test | |||
public void listRepositories_fail_if_pageSize_out_of_bounds() { | |||
UserAccessToken token = new UserAccessToken("token"); | |||
assertThatThrownBy(() -> underTest.listRepositories(appUrl, token, "test", null, 1, 0)) | |||
.isInstanceOf(IllegalArgumentException.class) | |||
.hasMessage("'pageSize' must be a value larger than 0 and smaller or equal to 100."); | |||
assertThatThrownBy(() -> underTest.listRepositories("", token, "test", null, 1, 101)) | |||
.isInstanceOf(IllegalArgumentException.class) | |||
.hasMessage("'pageSize' must be a value larger than 0 and smaller or equal to 100."); | |||
} | |||
@Test | |||
public void listRepositories_returns_empty_results() throws IOException { | |||
String appUrl = "https://github.sonarsource.com"; | |||
AccessToken accessToken = new UserAccessToken(randomAlphanumeric(10)); | |||
String responseJson = "{\n" | |||
+ " \"total_count\": 0\n" | |||
+ "}"; | |||
when(httpClient.get(appUrl, accessToken, String.format("/search/repositories?q=%s&page=%s&per_page=%s", "org:github", 1, 100))) | |||
.thenReturn(new OkGetResponse(responseJson)); | |||
GithubApplicationClient.Repositories repositories = underTest.listRepositories(appUrl, accessToken, "github", null, 1, 100); | |||
assertThat(repositories.getTotal()).isZero(); | |||
assertThat(repositories.getRepositories()).isNull(); | |||
} | |||
@Test | |||
public void listRepositories_returns_pages_results() throws IOException { | |||
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" | |||
+ "}"; | |||
when(httpClient.get(appUrl, accessToken, String.format("/search/repositories?q=%s&page=%s&per_page=%s", "org:github", 1, 100))) | |||
.thenReturn(new OkGetResponse(responseJson)); | |||
GithubApplicationClient.Repositories repositories = underTest.listRepositories(appUrl, accessToken, "github", null, 1, 100); | |||
assertThat(repositories.getTotal()).isEqualTo(2); | |||
assertThat(repositories.getRepositories()) | |||
.extracting(GithubApplicationClient.Repository::getName, GithubApplicationClient.Repository::getFullName) | |||
.containsOnly(tuple("HelloWorld", "github/HelloWorld"), tuple("HelloUniverse", "github/HelloUniverse")); | |||
} | |||
@Test | |||
public void listRepositories_returns_search_results() throws IOException { | |||
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" | |||
+ "}"; | |||
when(httpClient.get(appUrl, accessToken, String.format("/search/repositories?q=%s&page=%s&per_page=%s", "world+org:github", 1, 100))) | |||
.thenReturn(new GithubApplicationHttpClient.GetResponse() { | |||
@Override | |||
public Optional<String> getNextEndPoint() { | |||
return Optional.empty(); | |||
} | |||
@Override | |||
public int getCode() { | |||
return 200; | |||
} | |||
@Override | |||
public Optional<String> getContent() { | |||
return Optional.of(responseJson); | |||
} | |||
}); | |||
GithubApplicationClient.Repositories repositories = underTest.listRepositories(appUrl, accessToken, "github", "world", 1, 100); | |||
assertThat(repositories.getTotal()).isEqualTo(2); | |||
assertThat(repositories.getRepositories()) | |||
.extracting(GithubApplicationClient.Repository::getName, GithubApplicationClient.Repository::getFullName) | |||
.containsOnly(tuple("HelloWorld", "github/HelloWorld")); | |||
} | |||
@Test | |||
public void getRepository_returns_empty_when_repository_doesnt_exist() throws IOException { | |||
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"); | |||
assertThat(repository).isEmpty(); | |||
} | |||
@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)) | |||
.isInstanceOf(IllegalStateException.class) | |||
.hasMessage("Failed to get repository '%s' of '%s' accessible by user access token on '%s'", repositoryKey, organization, appUrl); | |||
} | |||
@Test | |||
public void getRepository_returns_repository() throws IOException { | |||
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" | |||
+ " }" | |||
+ "}"; | |||
when(httpClient.get(appUrl, accessToken, "/repos/octocat/Hello-World")) | |||
.thenReturn(new GithubApplicationHttpClient.GetResponse() { | |||
@Override | |||
public Optional<String> getNextEndPoint() { | |||
return Optional.empty(); | |||
} | |||
@Override | |||
public int getCode() { | |||
return 200; | |||
} | |||
@Override | |||
public Optional<String> getContent() { | |||
return Optional.of(responseJson); | |||
} | |||
}); | |||
Optional<GithubApplicationClient.Repository> repository = underTest.getRepository(appUrl, accessToken, "octocat", "octocat/Hello-World"); | |||
assertThat(repository) | |||
.isPresent() | |||
.get() | |||
.extracting(GithubApplicationClient.Repository::getId, GithubApplicationClient.Repository::getName, GithubApplicationClient.Repository::getFullName, | |||
GithubApplicationClient.Repository::getUrl, GithubApplicationClient.Repository::isPrivate) | |||
.containsOnly(1296269L, "Hello-World", "octocat/Hello-World", "https://github.sonarsource.com/api/v3/repos/octocat/Hello-World", false); | |||
} | |||
private static class OkGetResponse extends Response { | |||
private OkGetResponse(String content) { | |||
super(200, content); | |||
} | |||
} | |||
private static class Response implements GithubApplicationHttpClient.GetResponse { | |||
private final int code; | |||
private final String content; | |||
private final String nextEndPoint; | |||
private Response(int code, @Nullable String content) { | |||
this(code, content, null); | |||
} | |||
private Response(int code, @Nullable String content, @Nullable String nextEndPoint) { | |||
this.code = code; | |||
this.content = content; | |||
this.nextEndPoint = nextEndPoint; | |||
} | |||
@Override | |||
public int getCode() { | |||
return code; | |||
} | |||
@Override | |||
public Optional<String> getContent() { | |||
return Optional.ofNullable(content); | |||
} | |||
@Override | |||
public Optional<String> getNextEndPoint() { | |||
return Optional.ofNullable(nextEndPoint); | |||
} | |||
} | |||
} |
@@ -0,0 +1,373 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2021 SonarSource SA | |||
* mailto:info AT sonarsource DOT com | |||
* | |||
* This program is free software; you can redistribute it and/or | |||
* modify it under the terms of the GNU Lesser General Public | |||
* License as published by the Free Software Foundation; either | |||
* version 3 of the License, or (at your option) any later version. | |||
* | |||
* This program is distributed in the hope that it will be useful, | |||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | |||
* Lesser General Public License for more details. | |||
* | |||
* You should have received a copy of the GNU Lesser General Public License | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
package org.sonar.alm.client.github; | |||
import com.tngtech.java.junit.dataprovider.DataProvider; | |||
import com.tngtech.java.junit.dataprovider.DataProviderRunner; | |||
import com.tngtech.java.junit.dataprovider.UseDataProvider; | |||
import java.io.IOException; | |||
import java.net.SocketTimeoutException; | |||
import okhttp3.mockwebserver.MockResponse; | |||
import okhttp3.mockwebserver.MockWebServer; | |||
import okhttp3.mockwebserver.RecordedRequest; | |||
import okhttp3.mockwebserver.SocketPolicy; | |||
import org.junit.Before; | |||
import org.junit.Rule; | |||
import org.junit.Test; | |||
import org.junit.runner.RunWith; | |||
import org.sonar.alm.client.ConstantTimeoutConfiguration; | |||
import org.sonar.alm.client.github.GithubApplicationHttpClient.GetResponse; | |||
import org.sonar.alm.client.github.GithubApplicationHttpClient.Response; | |||
import org.sonar.alm.client.github.security.AccessToken; | |||
import org.sonar.alm.client.github.security.UserAccessToken; | |||
import static java.lang.String.format; | |||
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.junit.Assert.fail; | |||
@RunWith(DataProviderRunner.class) | |||
public class GithubApplicationHttpClientImplTest { | |||
private static final String BETA_API_HEADER = "application/vnd.github.antiope-preview+json, application/vnd.github.machine-man-preview+json"; | |||
@Rule | |||
public MockWebServer server = new MockWebServer(); | |||
private GithubApplicationHttpClientImpl underTest; | |||
private final AccessToken accessToken = new UserAccessToken(randomAlphabetic(10)); | |||
private final String randomEndPoint = "/" + randomAlphabetic(10); | |||
private final String randomBody = randomAlphabetic(40); | |||
private String appUrl; | |||
@Before | |||
public void setUp() { | |||
this.appUrl = format("http://%s:%s", server.getHostName(), server.getPort()); | |||
this.underTest = new GithubApplicationHttpClientImpl(new ConstantTimeoutConfiguration(500)); | |||
} | |||
@Test | |||
public void get_fails_if_endpoint_does_not_start_with_slash() throws IOException { | |||
assertThatThrownBy(() -> underTest.get(appUrl, accessToken, "api/foo/bar")) | |||
.hasMessage("endpoint must start with '/' or 'http'") | |||
.isInstanceOf(IllegalArgumentException.class); | |||
} | |||
@Test | |||
public void get_fails_if_endpoint_does_not_start_with_http() throws IOException { | |||
assertThatThrownBy(() -> underTest.get(appUrl, accessToken, "ttp://api/foo/bar")) | |||
.isInstanceOf(IllegalArgumentException.class) | |||
.hasMessage("endpoint must start with '/' or 'http'"); | |||
} | |||
@Test | |||
public void get_fails_if_github_endpoint_is_invalid() throws IOException { | |||
assertThatThrownBy(() -> underTest.get("invalidUrl", accessToken, "/endpoint")) | |||
.isInstanceOf(IllegalArgumentException.class) | |||
.hasMessage("invalidUrl/endpoint is not a valid url"); | |||
} | |||
@Test | |||
public void get_adds_authentication_header_with_Bearer_type_and_Accept_header() throws IOException, InterruptedException { | |||
server.enqueue(new MockResponse()); | |||
GetResponse response = underTest.get(appUrl, accessToken, randomEndPoint); | |||
assertThat(response).isNotNull(); | |||
RecordedRequest recordedRequest = server.takeRequest(); | |||
assertThat(recordedRequest.getMethod()).isEqualTo("GET"); | |||
assertThat(recordedRequest.getPath()).isEqualTo(randomEndPoint); | |||
assertThat(recordedRequest.getHeader("Authorization")).isEqualTo("token " + accessToken.getValue()); | |||
assertThat(recordedRequest.getHeader("Accept")).isEqualTo(BETA_API_HEADER); | |||
} | |||
@Test | |||
public void get_returns_body_as_response_if_code_is_200() throws IOException { | |||
server.enqueue(new MockResponse().setResponseCode(200).setBody(randomBody)); | |||
GetResponse response = underTest.get(appUrl, accessToken, randomEndPoint); | |||
assertThat(response.getContent()).contains(randomBody); | |||
} | |||
@Test | |||
public void get_timeout() { | |||
server.enqueue(new MockResponse().setSocketPolicy(SocketPolicy.NO_RESPONSE)); | |||
try { | |||
underTest.get(appUrl, accessToken, randomEndPoint); | |||
fail("Expected timeout"); | |||
} catch (Exception e) { | |||
assertThat(e).isInstanceOf(SocketTimeoutException.class); | |||
} | |||
} | |||
@Test | |||
@UseDataProvider("someHttpCodesWithContentBut200") | |||
public void get_empty_response_if_code_is_not_200(int code) throws IOException { | |||
server.enqueue(new MockResponse().setResponseCode(code).setBody(randomBody)); | |||
GetResponse response = underTest.get(appUrl, accessToken, randomEndPoint); | |||
assertThat(response.getContent()).isEmpty(); | |||
} | |||
@Test | |||
public void get_returns_empty_endPoint_when_no_link_header() throws IOException { | |||
server.enqueue(new MockResponse().setBody(randomBody)); | |||
GetResponse response = underTest.get(appUrl, accessToken, randomEndPoint); | |||
assertThat(response.getNextEndPoint()).isEmpty(); | |||
} | |||
@Test | |||
public void get_returns_empty_endPoint_when_link_header_does_not_have_next_rel() throws IOException { | |||
server.enqueue(new MockResponse().setBody(randomBody) | |||
.setHeader("link", "<https://api.github.com/installation/repositories?per_page=5&page=4>; rel=\"prev\", " + | |||
"<https://api.github.com/installation/repositories?per_page=5&page=1>; rel=\"first\"")); | |||
GetResponse response = underTest.get(appUrl, accessToken, randomEndPoint); | |||
assertThat(response.getNextEndPoint()).isEmpty(); | |||
} | |||
@Test | |||
@UseDataProvider("linkHeadersWithNextRel") | |||
public void get_returns_endPoint_when_link_header_has_next_rel(String linkHeader) throws IOException { | |||
server.enqueue(new MockResponse().setBody(randomBody) | |||
.setHeader("link", linkHeader)); | |||
GetResponse response = underTest.get(appUrl, accessToken, randomEndPoint); | |||
assertThat(response.getNextEndPoint()).contains("https://api.github.com/installation/repositories?per_page=5&page=2"); | |||
} | |||
@DataProvider | |||
public static Object[][] linkHeadersWithNextRel() { | |||
String expected = "https://api.github.com/installation/repositories?per_page=5&page=2"; | |||
return new Object[][] { | |||
{"<" + expected + ">; rel=\"next\""}, | |||
{"<" + expected + ">; rel=\"next\", " + | |||
"<https://api.github.com/installation/repositories?per_page=5&page=1>; rel=\"first\""}, | |||
{"<https://api.github.com/installation/repositories?per_page=5&page=1>; rel=\"first\", " + | |||
"<" + expected + ">; rel=\"next\""}, | |||
{"<https://api.github.com/installation/repositories?per_page=5&page=1>; rel=\"first\", " + | |||
"<" + expected + ">; rel=\"next\", " + | |||
"<https://api.github.com/installation/repositories?per_page=5&page=5>; rel=\"last\""}, | |||
}; | |||
} | |||
@DataProvider | |||
public static Object[][] someHttpCodesWithContentBut200() { | |||
return new Object[][] { | |||
{201}, | |||
{202}, | |||
{203}, | |||
{404}, | |||
{500} | |||
}; | |||
} | |||
@Test | |||
public void post_fails_if_endpoint_does_not_start_with_slash() throws IOException { | |||
assertThatThrownBy(() -> underTest.post(appUrl, accessToken, "api/foo/bar")) | |||
.isInstanceOf(IllegalArgumentException.class) | |||
.hasMessage("endpoint must start with '/' or 'http'"); | |||
} | |||
@Test | |||
public void post_fails_if_endpoint_does_not_start_with_http() throws IOException { | |||
assertThatThrownBy(() -> underTest.post(appUrl, accessToken, "ttp://api/foo/bar")) | |||
.isInstanceOf(IllegalArgumentException.class) | |||
.hasMessage("endpoint must start with '/' or 'http'"); | |||
} | |||
@Test | |||
public void post_fails_if_github_endpoint_is_invalid() throws IOException { | |||
assertThatThrownBy(() -> underTest.post("invalidUrl", accessToken, "/endpoint")) | |||
.isInstanceOf(IllegalArgumentException.class) | |||
.hasMessage("invalidUrl/endpoint is not a valid url"); | |||
} | |||
@Test | |||
public void post_adds_authentication_header_with_Bearer_type_and_Accept_header() throws IOException, InterruptedException { | |||
server.enqueue(new MockResponse()); | |||
Response response = underTest.post(appUrl, accessToken, randomEndPoint); | |||
assertThat(response).isNotNull(); | |||
RecordedRequest recordedRequest = server.takeRequest(); | |||
assertThat(recordedRequest.getMethod()).isEqualTo("POST"); | |||
assertThat(recordedRequest.getPath()).isEqualTo(randomEndPoint); | |||
assertThat(recordedRequest.getHeader("Authorization")).isEqualTo("token " + accessToken.getValue()); | |||
assertThat(recordedRequest.getHeader("Accept")).isEqualTo(BETA_API_HEADER); | |||
} | |||
@Test | |||
public void post_returns_body_as_response_if_code_is_200() throws IOException { | |||
server.enqueue(new MockResponse().setResponseCode(200).setBody(randomBody)); | |||
Response response = underTest.post(appUrl, accessToken, randomEndPoint); | |||
assertThat(response.getContent()).contains(randomBody); | |||
} | |||
@Test | |||
public void post_returns_body_as_response_if_code_is_201() throws IOException { | |||
server.enqueue(new MockResponse().setResponseCode(201).setBody(randomBody)); | |||
Response response = underTest.post(appUrl, accessToken, randomEndPoint); | |||
assertThat(response.getContent()).contains(randomBody); | |||
} | |||
@Test | |||
public void post_returns_empty_response_if_code_is_204() throws IOException { | |||
server.enqueue(new MockResponse().setResponseCode(204)); | |||
Response response = underTest.post(appUrl, accessToken, randomEndPoint); | |||
assertThat(response.getContent()).isEmpty(); | |||
} | |||
@Test | |||
@UseDataProvider("httpCodesBut200_201And204") | |||
public void post_has_json_error_in_body_if_code_is_neither_200_201_nor_204(int code) throws IOException { | |||
server.enqueue(new MockResponse().setResponseCode(code).setBody(randomBody)); | |||
Response response = underTest.post(appUrl, accessToken, randomEndPoint); | |||
assertThat(response.getContent()).contains(randomBody); | |||
} | |||
@DataProvider | |||
public static Object[][] httpCodesBut200_201And204() { | |||
return new Object[][] { | |||
{202}, | |||
{203}, | |||
{400}, | |||
{401}, | |||
{403}, | |||
{404}, | |||
{500} | |||
}; | |||
} | |||
@Test | |||
public void post_with_json_body_adds_json_to_body_request() throws IOException, InterruptedException { | |||
server.enqueue(new MockResponse()); | |||
String jsonBody = "{\"foo\": \"bar\"}"; | |||
Response response = underTest.post(appUrl, accessToken, randomEndPoint, jsonBody); | |||
assertThat(response).isNotNull(); | |||
RecordedRequest recordedRequest = server.takeRequest(); | |||
assertThat(recordedRequest.getBody().readUtf8()).isEqualTo(jsonBody); | |||
} | |||
@Test | |||
public void patch_with_json_body_adds_json_to_body_request() throws IOException, InterruptedException { | |||
server.enqueue(new MockResponse()); | |||
String jsonBody = "{\"foo\": \"bar\"}"; | |||
Response response = underTest.patch(appUrl, accessToken, randomEndPoint, jsonBody); | |||
assertThat(response).isNotNull(); | |||
RecordedRequest recordedRequest = server.takeRequest(); | |||
assertThat(recordedRequest.getBody().readUtf8()).isEqualTo(jsonBody); | |||
} | |||
@Test | |||
public void patch_returns_body_as_response_if_code_is_200() throws IOException { | |||
server.enqueue(new MockResponse().setResponseCode(200).setBody(randomBody)); | |||
Response response = underTest.patch(appUrl, accessToken, randomEndPoint, "{}"); | |||
assertThat(response.getContent()).contains(randomBody); | |||
} | |||
@Test | |||
public void patch_returns_empty_response_if_code_is_204() throws IOException { | |||
server.enqueue(new MockResponse().setResponseCode(204)); | |||
Response response = underTest.patch(appUrl, accessToken, randomEndPoint, "{}"); | |||
assertThat(response.getContent()).isEmpty(); | |||
} | |||
@Test | |||
public void delete_returns_empty_response_if_code_is_204() throws IOException { | |||
server.enqueue(new MockResponse().setResponseCode(204)); | |||
Response response = underTest.delete(appUrl, accessToken, randomEndPoint); | |||
assertThat(response.getContent()).isEmpty(); | |||
} | |||
@DataProvider | |||
public static Object[][] httpCodesBut204() { | |||
return new Object[][] { | |||
{200}, | |||
{201}, | |||
{202}, | |||
{203}, | |||
{400}, | |||
{401}, | |||
{403}, | |||
{404}, | |||
{500} | |||
}; | |||
} | |||
@Test | |||
@UseDataProvider("httpCodesBut204") | |||
public void delete_returns_response_if_code_is_not_204(int code) throws IOException { | |||
server.enqueue(new MockResponse().setResponseCode(code).setBody(randomBody)); | |||
Response response = underTest.delete(appUrl, accessToken, randomEndPoint); | |||
assertThat(response.getContent()).hasValue(randomBody); | |||
} | |||
@DataProvider | |||
public static Object[][] httpCodesBut200And204() { | |||
return new Object[][] { | |||
{201}, | |||
{202}, | |||
{203}, | |||
{400}, | |||
{401}, | |||
{403}, | |||
{404}, | |||
{500} | |||
}; | |||
} | |||
@Test | |||
@UseDataProvider("httpCodesBut200And204") | |||
public void patch_has_json_error_in_body_if_code_is_neither_200_nor_204(int code) throws IOException { | |||
server.enqueue(new MockResponse().setResponseCode(code).setBody(randomBody)); | |||
Response response = underTest.patch(appUrl, accessToken, randomEndPoint, "{}"); | |||
assertThat(response.getContent()).contains(randomBody); | |||
} | |||
} |
@@ -26,6 +26,10 @@ import org.sonar.server.almintegration.ws.azure.SearchAzureReposAction; | |||
import org.sonar.server.almintegration.ws.bitbucketserver.ImportBitbucketServerProjectAction; | |||
import org.sonar.server.almintegration.ws.bitbucketserver.ListBitbucketServerProjectsAction; | |||
import org.sonar.server.almintegration.ws.bitbucketserver.SearchBitbucketServerReposAction; | |||
import org.sonar.server.almintegration.ws.github.GetGithubClientIdAction; | |||
import org.sonar.server.almintegration.ws.github.ImportGithubProjectAction; | |||
import org.sonar.server.almintegration.ws.github.ListGithubOrganizationsAction; | |||
import org.sonar.server.almintegration.ws.github.ListGithubRepositoriesAction; | |||
import org.sonar.server.almintegration.ws.gitlab.ImportGitLabProjectAction; | |||
import org.sonar.server.almintegration.ws.gitlab.SearchGitlabReposAction; | |||
@@ -36,6 +40,10 @@ public class AlmIntegrationsWSModule extends Module { | |||
ImportBitbucketServerProjectAction.class, | |||
ListBitbucketServerProjectsAction.class, | |||
SearchBitbucketServerReposAction.class, | |||
GetGithubClientIdAction.class, | |||
ImportGithubProjectAction.class, | |||
ListGithubOrganizationsAction.class, | |||
ListGithubRepositoriesAction.class, | |||
ImportGitLabProjectAction.class, | |||
SearchGitlabReposAction.class, | |||
ImportAzureProjectAction.class, |
@@ -0,0 +1,23 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2021 SonarSource SA | |||
* mailto:info AT sonarsource DOT com | |||
* | |||
* This program is free software; you can redistribute it and/or | |||
* modify it under the terms of the GNU Lesser General Public | |||
* License as published by the Free Software Foundation; either | |||
* version 3 of the License, or (at your option) any later version. | |||
* | |||
* This program is distributed in the hope that it will be useful, | |||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | |||
* Lesser General Public License for more details. | |||
* | |||
* You should have received a copy of the GNU Lesser General Public License | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
@ParametersAreNonnullByDefault | |||
package org.sonar.server.almintegration.ws.bitbucketserver; | |||
import javax.annotation.ParametersAreNonnullByDefault; |
@@ -0,0 +1,84 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2021 SonarSource SA | |||
* mailto:info AT sonarsource DOT com | |||
* | |||
* This program is free software; you can redistribute it and/or | |||
* modify it under the terms of the GNU Lesser General Public | |||
* License as published by the Free Software Foundation; either | |||
* version 3 of the License, or (at your option) any later version. | |||
* | |||
* This program is distributed in the hope that it will be useful, | |||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | |||
* Lesser General Public License for more details. | |||
* | |||
* You should have received a copy of the GNU Lesser General Public License | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
package org.sonar.server.almintegration.ws.github; | |||
import org.sonar.api.server.ws.Request; | |||
import org.sonar.api.server.ws.Response; | |||
import org.sonar.api.server.ws.WebService; | |||
import org.sonar.db.DbClient; | |||
import org.sonar.db.DbSession; | |||
import org.sonar.db.alm.setting.AlmSettingDto; | |||
import org.sonar.server.almintegration.ws.AlmIntegrationsWsAction; | |||
import org.sonar.server.exceptions.NotFoundException; | |||
import org.sonar.server.user.UserSession; | |||
import org.sonarqube.ws.AlmIntegrations; | |||
import static org.sonar.db.permission.GlobalPermission.PROVISION_PROJECTS; | |||
import static org.sonar.server.ws.WsUtils.writeProtobuf; | |||
public class GetGithubClientIdAction implements AlmIntegrationsWsAction { | |||
public static final String PARAM_ALM_SETTING = "almSetting"; | |||
private final DbClient dbClient; | |||
private final UserSession userSession; | |||
public GetGithubClientIdAction(DbClient dbClient, UserSession userSession) { | |||
this.dbClient = dbClient; | |||
this.userSession = userSession; | |||
} | |||
@Override | |||
public void define(WebService.NewController context) { | |||
WebService.NewAction action = context.createAction("get_github_client_id") | |||
.setDescription("Get the client id of a Github ALM Integration.") | |||
.setInternal(true) | |||
.setSince("8.4") | |||
.setHandler(this); | |||
action.createParam(PARAM_ALM_SETTING) | |||
.setRequired(true) | |||
.setMaximumLength(200) | |||
.setDescription("ALM setting key"); | |||
} | |||
@Override | |||
public void handle(Request request, Response response) { | |||
AlmIntegrations.GithubClientIdWsResponse getResponse = doHandle(request); | |||
writeProtobuf(getResponse, request, response); | |||
} | |||
private AlmIntegrations.GithubClientIdWsResponse doHandle(Request request) { | |||
try (DbSession dbSession = dbClient.openSession(false)) { | |||
userSession.checkLoggedIn().checkPermission(PROVISION_PROJECTS); | |||
String almSettingKey = request.mandatoryParam(PARAM_ALM_SETTING); | |||
AlmSettingDto almSetting = dbClient.almSettingDao().selectByKey(dbSession, almSettingKey) | |||
.orElseThrow(() -> new NotFoundException(String.format("Github ALM Setting '%s' not found", almSettingKey))); | |||
if (almSetting.getClientId() == null) { | |||
throw new NotFoundException(String.format("No client ID for setting with key '%s'", almSettingKey)); | |||
} | |||
return AlmIntegrations.GithubClientIdWsResponse.newBuilder() | |||
.setClientId(almSetting.getClientId()) | |||
.build(); | |||
} | |||
} | |||
} |
@@ -0,0 +1,157 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2021 SonarSource SA | |||
* mailto:info AT sonarsource DOT com | |||
* | |||
* This program is free software; you can redistribute it and/or | |||
* modify it under the terms of the GNU Lesser General Public | |||
* License as published by the Free Software Foundation; either | |||
* version 3 of the License, or (at your option) any later version. | |||
* | |||
* This program is distributed in the hope that it will be useful, | |||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | |||
* Lesser General Public License for more details. | |||
* | |||
* You should have received a copy of the GNU Lesser General Public License | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
package org.sonar.server.almintegration.ws.github; | |||
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.Request; | |||
import org.sonar.api.server.ws.Response; | |||
import org.sonar.api.server.ws.WebService; | |||
import org.sonar.db.DbClient; | |||
import org.sonar.db.DbSession; | |||
import org.sonar.db.alm.pat.AlmPatDto; | |||
import org.sonar.db.alm.setting.AlmSettingDto; | |||
import org.sonar.db.alm.setting.ProjectAlmSettingDto; | |||
import org.sonar.db.component.ComponentDto; | |||
import org.sonar.server.almintegration.ws.AlmIntegrationsWsAction; | |||
import org.sonar.server.almintegration.ws.ImportHelper; | |||
import org.sonar.server.component.ComponentUpdater; | |||
import org.sonar.server.exceptions.NotFoundException; | |||
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.server.almintegration.ws.ImportHelper.PARAM_ALM_SETTING; | |||
import static org.sonar.server.almintegration.ws.ImportHelper.toCreateResponse; | |||
import static org.sonar.server.component.NewComponent.newComponentBuilder; | |||
import static org.sonar.server.ws.WsUtils.writeProtobuf; | |||
public class ImportGithubProjectAction implements AlmIntegrationsWsAction { | |||
public static final String PARAM_ORGANIZATION = "organization"; | |||
public static final String PARAM_REPOSITORY_KEY = "repositoryKey"; | |||
private final DbClient dbClient; | |||
private final UserSession userSession; | |||
private final ProjectDefaultVisibility projectDefaultVisibility; | |||
private final GithubApplicationClient githubApplicationClient; | |||
private final ComponentUpdater componentUpdater; | |||
private final ImportHelper importHelper; | |||
public ImportGithubProjectAction(DbClient dbClient, UserSession userSession, ProjectDefaultVisibility projectDefaultVisibility, | |||
GithubApplicationClientImpl githubApplicationClient, ComponentUpdater componentUpdater, ImportHelper importHelper) { | |||
this.dbClient = dbClient; | |||
this.userSession = userSession; | |||
this.projectDefaultVisibility = projectDefaultVisibility; | |||
this.githubApplicationClient = githubApplicationClient; | |||
this.componentUpdater = componentUpdater; | |||
this.importHelper = importHelper; | |||
} | |||
@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.<br/>" + | |||
"Requires the 'Create Projects' permission") | |||
.setPost(true) | |||
.setInternal(true) | |||
.setSince("8.4") | |||
.setHandler(this); | |||
action.createParam(PARAM_ALM_SETTING) | |||
.setRequired(true) | |||
.setMaximumLength(200) | |||
.setDescription("ALM setting key"); | |||
action.createParam(PARAM_ORGANIZATION) | |||
.setRequired(true) | |||
.setMaximumLength(200) | |||
.setDescription("GitHub organization"); | |||
action.createParam(PARAM_REPOSITORY_KEY) | |||
.setRequired(true) | |||
.setMaximumLength(256) | |||
.setDescription("GitHub repository key"); | |||
} | |||
@Override | |||
public void handle(Request request, Response response) { | |||
Projects.CreateWsResponse createResponse = doHandle(request); | |||
writeProtobuf(createResponse, request, response); | |||
} | |||
private Projects.CreateWsResponse doHandle(Request request) { | |||
importHelper.checkProvisionProjectPermission(); | |||
AlmSettingDto almSettingDto = importHelper.getAlmSetting(request); | |||
String userUuid = importHelper.getUserUuid(); | |||
try (DbSession dbSession = dbClient.openSession(false)) { | |||
AccessToken accessToken = dbClient.almPatDao().selectByUserAndAlmSetting(dbSession, userUuid, almSettingDto) | |||
.map(AlmPatDto::getPersonalAccessToken) | |||
.map(UserAccessToken::new) | |||
.orElseThrow(() -> new IllegalArgumentException("No personal access token found")); | |||
String githubOrganization = request.mandatoryParam(PARAM_ORGANIZATION); | |||
String repositoryKey = request.mandatoryParam(PARAM_REPOSITORY_KEY); | |||
String url = requireNonNull(almSettingDto.getUrl(), "ALM url cannot be null"); | |||
Repository repository = githubApplicationClient.getRepository(url, accessToken, githubOrganization, repositoryKey) | |||
.orElseThrow(() -> new NotFoundException(String.format("GitHub repository '%s' not found", repositoryKey))); | |||
ComponentDto componentDto = createProject(dbSession, repository); | |||
populatePRSetting(dbSession, repository, componentDto, almSettingDto); | |||
return toCreateResponse(componentDto); | |||
} | |||
} | |||
private ComponentDto createProject(DbSession dbSession, Repository repo) { | |||
boolean visibility = projectDefaultVisibility.get(dbSession).isPrivate(); | |||
return componentUpdater.create(dbSession, newComponentBuilder() | |||
.setKey(getProjectKeyFromRepository(repo)) | |||
.setName(repo.getName()) | |||
.setPrivate(visibility) | |||
.setQualifier(PROJECT) | |||
.build(), | |||
userSession.getUuid()); | |||
} | |||
static String getProjectKeyFromRepository(Repository repo) { | |||
return repo.getFullName().replace("/", "_"); | |||
} | |||
private void populatePRSetting(DbSession dbSession, Repository repo, ComponentDto componentDto, AlmSettingDto almSettingDto) { | |||
ProjectAlmSettingDto projectAlmSettingDto = new ProjectAlmSettingDto() | |||
.setAlmSettingUuid(almSettingDto.getUuid()) | |||
.setAlmRepo(repo.getFullName()) | |||
.setAlmSlug(null) | |||
.setProjectUuid(componentDto.uuid()) | |||
.setSummaryCommentEnabled(true) | |||
.setMonorepo(false); | |||
dbClient.projectAlmSettingDao().insertOrUpdate(dbSession, projectAlmSettingDto); | |||
dbSession.commit(); | |||
} | |||
} |
@@ -0,0 +1,164 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2021 SonarSource SA | |||
* mailto:info AT sonarsource DOT com | |||
* | |||
* This program is free software; you can redistribute it and/or | |||
* modify it under the terms of the GNU Lesser General Public | |||
* License as published by the Free Software Foundation; either | |||
* version 3 of the License, or (at your option) any later version. | |||
* | |||
* This program is distributed in the hope that it will be useful, | |||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | |||
* Lesser General Public License for more details. | |||
* | |||
* You should have received a copy of the GNU Lesser General Public License | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
package org.sonar.server.almintegration.ws.github; | |||
import java.util.List; | |||
import java.util.Optional; | |||
import org.sonar.alm.client.github.GithubApplicationClient; | |||
import org.sonar.alm.client.github.GithubApplicationClient.Organization; | |||
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.Request; | |||
import org.sonar.api.server.ws.Response; | |||
import org.sonar.api.server.ws.WebService; | |||
import org.sonar.db.DbClient; | |||
import org.sonar.db.DbSession; | |||
import org.sonar.db.alm.pat.AlmPatDto; | |||
import org.sonar.db.alm.setting.AlmSettingDto; | |||
import org.sonar.server.almintegration.ws.AlmIntegrationsWsAction; | |||
import org.sonar.server.exceptions.BadRequestException; | |||
import org.sonar.server.exceptions.NotFoundException; | |||
import org.sonar.server.user.UserSession; | |||
import org.sonarqube.ws.AlmIntegrations; | |||
import org.sonarqube.ws.AlmIntegrations.ListGithubOrganizationsWsResponse; | |||
import org.sonarqube.ws.Common; | |||
import static java.util.Objects.requireNonNull; | |||
import static org.sonar.api.server.ws.WebService.Param.PAGE; | |||
import static org.sonar.api.server.ws.WebService.Param.PAGE_SIZE; | |||
import static org.sonar.db.permission.GlobalPermission.PROVISION_PROJECTS; | |||
import static org.sonar.server.ws.WsUtils.writeProtobuf; | |||
public class ListGithubOrganizationsAction implements AlmIntegrationsWsAction { | |||
public static final String PARAM_ALM_SETTING = "almSetting"; | |||
public static final String PARAM_TOKEN = "token"; | |||
private final DbClient dbClient; | |||
private final UserSession userSession; | |||
private final GithubApplicationClient githubApplicationClient; | |||
public ListGithubOrganizationsAction(DbClient dbClient, UserSession userSession, GithubApplicationClientImpl githubApplicationClient) { | |||
this.dbClient = dbClient; | |||
this.userSession = userSession; | |||
this.githubApplicationClient = githubApplicationClient; | |||
} | |||
@Override | |||
public void define(WebService.NewController context) { | |||
WebService.NewAction action = context.createAction("list_github_organizations") | |||
.setDescription("List GitHub organizations<br/>" + | |||
"Requires the 'Create Projects' permission") | |||
.setInternal(true) | |||
.setSince("8.4") | |||
.setHandler(this); | |||
action.createParam(PARAM_ALM_SETTING) | |||
.setRequired(true) | |||
.setMaximumLength(200) | |||
.setDescription("ALM setting key"); | |||
action.createParam(PARAM_TOKEN) | |||
.setMaximumLength(200) | |||
.setDescription("Github authorization code"); | |||
action.createParam(PAGE) | |||
.setDescription("Index of the page to display") | |||
.setDefaultValue(1); | |||
action.createParam(PAGE_SIZE) | |||
.setDescription("Size for the paging to apply") | |||
.setDefaultValue(100); | |||
} | |||
@Override | |||
public void handle(Request request, Response response) { | |||
ListGithubOrganizationsWsResponse getResponse = doHandle(request); | |||
writeProtobuf(getResponse, request, response); | |||
} | |||
private ListGithubOrganizationsWsResponse doHandle(Request request) { | |||
try (DbSession dbSession = dbClient.openSession(false)) { | |||
userSession.checkLoggedIn().checkPermission(PROVISION_PROJECTS); | |||
String almSettingKey = request.mandatoryParam(PARAM_ALM_SETTING); | |||
AlmSettingDto almSettingDto = dbClient.almSettingDao().selectByKey(dbSession, almSettingKey) | |||
.orElseThrow(() -> new NotFoundException(String.format("GitHub ALM Setting '%s' not found", almSettingKey))); | |||
String userUuid = requireNonNull(userSession.getUuid(), "User UUID is not null"); | |||
String url = requireNonNull(almSettingDto.getUrl(), String.format("No URL set for GitHub ALM '%s'", almSettingKey)); | |||
AccessToken accessToken; | |||
if (request.hasParam(PARAM_TOKEN)) { | |||
String code = request.mandatoryParam(PARAM_TOKEN); | |||
String clientId = requireNonNull(almSettingDto.getClientId(), String.format("No clientId set for GitHub ALM '%s'", almSettingKey)); | |||
String clientSecret = requireNonNull(almSettingDto.getClientSecret(), String.format("No clientSecret set for GitHub ALM '%s'", almSettingKey)); | |||
try { | |||
accessToken = githubApplicationClient.createUserAccessToken(url, clientId, clientSecret, code); | |||
} catch (IllegalArgumentException e) { | |||
// it could also be that the code has expired! | |||
throw BadRequestException.create("Unable to authenticate with GitHub. " | |||
+ "Check the GitHub App client ID and client secret configured in the Global Settings and try again."); | |||
} | |||
Optional<AlmPatDto> almPatDto = dbClient.almPatDao().selectByUserAndAlmSetting(dbSession, userUuid, almSettingDto); | |||
if (almPatDto.isPresent()) { | |||
AlmPatDto almPat = almPatDto.get(); | |||
almPat.setPersonalAccessToken(accessToken.getValue()); | |||
dbClient.almPatDao().update(dbSession, almPat); | |||
} else { | |||
AlmPatDto almPat = new AlmPatDto() | |||
.setPersonalAccessToken(accessToken.getValue()) | |||
.setAlmSettingUuid(almSettingDto.getUuid()) | |||
.setUserUuid(userUuid); | |||
dbClient.almPatDao().insert(dbSession, almPat); | |||
} | |||
dbSession.commit(); | |||
} else { | |||
accessToken = dbClient.almPatDao().selectByUserAndAlmSetting(dbSession, userUuid, almSettingDto) | |||
.map(AlmPatDto::getPersonalAccessToken) | |||
.map(UserAccessToken::new) | |||
.orElseThrow(() -> new IllegalArgumentException("No personal access token found")); | |||
} | |||
int page = request.hasParam(PAGE) ? request.mandatoryParamAsInt(PAGE) : 1; | |||
int pageSize = request.hasParam(PAGE_SIZE) ? request.mandatoryParamAsInt(PAGE_SIZE) : 100; | |||
GithubApplicationClient.Organizations githubOrganizations = githubApplicationClient.listOrganizations(url, accessToken, page, pageSize); | |||
ListGithubOrganizationsWsResponse.Builder response = ListGithubOrganizationsWsResponse.newBuilder() | |||
.setPaging(Common.Paging.newBuilder() | |||
.setPageIndex(page) | |||
.setPageSize(pageSize) | |||
.setTotal(githubOrganizations.getTotal()) | |||
.build()); | |||
List<Organization> organizations = githubOrganizations.getOrganizations(); | |||
if (organizations != null) { | |||
organizations | |||
.forEach(githubOrganization -> response.addOrganizations(AlmIntegrations.GithubOrganization.newBuilder() | |||
.setKey(githubOrganization.getLogin()) | |||
.setName(githubOrganization.getLogin()) | |||
.build())); | |||
} | |||
return response.build(); | |||
} | |||
} | |||
} |
@@ -0,0 +1,150 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2021 SonarSource SA | |||
* mailto:info AT sonarsource DOT com | |||
* | |||
* This program is free software; you can redistribute it and/or | |||
* modify it under the terms of the GNU Lesser General Public | |||
* License as published by the Free Software Foundation; either | |||
* version 3 of the License, or (at your option) any later version. | |||
* | |||
* This program is distributed in the hope that it will be useful, | |||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | |||
* Lesser General Public License for more details. | |||
* | |||
* You should have received a copy of the GNU Lesser General Public License | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
package org.sonar.server.almintegration.ws.github; | |||
import java.util.List; | |||
import java.util.Optional; | |||
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.Request; | |||
import org.sonar.api.server.ws.Response; | |||
import org.sonar.api.server.ws.WebService; | |||
import org.sonar.db.DbClient; | |||
import org.sonar.db.DbSession; | |||
import org.sonar.db.alm.pat.AlmPatDto; | |||
import org.sonar.db.alm.setting.AlmSettingDto; | |||
import org.sonar.db.project.ProjectDto; | |||
import org.sonar.server.almintegration.ws.AlmIntegrationsWsAction; | |||
import org.sonar.server.exceptions.NotFoundException; | |||
import org.sonar.server.user.UserSession; | |||
import org.sonarqube.ws.AlmIntegrations; | |||
import org.sonarqube.ws.Common; | |||
import static java.util.Objects.requireNonNull; | |||
import static org.sonar.api.server.ws.WebService.Param.PAGE; | |||
import static org.sonar.api.server.ws.WebService.Param.PAGE_SIZE; | |||
import static org.sonar.api.server.ws.WebService.Param.TEXT_QUERY; | |||
import static org.sonar.db.permission.GlobalPermission.PROVISION_PROJECTS; | |||
import static org.sonar.server.almintegration.ws.github.ImportGithubProjectAction.getProjectKeyFromRepository; | |||
import static org.sonar.server.ws.WsUtils.writeProtobuf; | |||
public class ListGithubRepositoriesAction implements AlmIntegrationsWsAction { | |||
public static final String PARAM_ALM_SETTING = "almSetting"; | |||
public static final String PARAM_ORGANIZATION = "organization"; | |||
private final DbClient dbClient; | |||
private final UserSession userSession; | |||
private final GithubApplicationClient githubApplicationClient; | |||
public ListGithubRepositoriesAction(DbClient dbClient, UserSession userSession, GithubApplicationClientImpl githubApplicationClient) { | |||
this.dbClient = dbClient; | |||
this.userSession = userSession; | |||
this.githubApplicationClient = githubApplicationClient; | |||
} | |||
@Override | |||
public void define(WebService.NewController context) { | |||
WebService.NewAction action = context.createAction("list_github_repositories") | |||
.setDescription("List the GitHub repositories for an organization<br/>" + | |||
"Requires the 'Create Projects' permission") | |||
.setInternal(true) | |||
.setSince("8.4") | |||
.setHandler(this); | |||
action.createParam(PARAM_ALM_SETTING) | |||
.setRequired(true) | |||
.setMaximumLength(200) | |||
.setDescription("ALM setting key"); | |||
action.createParam(PARAM_ORGANIZATION) | |||
.setRequired(true) | |||
.setMaximumLength(200) | |||
.setDescription("Github organization"); | |||
action.createParam(TEXT_QUERY) | |||
.setDescription("Limit search to repositories that contain the supplied string") | |||
.setExampleValue("Apache"); | |||
action.createParam(PAGE) | |||
.setDescription("Index of the page to display") | |||
.setDefaultValue(1); | |||
action.createParam(PAGE_SIZE) | |||
.setDescription("Size for the paging to apply") | |||
.setDefaultValue(100); | |||
} | |||
@Override | |||
public void handle(Request request, Response response) { | |||
AlmIntegrations.ListGithubRepositoriesWsResponse getResponse = doHandle(request); | |||
writeProtobuf(getResponse, request, response); | |||
} | |||
private AlmIntegrations.ListGithubRepositoriesWsResponse doHandle(Request request) { | |||
try (DbSession dbSession = dbClient.openSession(false)) { | |||
userSession.checkLoggedIn().checkPermission(PROVISION_PROJECTS); | |||
String almSettingKey = request.mandatoryParam(PARAM_ALM_SETTING); | |||
AlmSettingDto almSettingDto = dbClient.almSettingDao().selectByKey(dbSession, almSettingKey) | |||
.orElseThrow(() -> new NotFoundException(String.format("GitHub ALM Setting '%s' not found", almSettingKey))); | |||
String userUuid = requireNonNull(userSession.getUuid(), "User UUID is not null"); | |||
String url = requireNonNull(almSettingDto.getUrl(), String.format("No URL set for GitHub ALM '%s'", almSettingKey)); | |||
AccessToken accessToken = dbClient.almPatDao().selectByUserAndAlmSetting(dbSession, userUuid, almSettingDto) | |||
.map(AlmPatDto::getPersonalAccessToken) | |||
.map(UserAccessToken::new) | |||
.orElseThrow(() -> new IllegalArgumentException("No personal access token found")); | |||
int pageIndex = request.hasParam(PAGE) ? request.mandatoryParamAsInt(PAGE) : 1; | |||
int pageSize = request.hasParam(PAGE_SIZE) ? request.mandatoryParamAsInt(PAGE_SIZE) : 100; | |||
GithubApplicationClient.Repositories repositories = githubApplicationClient | |||
.listRepositories(url, accessToken, request.mandatoryParam(PARAM_ORGANIZATION), request.param(TEXT_QUERY), pageIndex, pageSize); | |||
AlmIntegrations.ListGithubRepositoriesWsResponse.Builder response = AlmIntegrations.ListGithubRepositoriesWsResponse.newBuilder() | |||
.setPaging(Common.Paging.newBuilder() | |||
.setPageIndex(pageIndex) | |||
.setPageSize(pageSize) | |||
.setTotal(repositories.getTotal()) | |||
.build()); | |||
List<Repository> repositoryList = repositories.getRepositories(); | |||
if (repositoryList != null) { | |||
repositoryList.forEach(repository -> { | |||
Optional<String> sonarQubeKey = dbClient.projectDao().selectProjectByKey(dbSession, getProjectKeyFromRepository(repository)).map(ProjectDto::getKey); | |||
response.addRepositories(AlmIntegrations.GithubRepository.newBuilder() | |||
.setId(repository.getId()) | |||
.setKey(repository.getFullName()) | |||
.setName(repository.getName()) | |||
.setUrl(repository.getUrl()) | |||
.setSqProjectKey(sonarQubeKey.orElse("")) | |||
.build()); | |||
} | |||
); | |||
} | |||
return response.build(); | |||
} | |||
} | |||
} |
@@ -0,0 +1,23 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2021 SonarSource SA | |||
* mailto:info AT sonarsource DOT com | |||
* | |||
* This program is free software; you can redistribute it and/or | |||
* modify it under the terms of the GNU Lesser General Public | |||
* License as published by the Free Software Foundation; either | |||
* version 3 of the License, or (at your option) any later version. | |||
* | |||
* This program is distributed in the hope that it will be useful, | |||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | |||
* Lesser General Public License for more details. | |||
* | |||
* You should have received a copy of the GNU Lesser General Public License | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
@ParametersAreNonnullByDefault | |||
package org.sonar.server.almintegration.ws.github; | |||
import javax.annotation.ParametersAreNonnullByDefault; |
@@ -0,0 +1,95 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2021 SonarSource SA | |||
* mailto:info AT sonarsource DOT com | |||
* | |||
* This program is free software; you can redistribute it and/or | |||
* modify it under the terms of the GNU Lesser General Public | |||
* License as published by the Free Software Foundation; either | |||
* version 3 of the License, or (at your option) any later version. | |||
* | |||
* This program is distributed in the hope that it will be useful, | |||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | |||
* Lesser General Public License for more details. | |||
* | |||
* You should have received a copy of the GNU Lesser General Public License | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
package org.sonar.server.almintegration.ws.github; | |||
import org.junit.Rule; | |||
import org.junit.Test; | |||
import org.sonar.api.utils.System2; | |||
import org.sonar.db.DbTester; | |||
import org.sonar.db.alm.setting.AlmSettingDto; | |||
import org.sonar.db.permission.GlobalPermission; | |||
import org.sonar.db.user.UserDto; | |||
import org.sonar.server.exceptions.NotFoundException; | |||
import org.sonar.server.exceptions.UnauthorizedException; | |||
import org.sonar.server.tester.UserSessionRule; | |||
import org.sonar.server.ws.TestRequest; | |||
import org.sonar.server.ws.WsActionTester; | |||
import org.sonarqube.ws.AlmIntegrations; | |||
import static org.assertj.core.api.Assertions.assertThat; | |||
import static org.assertj.core.api.Assertions.assertThatThrownBy; | |||
import static org.mockito.Mockito.mock; | |||
import static org.sonar.server.tester.UserSessionRule.standalone; | |||
public class GetGithubClientIdActionTest { | |||
@Rule | |||
public UserSessionRule userSession = standalone(); | |||
private final System2 system2 = mock(System2.class); | |||
@Rule | |||
public DbTester db = DbTester.create(system2); | |||
private final WsActionTester ws = new WsActionTester(new GetGithubClientIdAction(db.getDbClient(), userSession)); | |||
@Test | |||
public void get_client_id() { | |||
UserDto user = db.users().insertUser(); | |||
userSession.logIn(user).addPermission(GlobalPermission.PROVISION_PROJECTS); | |||
AlmSettingDto githubAlmSetting = db.almSettings().insertGitHubAlmSetting(alm -> alm.setClientId("client_123").setClientSecret("client_secret_123")); | |||
AlmIntegrations.GithubClientIdWsResponse response = ws.newRequest().setParam(GetGithubClientIdAction.PARAM_ALM_SETTING, githubAlmSetting.getKey()) | |||
.executeProtobuf(AlmIntegrations.GithubClientIdWsResponse.class); | |||
assertThat(response.getClientId()).isEqualTo(githubAlmSetting.getClientId()); | |||
} | |||
@Test | |||
public void fail_when_missing_create_project_permission() { | |||
TestRequest request = ws.newRequest(); | |||
assertThatThrownBy(request::execute) | |||
.isInstanceOf(UnauthorizedException.class); | |||
} | |||
@Test | |||
public void fail_when_almSetting_does_not_exist() { | |||
UserDto user = db.users().insertUser(); | |||
userSession.logIn(user).addPermission(GlobalPermission.PROVISION_PROJECTS); | |||
TestRequest request = ws.newRequest().setParam(GetGithubClientIdAction.PARAM_ALM_SETTING, "unknown"); | |||
assertThatThrownBy(request::execute) | |||
.isInstanceOf(NotFoundException.class) | |||
.hasMessage("Github ALM Setting 'unknown' not found"); | |||
} | |||
@Test | |||
public void fail_when_client_id_does_not_exist() { | |||
UserDto user = db.users().insertUser(); | |||
userSession.logIn(user).addPermission(GlobalPermission.PROVISION_PROJECTS); | |||
AlmSettingDto githubAlmSetting = db.almSettings().insertGitHubAlmSetting(); | |||
TestRequest request = ws.newRequest() | |||
.setParam(GetGithubClientIdAction.PARAM_ALM_SETTING, githubAlmSetting.getKey()); | |||
assertThatThrownBy(request::execute) | |||
.isInstanceOf(NotFoundException.class) | |||
.hasMessage("No client ID for setting with key '%s'", githubAlmSetting.getKey()); | |||
} | |||
} |
@@ -0,0 +1,198 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2021 SonarSource SA | |||
* mailto:info AT sonarsource DOT com | |||
* | |||
* This program is free software; you can redistribute it and/or | |||
* modify it under the terms of the GNU Lesser General Public | |||
* License as published by the Free Software Foundation; either | |||
* version 3 of the License, or (at your option) any later version. | |||
* | |||
* This program is distributed in the hope that it will be useful, | |||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | |||
* Lesser General Public License for more details. | |||
* | |||
* You should have received a copy of the GNU Lesser General Public License | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
package org.sonar.server.almintegration.ws.github; | |||
import java.util.Optional; | |||
import org.junit.Before; | |||
import org.junit.Rule; | |||
import org.junit.Test; | |||
import org.sonar.alm.client.github.GithubApplicationClient; | |||
import org.sonar.alm.client.github.GithubApplicationClientImpl; | |||
import org.sonar.api.server.ws.WebService; | |||
import org.sonar.api.utils.System2; | |||
import org.sonar.core.i18n.I18n; | |||
import org.sonar.core.util.SequenceUuidFactory; | |||
import org.sonar.db.DbTester; | |||
import org.sonar.db.alm.setting.AlmSettingDto; | |||
import org.sonar.db.permission.GlobalPermission; | |||
import org.sonar.db.project.ProjectDto; | |||
import org.sonar.db.user.UserDto; | |||
import org.sonar.server.almintegration.ws.ImportHelper; | |||
import org.sonar.server.component.ComponentUpdater; | |||
import org.sonar.server.es.TestProjectIndexers; | |||
import org.sonar.server.exceptions.BadRequestException; | |||
import org.sonar.server.exceptions.NotFoundException; | |||
import org.sonar.server.exceptions.UnauthorizedException; | |||
import org.sonar.server.favorite.FavoriteUpdater; | |||
import org.sonar.server.permission.PermissionTemplateService; | |||
import org.sonar.server.project.ProjectDefaultVisibility; | |||
import org.sonar.server.project.Visibility; | |||
import org.sonar.server.tester.UserSessionRule; | |||
import org.sonar.server.ws.TestRequest; | |||
import org.sonar.server.ws.WsActionTester; | |||
import org.sonarqube.ws.Projects; | |||
import static org.assertj.core.api.Assertions.assertThat; | |||
import static org.assertj.core.api.Assertions.assertThatThrownBy; | |||
import static org.assertj.core.api.Assertions.tuple; | |||
import static org.mockito.ArgumentMatchers.any; | |||
import static org.mockito.Mockito.mock; | |||
import static org.mockito.Mockito.when; | |||
import static org.sonar.server.almintegration.ws.ImportHelper.PARAM_ALM_SETTING; | |||
import static org.sonar.server.almintegration.ws.github.ImportGithubProjectAction.PARAM_ORGANIZATION; | |||
import static org.sonar.server.almintegration.ws.github.ImportGithubProjectAction.PARAM_REPOSITORY_KEY; | |||
import static org.sonar.server.tester.UserSessionRule.standalone; | |||
public class ImportGithubProjectActionTest { | |||
@Rule | |||
public UserSessionRule userSession = standalone(); | |||
private final System2 system2 = mock(System2.class); | |||
private final GithubApplicationClientImpl appClient = mock(GithubApplicationClientImpl.class); | |||
@Rule | |||
public DbTester db = DbTester.create(system2); | |||
private final ComponentUpdater componentUpdater = new ComponentUpdater(db.getDbClient(), mock(I18n.class), System2.INSTANCE, | |||
mock(PermissionTemplateService.class), new FavoriteUpdater(db.getDbClient()), new TestProjectIndexers(), new SequenceUuidFactory()); | |||
private final ImportHelper importHelper = new ImportHelper(db.getDbClient(), userSession); | |||
private final ProjectDefaultVisibility projectDefaultVisibility = mock(ProjectDefaultVisibility.class); | |||
private final WsActionTester ws = new WsActionTester(new ImportGithubProjectAction(db.getDbClient(), userSession, | |||
projectDefaultVisibility, appClient, componentUpdater, importHelper)); | |||
@Before | |||
public void before() { | |||
when(projectDefaultVisibility.get(any())).thenReturn(Visibility.PRIVATE); | |||
} | |||
@Test | |||
public void import_project() { | |||
AlmSettingDto githubAlmSetting = setupAlm(); | |||
db.almPats().insert(p -> p.setAlmSettingUuid(githubAlmSetting.getUuid()).setUserUuid(userSession.getUuid())); | |||
GithubApplicationClient.Repository repository = new GithubApplicationClient.Repository(1L, "Hello-World", false, "octocat/Hello-World", | |||
"https://github.sonarsource.com/api/v3/repos/octocat/Hello-World"); | |||
when(appClient.getRepository(any(), any(), any(), any())) | |||
.thenReturn(Optional.of(repository)); | |||
Projects.CreateWsResponse response = ws.newRequest() | |||
.setParam(PARAM_ALM_SETTING, githubAlmSetting.getKey()) | |||
.setParam(PARAM_ORGANIZATION, "octocat") | |||
.setParam(PARAM_REPOSITORY_KEY, "octocat/Hello-World") | |||
.executeProtobuf(Projects.CreateWsResponse.class); | |||
Projects.CreateWsResponse.Project result = response.getProject(); | |||
assertThat(result.getKey()).isEqualTo(repository.getFullName().replace("/", "_")); | |||
assertThat(result.getName()).isEqualTo(repository.getName()); | |||
Optional<ProjectDto> projectDto = db.getDbClient().projectDao().selectProjectByKey(db.getSession(), result.getKey()); | |||
assertThat(projectDto).isPresent(); | |||
assertThat(db.getDbClient().projectAlmSettingDao().selectByProject(db.getSession(), projectDto.get())).isPresent(); | |||
} | |||
@Test | |||
public void fail_project_already_exist() { | |||
AlmSettingDto githubAlmSetting = setupAlm(); | |||
db.almPats().insert(p -> p.setAlmSettingUuid(githubAlmSetting.getUuid()).setUserUuid(userSession.getUuid())); | |||
db.components().insertPublicProject(p -> p.setDbKey("octocat_Hello-World")); | |||
GithubApplicationClient.Repository repository = new GithubApplicationClient.Repository(1L, "Hello-World", false, "octocat/Hello-World", | |||
"https://github.sonarsource.com/api/v3/repos/octocat/Hello-World"); | |||
when(appClient.getRepository(any(), any(), any(), any())) | |||
.thenReturn(Optional.of(repository)); | |||
TestRequest request = ws.newRequest() | |||
.setParam(PARAM_ALM_SETTING, githubAlmSetting.getKey()) | |||
.setParam(PARAM_ORGANIZATION, "octocat") | |||
.setParam(PARAM_REPOSITORY_KEY, "octocat/Hello-World"); | |||
assertThatThrownBy(() -> request.execute()) | |||
.isInstanceOf(BadRequestException.class) | |||
.hasMessage("Could not create null, key already exists: octocat_Hello-World"); | |||
} | |||
@Test | |||
public void fail_when_not_logged_in() { | |||
TestRequest request = ws.newRequest() | |||
.setParam(PARAM_ALM_SETTING, "asdfghjkl") | |||
.setParam(PARAM_ORGANIZATION, "test") | |||
.setParam(PARAM_REPOSITORY_KEY, "test/repo"); | |||
assertThatThrownBy(() -> request | |||
.execute()) | |||
.isInstanceOf(UnauthorizedException.class); | |||
} | |||
@Test | |||
public void fail_when_missing_create_project_permission() { | |||
TestRequest request = ws.newRequest(); | |||
assertThatThrownBy(() -> request.execute()) | |||
.isInstanceOf(UnauthorizedException.class); | |||
} | |||
@Test | |||
public void fail_when_almSetting_does_not_exist() { | |||
UserDto user = db.users().insertUser(); | |||
userSession.logIn(user).addPermission(GlobalPermission.PROVISION_PROJECTS); | |||
TestRequest request = ws.newRequest() | |||
.setParam(PARAM_ALM_SETTING, "unknown") | |||
.setParam(PARAM_ORGANIZATION, "test") | |||
.setParam(PARAM_REPOSITORY_KEY, "test/repo"); | |||
assertThatThrownBy(() -> request | |||
.execute()) | |||
.isInstanceOf(NotFoundException.class) | |||
.hasMessage("ALM Setting 'unknown' not found"); | |||
} | |||
@Test | |||
public void fail_when_personal_access_token_doesnt_exist() { | |||
AlmSettingDto githubAlmSetting = setupAlm(); | |||
TestRequest request = ws.newRequest() | |||
.setParam(PARAM_ALM_SETTING, githubAlmSetting.getKey()) | |||
.setParam(PARAM_ORGANIZATION, "test") | |||
.setParam(PARAM_REPOSITORY_KEY, "test/repo"); | |||
assertThatThrownBy(() -> request.execute()) | |||
.isInstanceOf(IllegalArgumentException.class) | |||
.hasMessage("No personal access token found"); | |||
} | |||
@Test | |||
public void definition() { | |||
WebService.Action def = ws.getDef(); | |||
assertThat(def.since()).isEqualTo("8.4"); | |||
assertThat(def.isPost()).isTrue(); | |||
assertThat(def.params()) | |||
.extracting(WebService.Param::key, WebService.Param::isRequired) | |||
.containsExactlyInAnyOrder( | |||
tuple(PARAM_ALM_SETTING, true), | |||
tuple(PARAM_ORGANIZATION, true), | |||
tuple(PARAM_REPOSITORY_KEY, true)); | |||
} | |||
private AlmSettingDto setupAlm() { | |||
UserDto user = db.users().insertUser(); | |||
userSession.logIn(user).addPermission(GlobalPermission.PROVISION_PROJECTS); | |||
return db.almSettings().insertGitHubAlmSetting(alm -> alm.setClientId("client_123").setClientSecret("client_secret_123")); | |||
} | |||
} |
@@ -0,0 +1,227 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2021 SonarSource SA | |||
* mailto:info AT sonarsource DOT com | |||
* | |||
* This program is free software; you can redistribute it and/or | |||
* modify it under the terms of the GNU Lesser General Public | |||
* License as published by the Free Software Foundation; either | |||
* version 3 of the License, or (at your option) any later version. | |||
* | |||
* This program is distributed in the hope that it will be useful, | |||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | |||
* Lesser General Public License for more details. | |||
* | |||
* You should have received a copy of the GNU Lesser General Public License | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
package org.sonar.server.almintegration.ws.github; | |||
import java.util.stream.Collectors; | |||
import java.util.stream.Stream; | |||
import org.junit.Rule; | |||
import org.junit.Test; | |||
import org.mockito.Mockito; | |||
import org.sonar.alm.client.github.GithubApplicationClient; | |||
import org.sonar.alm.client.github.GithubApplicationClientImpl; | |||
import org.sonar.alm.client.github.security.UserAccessToken; | |||
import org.sonar.api.utils.System2; | |||
import org.sonar.db.DbTester; | |||
import org.sonar.db.alm.pat.AlmPatDto; | |||
import org.sonar.db.alm.setting.AlmSettingDto; | |||
import org.sonar.db.permission.GlobalPermission; | |||
import org.sonar.db.user.UserDto; | |||
import org.sonar.server.exceptions.BadRequestException; | |||
import org.sonar.server.exceptions.NotFoundException; | |||
import org.sonar.server.exceptions.UnauthorizedException; | |||
import org.sonar.server.tester.UserSessionRule; | |||
import org.sonar.server.ws.TestRequest; | |||
import org.sonar.server.ws.WsActionTester; | |||
import org.sonarqube.ws.AlmIntegrations.GithubOrganization; | |||
import org.sonarqube.ws.AlmIntegrations.ListGithubOrganizationsWsResponse; | |||
import org.sonarqube.ws.Common; | |||
import static org.assertj.core.api.Assertions.assertThat; | |||
import static org.assertj.core.api.Assertions.assertThatThrownBy; | |||
import static org.assertj.core.api.Assertions.tuple; | |||
import static org.mockito.ArgumentMatchers.any; | |||
import static org.mockito.ArgumentMatchers.argThat; | |||
import static org.mockito.ArgumentMatchers.eq; | |||
import static org.mockito.Mockito.mock; | |||
import static org.mockito.Mockito.never; | |||
import static org.mockito.Mockito.verify; | |||
import static org.mockito.Mockito.when; | |||
import static org.sonar.server.almintegration.ws.github.ListGithubOrganizationsAction.PARAM_ALM_SETTING; | |||
import static org.sonar.server.almintegration.ws.github.ListGithubOrganizationsAction.PARAM_TOKEN; | |||
import static org.sonar.server.tester.UserSessionRule.standalone; | |||
public class ListGithubOrganizationsActionTest { | |||
@Rule | |||
public UserSessionRule userSession = standalone(); | |||
private final System2 system2 = mock(System2.class); | |||
private final GithubApplicationClientImpl appClient = mock(GithubApplicationClientImpl.class); | |||
@Rule | |||
public DbTester db = DbTester.create(system2); | |||
private final WsActionTester ws = new WsActionTester(new ListGithubOrganizationsAction(db.getDbClient(), userSession, appClient)); | |||
@Test | |||
public void fail_when_missing_create_project_permission() { | |||
TestRequest request = ws.newRequest(); | |||
assertThatThrownBy(request::execute).isInstanceOf(UnauthorizedException.class); | |||
} | |||
@Test | |||
public void fail_when_almSetting_does_not_exist() { | |||
UserDto user = db.users().insertUser(); | |||
userSession.logIn(user).addPermission(GlobalPermission.PROVISION_PROJECTS); | |||
TestRequest request = ws.newRequest().setParam(PARAM_ALM_SETTING, "unknown"); | |||
assertThatThrownBy(request::execute) | |||
.isInstanceOf(NotFoundException.class) | |||
.hasMessage("GitHub ALM Setting 'unknown' not found"); | |||
} | |||
@Test | |||
public void fail_when_unable_to_create_personal_access_token() { | |||
AlmSettingDto githubAlmSetting = setupAlm(); | |||
when(appClient.createUserAccessToken(githubAlmSetting.getUrl(), githubAlmSetting.getClientId(), githubAlmSetting.getClientSecret(), "abc")) | |||
.thenThrow(IllegalStateException.class); | |||
TestRequest request = ws.newRequest() | |||
.setParam(PARAM_ALM_SETTING, githubAlmSetting.getKey()) | |||
.setParam(PARAM_TOKEN, "abc"); | |||
assertThatThrownBy(request::execute) | |||
.isInstanceOf(IllegalStateException.class) | |||
.hasMessage(null); | |||
} | |||
@Test | |||
public void fail_create_personal_access_token_because_of_invalid_settings() { | |||
AlmSettingDto githubAlmSetting = setupAlm(); | |||
when(appClient.createUserAccessToken(githubAlmSetting.getUrl(), githubAlmSetting.getClientId(), githubAlmSetting.getClientSecret(), "abc")) | |||
.thenThrow(IllegalArgumentException.class); | |||
TestRequest request = ws.newRequest() | |||
.setParam(PARAM_ALM_SETTING, githubAlmSetting.getKey()) | |||
.setParam(PARAM_TOKEN, "abc"); | |||
assertThatThrownBy(request::execute) | |||
.isInstanceOf(BadRequestException.class) | |||
.hasMessage("Unable to authenticate with GitHub. Check the GitHub App client ID and client secret configured in the Global Settings and try again."); | |||
} | |||
@Test | |||
public void fail_when_personal_access_token_doesnt_exist() { | |||
AlmSettingDto githubAlmSetting = setupAlm(); | |||
TestRequest request = ws.newRequest().setParam(PARAM_ALM_SETTING, githubAlmSetting.getKey()); | |||
assertThatThrownBy(request::execute) | |||
.isInstanceOf(IllegalArgumentException.class) | |||
.hasMessage("No personal access token found"); | |||
} | |||
@Test | |||
public void return_organizations_and_store_personal_access_token() { | |||
UserAccessToken accessToken = new UserAccessToken("token_for_abc"); | |||
AlmSettingDto githubAlmSettings = setupAlm(); | |||
when(appClient.createUserAccessToken(githubAlmSettings.getUrl(), githubAlmSettings.getClientId(), githubAlmSettings.getClientSecret(), "abc")) | |||
.thenReturn(accessToken); | |||
setupGhOrganizations(githubAlmSettings, accessToken.getValue()); | |||
ListGithubOrganizationsWsResponse response = ws.newRequest() | |||
.setParam(PARAM_ALM_SETTING, githubAlmSettings.getKey()) | |||
.setParam(PARAM_TOKEN, "abc") | |||
.executeProtobuf(ListGithubOrganizationsWsResponse.class); | |||
assertThat(response.getPaging()) | |||
.extracting(Common.Paging::getPageIndex, Common.Paging::getPageSize, Common.Paging::getTotal) | |||
.containsOnly(1, 100, 2); | |||
assertThat(response.getOrganizationsList()) | |||
.extracting(GithubOrganization::getKey, GithubOrganization::getName) | |||
.containsOnly(tuple("github", "github"), tuple("octacat", "octacat")); | |||
verify(appClient).createUserAccessToken(githubAlmSettings.getUrl(), githubAlmSettings.getClientId(), githubAlmSettings.getClientSecret(), "abc"); | |||
verify(appClient).listOrganizations(githubAlmSettings.getUrl(), accessToken, 1, 100); | |||
Mockito.verifyNoMoreInteractions(appClient); | |||
assertThat(db.getDbClient().almPatDao().selectByUserAndAlmSetting(db.getSession(), userSession.getUuid(), githubAlmSettings).get().getPersonalAccessToken()) | |||
.isEqualTo(accessToken.getValue()); | |||
} | |||
@Test | |||
public void return_organizations_overriding_existing_personal_access_token() { | |||
AlmSettingDto githubAlmSettings = setupAlm(); | |||
// old pat | |||
AlmPatDto pat = db.almPats().insert(p -> p.setAlmSettingUuid(githubAlmSettings.getUuid()).setUserUuid(userSession.getUuid())); | |||
// new pat | |||
UserAccessToken accessToken = new UserAccessToken("token_for_abc"); | |||
when(appClient.createUserAccessToken(githubAlmSettings.getUrl(), githubAlmSettings.getClientId(), githubAlmSettings.getClientSecret(), "abc")) | |||
.thenReturn(accessToken); | |||
setupGhOrganizations(githubAlmSettings, accessToken.getValue()); | |||
ListGithubOrganizationsWsResponse response = ws.newRequest() | |||
.setParam(PARAM_ALM_SETTING, githubAlmSettings.getKey()) | |||
.setParam(PARAM_TOKEN, "abc") | |||
.executeProtobuf(ListGithubOrganizationsWsResponse.class); | |||
assertThat(response.getPaging()) | |||
.extracting(Common.Paging::getPageIndex, Common.Paging::getPageSize, Common.Paging::getTotal) | |||
.containsOnly(1, 100, 2); | |||
assertThat(response.getOrganizationsList()) | |||
.extracting(GithubOrganization::getKey, GithubOrganization::getName) | |||
.containsOnly(tuple("github", "github"), tuple("octacat", "octacat")); | |||
verify(appClient).createUserAccessToken(githubAlmSettings.getUrl(), githubAlmSettings.getClientId(), githubAlmSettings.getClientSecret(), "abc"); | |||
verify(appClient).listOrganizations(eq(githubAlmSettings.getUrl()), argThat(token -> token.getValue().equals(accessToken.getValue())), eq(1), eq(100)); | |||
Mockito.verifyNoMoreInteractions(appClient); | |||
assertThat(db.getDbClient().almPatDao().selectByUserAndAlmSetting(db.getSession(), userSession.getUuid(), githubAlmSettings).get().getPersonalAccessToken()) | |||
.isEqualTo(accessToken.getValue()); | |||
} | |||
@Test | |||
public void return_organizations_using_existing_personal_access_token() { | |||
AlmSettingDto githubAlmSettings = setupAlm(); | |||
AlmPatDto pat = db.almPats().insert(p -> p.setAlmSettingUuid(githubAlmSettings.getUuid()).setUserUuid(userSession.getUuid())); | |||
setupGhOrganizations(githubAlmSettings, pat.getPersonalAccessToken()); | |||
ListGithubOrganizationsWsResponse response = ws.newRequest() | |||
.setParam(PARAM_ALM_SETTING, githubAlmSettings.getKey()) | |||
.executeProtobuf(ListGithubOrganizationsWsResponse.class); | |||
assertThat(response.getPaging()) | |||
.extracting(Common.Paging::getPageIndex, Common.Paging::getPageSize, Common.Paging::getTotal) | |||
.containsOnly(1, 100, 2); | |||
assertThat(response.getOrganizationsList()) | |||
.extracting(GithubOrganization::getKey, GithubOrganization::getName) | |||
.containsOnly(tuple("github", "github"), tuple("octacat", "octacat")); | |||
verify(appClient, never()).createUserAccessToken(any(), any(), any(), any()); | |||
verify(appClient).listOrganizations(eq(githubAlmSettings.getUrl()), argThat(token -> token.getValue().equals(pat.getPersonalAccessToken())), eq(1), eq(100)); | |||
Mockito.verifyNoMoreInteractions(appClient); | |||
assertThat(db.getDbClient().almPatDao().selectByUserAndAlmSetting(db.getSession(), userSession.getUuid(), githubAlmSettings).get().getPersonalAccessToken()) | |||
.isEqualTo(pat.getPersonalAccessToken()); | |||
} | |||
private void setupGhOrganizations(AlmSettingDto almSetting, String pat) { | |||
when(appClient.listOrganizations(eq(almSetting.getUrl()), argThat(token -> token.getValue().equals(pat)), eq(1), eq(100))) | |||
.thenReturn(new GithubApplicationClient.Organizations() | |||
.setTotal(2) | |||
.setOrganizations(Stream.of("github", "octacat") | |||
.map(login -> new GithubApplicationClient.Organization(login.length(), login, login, null, null, null, null, "Organization")) | |||
.collect(Collectors.toList()))); | |||
} | |||
private AlmSettingDto setupAlm() { | |||
UserDto user = db.users().insertUser(); | |||
userSession.logIn(user).addPermission(GlobalPermission.PROVISION_PROJECTS); | |||
return db.almSettings().insertGitHubAlmSetting(alm -> alm.setClientId("client_123").setClientSecret("client_secret_123")); | |||
} | |||
} |
@@ -0,0 +1,138 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2021 SonarSource SA | |||
* mailto:info AT sonarsource DOT com | |||
* | |||
* This program is free software; you can redistribute it and/or | |||
* modify it under the terms of the GNU Lesser General Public | |||
* License as published by the Free Software Foundation; either | |||
* version 3 of the License, or (at your option) any later version. | |||
* | |||
* This program is distributed in the hope that it will be useful, | |||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | |||
* Lesser General Public License for more details. | |||
* | |||
* You should have received a copy of the GNU Lesser General Public License | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
package org.sonar.server.almintegration.ws.github; | |||
import java.util.stream.Collectors; | |||
import java.util.stream.Stream; | |||
import org.junit.Rule; | |||
import org.junit.Test; | |||
import org.sonar.alm.client.github.GithubApplicationClient; | |||
import org.sonar.alm.client.github.GithubApplicationClientImpl; | |||
import org.sonar.api.utils.System2; | |||
import org.sonar.db.DbTester; | |||
import org.sonar.db.alm.pat.AlmPatDto; | |||
import org.sonar.db.alm.setting.AlmSettingDto; | |||
import org.sonar.db.permission.GlobalPermission; | |||
import org.sonar.db.project.ProjectDto; | |||
import org.sonar.db.user.UserDto; | |||
import org.sonar.server.exceptions.NotFoundException; | |||
import org.sonar.server.exceptions.UnauthorizedException; | |||
import org.sonar.server.tester.UserSessionRule; | |||
import org.sonar.server.ws.TestRequest; | |||
import org.sonar.server.ws.WsActionTester; | |||
import org.sonarqube.ws.Common; | |||
import static org.assertj.core.api.Assertions.assertThat; | |||
import static org.assertj.core.api.Assertions.assertThatThrownBy; | |||
import static org.assertj.core.api.Assertions.tuple; | |||
import static org.mockito.ArgumentMatchers.argThat; | |||
import static org.mockito.ArgumentMatchers.eq; | |||
import static org.mockito.ArgumentMatchers.isNull; | |||
import static org.mockito.Mockito.mock; | |||
import static org.mockito.Mockito.verify; | |||
import static org.mockito.Mockito.when; | |||
import static org.sonar.server.tester.UserSessionRule.standalone; | |||
import static org.sonarqube.ws.AlmIntegrations.GithubRepository; | |||
import static org.sonarqube.ws.AlmIntegrations.ListGithubRepositoriesWsResponse; | |||
public class ListGithubRepositoriesActionTest { | |||
@Rule | |||
public UserSessionRule userSession = standalone(); | |||
private final System2 system2 = mock(System2.class); | |||
private final GithubApplicationClientImpl appClient = mock(GithubApplicationClientImpl.class); | |||
@Rule | |||
public DbTester db = DbTester.create(system2); | |||
private final WsActionTester ws = new WsActionTester(new ListGithubRepositoriesAction(db.getDbClient(), userSession, appClient)); | |||
@Test | |||
public void fail_when_missing_create_project_permission() { | |||
TestRequest request = ws.newRequest(); | |||
assertThatThrownBy(request::execute) | |||
.isInstanceOf(UnauthorizedException.class); | |||
} | |||
@Test | |||
public void fail_when_almSetting_does_not_exist() { | |||
UserDto user = db.users().insertUser(); | |||
userSession.logIn(user).addPermission(GlobalPermission.PROVISION_PROJECTS); | |||
TestRequest request = ws.newRequest() | |||
.setParam(ListGithubRepositoriesAction.PARAM_ALM_SETTING, "unknown") | |||
.setParam(ListGithubRepositoriesAction.PARAM_ORGANIZATION, "test"); | |||
assertThatThrownBy(request::execute) | |||
.isInstanceOf(NotFoundException.class) | |||
.hasMessage("GitHub ALM Setting 'unknown' not found"); | |||
} | |||
@Test | |||
public void fail_when_personal_access_token_doesnt_exist() { | |||
AlmSettingDto githubAlmSetting = setupAlm(); | |||
TestRequest request = ws.newRequest() | |||
.setParam(ListGithubRepositoriesAction.PARAM_ALM_SETTING, githubAlmSetting.getKey()) | |||
.setParam(ListGithubRepositoriesAction.PARAM_ORGANIZATION, "test"); | |||
assertThatThrownBy(request::execute) | |||
.isInstanceOf(IllegalArgumentException.class) | |||
.hasMessage("No personal access token found"); | |||
} | |||
@Test | |||
public void return_repositories_using_existing_personal_access_token() { | |||
AlmSettingDto githubAlmSettings = setupAlm(); | |||
AlmPatDto pat = db.almPats().insert(p -> p.setAlmSettingUuid(githubAlmSettings.getUuid()).setUserUuid(userSession.getUuid())); | |||
when(appClient.listRepositories(eq(githubAlmSettings.getUrl()), argThat(token -> token.getValue().equals(pat.getPersonalAccessToken())), eq("github"), isNull(), eq(1), | |||
eq(100))) | |||
.thenReturn(new GithubApplicationClient.Repositories() | |||
.setTotal(2) | |||
.setRepositories(Stream.of("HelloWorld", "HelloUniverse") | |||
.map(name -> new GithubApplicationClient.Repository(name.length(), name, false, "github/" + name, "https://github-enterprise.sonarqube.com/api/v3/github/HelloWorld")) | |||
.collect(Collectors.toList()))); | |||
ProjectDto project = db.components().insertPrivateProjectDto(componentDto -> componentDto.setDbKey("github_HelloWorld")); | |||
ListGithubRepositoriesWsResponse response = ws.newRequest() | |||
.setParam(ListGithubRepositoriesAction.PARAM_ALM_SETTING, githubAlmSettings.getKey()) | |||
.setParam(ListGithubRepositoriesAction.PARAM_ORGANIZATION, "github") | |||
.executeProtobuf(ListGithubRepositoriesWsResponse.class); | |||
assertThat(response.getPaging()) | |||
.extracting(Common.Paging::getPageIndex, Common.Paging::getPageSize, Common.Paging::getTotal) | |||
.containsOnly(1, 100, 2); | |||
assertThat(response.getRepositoriesCount()).isEqualTo(2); | |||
assertThat(response.getRepositoriesList()) | |||
.extracting(GithubRepository::getKey, GithubRepository::getName, GithubRepository::getSqProjectKey) | |||
.containsOnly(tuple("github/HelloWorld", "HelloWorld", project.getKey()), tuple("github/HelloUniverse", "HelloUniverse", "")); | |||
verify(appClient).listRepositories(eq(githubAlmSettings.getUrl()), argThat(token -> token.getValue().equals(pat.getPersonalAccessToken())), eq("github"), isNull(), eq(1), | |||
eq(100)); | |||
} | |||
private AlmSettingDto setupAlm() { | |||
UserDto user = db.users().insertUser(); | |||
userSession.logIn(user).addPermission(GlobalPermission.PROVISION_PROJECTS); | |||
return db.almSettings().insertGitHubAlmSetting(alm -> alm.setClientId("client_123").setClientSecret("client_secret_123")); | |||
} | |||
} |
@@ -23,6 +23,8 @@ import java.util.List; | |||
import org.sonar.alm.client.TimeoutConfigurationImpl; | |||
import org.sonar.alm.client.azure.AzureDevOpsHttpClient; | |||
import org.sonar.alm.client.bitbucketserver.BitbucketServerRestClient; | |||
import org.sonar.alm.client.github.GithubApplicationClientImpl; | |||
import org.sonar.alm.client.github.GithubApplicationHttpClientImpl; | |||
import org.sonar.alm.client.gitlab.GitlabHttpClient; | |||
import org.sonar.api.profiles.AnnotationProfileParser; | |||
import org.sonar.api.profiles.XMLProfileParser; | |||
@@ -493,6 +495,8 @@ public class PlatformLevel4 extends PlatformLevel { | |||
// ALM integrations | |||
TimeoutConfigurationImpl.class, | |||
ImportHelper.class, | |||
GithubApplicationClientImpl.class, | |||
GithubApplicationHttpClientImpl.class, | |||
BitbucketServerRestClient.class, | |||
GitlabHttpClient.class, | |||
AzureDevOpsHttpClient.class, |