/*
* SonarQube
* Copyright (C) 2009-2025 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.gitlab;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import java.util.function.Function;
import javax.annotation.Nullable;
import okhttp3.mockwebserver.MockResponse;
import okhttp3.mockwebserver.MockWebServer;
import okhttp3.mockwebserver.RecordedRequest;
import org.apache.commons.io.IOUtils;
import org.junit.After;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.mockito.ArgumentCaptor;
import org.slf4j.event.Level;
import org.sonar.alm.client.ConstantTimeoutConfiguration;
import org.sonar.alm.client.TimeoutConfiguration;
import org.sonar.api.testfixtures.log.LogTester;
import org.sonar.auth.gitlab.GsonGroup;
import org.sonar.auth.gitlab.GsonMemberRole;
import org.sonar.auth.gitlab.GsonProjectMember;
import org.sonar.auth.gitlab.GsonUser;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
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.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
public class GitlabApplicationClientTest {
@Rule
public LogTester logTester = new LogTester();
private final GitlabPaginatedHttpClient gitlabPaginatedHttpClient = mock();
private final MockWebServer server = new MockWebServer();
private GitlabApplicationClient underTest;
private String gitlabUrl;
@Before
public void prepare() throws IOException {
server.start();
String urlWithEndingSlash = server.url("").toString();
gitlabUrl = urlWithEndingSlash.substring(0, urlWithEndingSlash.length() - 1);
TimeoutConfiguration timeoutConfiguration = new ConstantTimeoutConfiguration(10_000);
underTest = new GitlabApplicationClient(gitlabPaginatedHttpClient, timeoutConfiguration);
}
@After
public void stopServer() throws IOException {
server.shutdown();
}
@Test
public void should_throw_IllegalArgumentException_when_token_is_revoked() {
MockResponse response = new MockResponse()
.setResponseCode(401)
.setBody("{\"error\":\"invalid_token\",\"error_description\":\"Token was revoked. You have to re-authorize from the user.\"}");
server.enqueue(response);
assertThatThrownBy(() -> underTest.searchProjects(gitlabUrl, "pat", "example", 1, 2))
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("Your GitLab token was revoked");
}
@Test
public void should_throw_IllegalArgumentException_when_token_insufficient_scope() {
MockResponse response = new MockResponse()
.setResponseCode(403)
.setBody("{\"error\":\"insufficient_scope\"," +
"\"error_description\":\"The request requires higher privileges than provided by the access token.\"," +
"\"scope\":\"api read_api\"}");
server.enqueue(response);
assertThatThrownBy(() -> underTest.searchProjects(gitlabUrl, "pat", "example", 1, 2))
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("Your GitLab token has insufficient scope");
}
@Test
public void should_throw_IllegalArgumentException_when_invalide_json_in_401_response() {
MockResponse response = new MockResponse()
.setResponseCode(401)
.setBody("error in pat");
server.enqueue(response);
assertThatThrownBy(() -> underTest.searchProjects(gitlabUrl, "pat", "example", 1, 2))
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("Invalid personal access token");
}
@Test
public void should_throw_IllegalArgumentException_when_redirected() {
MockResponse response = new MockResponse()
.setResponseCode(308);
server.enqueue(response);
assertThatThrownBy(() -> underTest.searchProjects(gitlabUrl, "pat", "example", 1, 2))
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("Request was redirected, please provide the correct URL");
}
@Test
public void get_project() {
MockResponse response = new MockResponse()
.setResponseCode(200)
.setBody("""
{
"id": 12345,
"name": "SonarQube example 1",
"name_with_namespace": "SonarSource / SonarQube / SonarQube example 1",
"path": "sonarqube-example-1",
"path_with_namespace": "sonarsource/sonarqube/sonarqube-example-1",
"visibility": "visibilityFromGitLab",
"web_url": "https://example.gitlab.com/sonarsource/sonarqube/sonarqube-example-1"
}
""");
server.enqueue(response);
assertThat(underTest.getProject(gitlabUrl, "pat", 12345L))
.extracting(Project::getId, Project::getName, Project::getVisibility)
.containsExactly(12345L, "SonarQube example 1", "visibilityFromGitLab");
}
@Test
public void get_project_fail_if_non_json_payload() {
MockResponse response = new MockResponse()
.setResponseCode(200)
.setBody("non json payload");
server.enqueue(response);
assertThatThrownBy(() -> underTest.getProject(gitlabUrl, "pat", 12345L))
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("Could not parse GitLab answer to retrieve a project. Got a non-json payload as result.");
}
@Test
public void get_branches() {
MockResponse response = new MockResponse()
.setResponseCode(200)
.setBody("""
[{
"name": "main",
"default": true
},{
"name": "other",
"default": false
}]""");
server.enqueue(response);
assertThat(underTest.getBranches(gitlabUrl, "pat", 12345L))
.extracting(GitLabBranch::getName, GitLabBranch::isDefault)
.containsExactly(
tuple("main", true),
tuple("other", false));
}
@Test
public void get_branches_fail_if_non_json_payload() {
MockResponse response = new MockResponse()
.setResponseCode(200)
.setBody("non json payload");
server.enqueue(response);
String instanceUrl = gitlabUrl;
assertThatThrownBy(() -> underTest.getBranches(instanceUrl, "pat", 12345L))
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("Could not parse GitLab answer to retrieve project branches. Got a non-json payload as result.");
}
@Test
public void get_branches_fail_if_exception() throws IOException {
server.shutdown();
String instanceUrl = gitlabUrl;
assertThatThrownBy(() -> underTest.getBranches(instanceUrl, "pat", 12345L))
.isInstanceOf(IllegalStateException.class)
.hasMessageContaining("Failed to connect to");
}
@Test
public void search_projects() throws InterruptedException {
MockResponse projects = new MockResponse()
.setResponseCode(200)
.setBody("""
[
{
"id": 1,
"name": "SonarQube example 1",
"name_with_namespace": "SonarSource / SonarQube / SonarQube example 1",
"path": "sonarqube-example-1",
"path_with_namespace": "sonarsource/sonarqube/sonarqube-example-1",
"web_url": "https://example.gitlab.com/sonarsource/sonarqube/sonarqube-example-1"
},
{
"id": 2,
"name": "SonarQube example 2",
"name_with_namespace": "SonarSource / SonarQube / SonarQube example 2",
"path": "sonarqube-example-2",
"path_with_namespace": "sonarsource/sonarqube/sonarqube-example-2",
"web_url": "https://example.gitlab.com/sonarsource/sonarqube/sonarqube-example-2"
},
{
"id": 3,
"name": "SonarQube example 3",
"name_with_namespace": "SonarSource / SonarQube / SonarQube example 3",
"path": "sonarqube-example-3",
"path_with_namespace": "sonarsource/sonarqube/sonarqube-example-3",
"web_url": "https://example.gitlab.com/sonarsource/sonarqube/sonarqube-example-3"
}
]""");
projects.addHeader("X-Page", 1);
projects.addHeader("X-Per-Page", 10);
projects.addHeader("X-Total", 3);
server.enqueue(projects);
ProjectList projectList = underTest.searchProjects(gitlabUrl, "pat", "example", 1, 10);
assertThat(projectList.getPageNumber()).isOne();
assertThat(projectList.getPageSize()).isEqualTo(10);
assertThat(projectList.getTotal()).isEqualTo(3);
assertThat(projectList.getProjects()).hasSize(3);
assertThat(projectList.getProjects()).extracting(
Project::getId, Project::getName, Project::getNameWithNamespace, Project::getPath, Project::getPathWithNamespace, Project::getWebUrl).containsExactly(
tuple(1L, "SonarQube example 1", "SonarSource / SonarQube / SonarQube example 1", "sonarqube-example-1", "sonarsource/sonarqube/sonarqube-example-1",
"https://example.gitlab.com/sonarsource/sonarqube/sonarqube-example-1"),
tuple(2L, "SonarQube example 2", "SonarSource / SonarQube / SonarQube example 2", "sonarqube-example-2", "sonarsource/sonarqube/sonarqube-example-2",
"https://example.gitlab.com/sonarsource/sonarqube/sonarqube-example-2"),
tuple(3L, "SonarQube example 3", "SonarSource / SonarQube / SonarQube example 3", "sonarqube-example-3", "sonarsource/sonarqube/sonarqube-example-3",
"https://example.gitlab.com/sonarsource/sonarqube/sonarqube-example-3"));
RecordedRequest projectGitlabRequest = server.takeRequest(10, TimeUnit.SECONDS);
String gitlabUrlCall = projectGitlabRequest.getRequestUrl().toString();
assertThat(gitlabUrlCall).isEqualTo(server.url("") + "projects?archived=false&simple=true&membership=true&order_by=name&sort=asc&search=example&page=1&per_page=10");
assertThat(projectGitlabRequest.getMethod()).isEqualTo("GET");
}
@Test
public void search_projects_dont_fail_if_no_x_total() throws InterruptedException {
MockResponse projects = new MockResponse()
.setResponseCode(200)
.setBody("""
[
{
"id": 1,
"name": "SonarQube example 1",
"name_with_namespace": "SonarSource / SonarQube / SonarQube example 1",
"path": "sonarqube-example-1",
"path_with_namespace": "sonarsource/sonarqube/sonarqube-example-1",
"web_url": "https://example.gitlab.com/sonarsource/sonarqube/sonarqube-example-1"
}\
]""");
projects.addHeader("X-Page", 1);
projects.addHeader("X-Per-Page", 10);
server.enqueue(projects);
ProjectList projectList = underTest.searchProjects(gitlabUrl, "pat", "example", 1, 10);
assertThat(projectList.getPageNumber()).isOne();
assertThat(projectList.getPageSize()).isEqualTo(10);
assertThat(projectList.getTotal()).isNull();
assertThat(projectList.getProjects()).hasSize(1);
assertThat(projectList.getProjects()).extracting(
Project::getId, Project::getName, Project::getNameWithNamespace, Project::getPath, Project::getPathWithNamespace, Project::getWebUrl).containsExactly(
tuple(1L, "SonarQube example 1", "SonarSource / SonarQube / SonarQube example 1", "sonarqube-example-1", "sonarsource/sonarqube/sonarqube-example-1",
"https://example.gitlab.com/sonarsource/sonarqube/sonarqube-example-1"));
RecordedRequest projectGitlabRequest = server.takeRequest(10, TimeUnit.SECONDS);
String gitlabUrlCall = projectGitlabRequest.getRequestUrl().toString();
assertThat(gitlabUrlCall).isEqualTo(server.url("") + "projects?archived=false&simple=true&membership=true&order_by=name&sort=asc&search=example&page=1&per_page=10");
assertThat(projectGitlabRequest.getMethod()).isEqualTo("GET");
}
@Test
public void search_projects_with_case_insensitive_pagination_headers() throws InterruptedException {
MockResponse projects1 = new MockResponse()
.setResponseCode(200)
.setBody("""
[
{
"id": 1,
"name": "SonarQube example 1",
"name_with_namespace": "SonarSource / SonarQube / SonarQube example 1",
"path": "sonarqube-example-1",
"path_with_namespace": "sonarsource/sonarqube/sonarqube-example-1",
"web_url": "https://example.gitlab.com/sonarsource/sonarqube/sonarqube-example-1"
}\
]""");
projects1.addHeader("x-page", 1);
projects1.addHeader("x-Per-page", 1);
projects1.addHeader("X-Total", 2);
server.enqueue(projects1);
ProjectList projectList = underTest.searchProjects(gitlabUrl, "pat", "example", 1, 10);
assertThat(projectList.getPageNumber()).isOne();
assertThat(projectList.getPageSize()).isOne();
assertThat(projectList.getTotal()).isEqualTo(2);
assertThat(projectList.getProjects()).hasSize(1);
assertThat(projectList.getProjects()).extracting(
Project::getId, Project::getName, Project::getNameWithNamespace, Project::getPath, Project::getPathWithNamespace, Project::getWebUrl).containsExactly(
tuple(1L, "SonarQube example 1", "SonarSource / SonarQube / SonarQube example 1", "sonarqube-example-1", "sonarsource/sonarqube/sonarqube-example-1",
"https://example.gitlab.com/sonarsource/sonarqube/sonarqube-example-1"));
RecordedRequest projectGitlabRequest = server.takeRequest(10, TimeUnit.SECONDS);
String gitlabUrlCall = projectGitlabRequest.getRequestUrl().toString();
assertThat(gitlabUrlCall).isEqualTo(server.url("") + "projects?archived=false&simple=true&membership=true&order_by=name&sort=asc&search=example&page=1&per_page=10");
assertThat(projectGitlabRequest.getMethod()).isEqualTo("GET");
}
@Test
public void search_projects_projectName_param_should_be_encoded() throws InterruptedException {
MockResponse projects = new MockResponse()
.setResponseCode(200)
.setBody("[]");
projects.addHeader("X-Page", 1);
projects.addHeader("X-Per-Page", 10);
projects.addHeader("X-Total", 0);
server.enqueue(projects);
ProjectList projectList = underTest.searchProjects(gitlabUrl, "pat", "&page=", 1, 10);
RecordedRequest projectGitlabRequest = server.takeRequest(10, TimeUnit.SECONDS);
String gitlabUrlCall = projectGitlabRequest.getRequestUrl().toString();
assertThat(projectList.getProjects()).isEmpty();
assertThat(gitlabUrlCall).isEqualTo(
server.url("")
+ "projects?archived=false&simple=true&membership=true&order_by=name&sort=asc&search=%26page%3D%3Cscript%3Ealert%28%27nasty%27%29%3C%2Fscript%3E&page=1&per_page=10");
assertThat(projectGitlabRequest.getMethod()).isEqualTo("GET");
}
@Test
public void search_projects_projectName_param_null_should_pass_empty_string() throws InterruptedException {
MockResponse projects = new MockResponse()
.setResponseCode(200)
.setBody("[]");
projects.addHeader("X-Page", 1);
projects.addHeader("X-Per-Page", 10);
projects.addHeader("X-Total", 0);
server.enqueue(projects);
ProjectList projectList = underTest.searchProjects(gitlabUrl, "pat", null, 1, 10);
RecordedRequest projectGitlabRequest = server.takeRequest(10, TimeUnit.SECONDS);
String gitlabUrlCall = projectGitlabRequest.getRequestUrl().toString();
assertThat(projectList.getProjects()).isEmpty();
assertThat(gitlabUrlCall).isEqualTo(
server.url("") + "projects?archived=false&simple=true&membership=true&order_by=name&sort=asc&search=&page=1&per_page=10");
assertThat(projectGitlabRequest.getMethod()).isEqualTo("GET");
}
@Test
public void get_project_details() throws InterruptedException {
MockResponse projectResponse = new MockResponse()
.setResponseCode(200)
.setBody("""
{\
"id": 1234,\
"name": "SonarQube example 2",\
"name_with_namespace": "SonarSource / SonarQube / SonarQube example 2",\
"path": "sonarqube-example-2",\
"path_with_namespace": "sonarsource/sonarqube/sonarqube-example-2",\
"web_url": "https://example.gitlab.com/sonarsource/sonarqube/sonarqube-example-2"\
}""");
server.enqueue(projectResponse);
Project project = underTest.getProject(gitlabUrl, "pat", 1234L);
RecordedRequest projectGitlabRequest = server.takeRequest(10, TimeUnit.SECONDS);
String gitlabUrlCall = projectGitlabRequest.getRequestUrl().toString();
assertThat(project).isNotNull();
assertThat(gitlabUrlCall).isEqualTo(
server.url("") + "projects/1234");
assertThat(projectGitlabRequest.getMethod()).isEqualTo("GET");
}
@Test
public void get_reporter_level_access_project() throws InterruptedException {
MockResponse projectResponse = new MockResponse()
.setResponseCode(200)
.setBody("[{"
+ " \"id\": 1234,"
+ " \"name\": \"SonarQube example 2\","
+ " \"name_with_namespace\": \"SonarSource / SonarQube / SonarQube example 2\","
+ " \"path\": \"sonarqube-example-2\","
+ " \"path_with_namespace\": \"sonarsource/sonarqube/sonarqube-example-2\","
+ " \"web_url\": \"https://example.gitlab.com/sonarsource/sonarqube/sonarqube-example-2\""
+ "}]");
server.enqueue(projectResponse);
Optional project = underTest.getReporterLevelAccessProject(gitlabUrl, "pat", 1234L);
RecordedRequest projectGitlabRequest = server.takeRequest(10, TimeUnit.SECONDS);
String gitlabUrlCall = projectGitlabRequest.getRequestUrl().toString();
assertThat(project).isNotNull();
assertThat(gitlabUrlCall).isEqualTo(
server.url("") + "projects?min_access_level=20&id_after=1233&id_before=1235");
assertThat(projectGitlabRequest.getMethod()).isEqualTo("GET");
}
@Test
public void search_projects_fail_if_could_not_parse_pagination_number() {
MockResponse projects = new MockResponse()
.setResponseCode(200)
.setBody("[ ]");
projects.addHeader("X-Page", "bad-page-number");
projects.addHeader("X-Per-Page", "bad-per-page-number");
projects.addHeader("X-Total", "bad-total-number");
server.enqueue(projects);
assertThatThrownBy(() -> underTest.searchProjects(gitlabUrl, "pat", "example", 1, 10))
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("Could not parse pagination number");
}
@Test
public void search_projects_fail_if_pagination_data_not_returned() {
MockResponse projects = new MockResponse()
.setResponseCode(200)
.setBody("[ ]");
server.enqueue(projects);
assertThatThrownBy(() -> underTest.searchProjects(gitlabUrl, "pat", "example", 1, 10))
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("Pagination data from GitLab response is missing");
}
@Test
public void throws_ISE_when_get_projects_not_http_200() {
MockResponse projects = new MockResponse()
.setResponseCode(500)
.setBody("test");
server.enqueue(projects);
assertThatThrownBy(() -> underTest.searchProjects(gitlabUrl, "pat", "example", 1, 2))
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("Could not get projects from GitLab instance");
}
@Test
public void fail_check_read_permission_with_unexpected_io_exception_with_detailed_log() throws IOException {
server.shutdown();
assertThatThrownBy(() -> underTest.checkReadPermission(gitlabUrl, "token"))
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("Could not validate GitLab read permission. Got an unexpected answer.");
assertThat(logTester.logs(Level.INFO).get(0))
.contains("Gitlab API call to [" + server.url("/projects") + "] " +
"failed with error message : [Failed to connect to " + server.getHostName());
}
@Test
public void fail_check_token_with_unexpected_io_exception_with_detailed_log() throws IOException {
server.shutdown();
assertThatThrownBy(() -> underTest.checkToken(gitlabUrl, "token"))
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("Could not validate GitLab token. Got an unexpected answer.");
assertThat(logTester.logs(Level.INFO).get(0))
.contains("Gitlab API call to [" + server.url("user") + "] " +
"failed with error message : [Failed to connect to " + server.getHostName());
}
@Test
public void fail_check_write_permission_with_unexpected_io_exception_with_detailed_log() throws IOException {
server.shutdown();
assertThatThrownBy(() -> underTest.checkWritePermission(gitlabUrl, "token"))
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("Could not validate GitLab write permission. Got an unexpected answer.");
assertThat(logTester.logs(Level.INFO).get(0))
.contains("Gitlab API call to [" + server.url("/markdown") + "] " +
"failed with error message : [Failed to connect to " + server.getHostName());
}
@Test
public void fail_get_project_with_unexpected_io_exception_with_detailed_log() throws IOException {
server.shutdown();
assertThatThrownBy(() -> underTest.getProject(gitlabUrl, "token", 0L))
.isInstanceOf(IllegalStateException.class)
.hasMessageContaining("Failed to connect to");
assertThat(logTester.logs(Level.INFO).get(0))
.contains("Gitlab API call to [" + server.url("/projects/0") + "] " +
"failed with error message : [Failed to connect to " + server.getHostName());
}
@Test
public void fail_get_branches_with_unexpected_io_exception_with_detailed_log() throws IOException {
server.shutdown();
assertThatThrownBy(() -> underTest.getBranches(gitlabUrl, "token", 0L))
.isInstanceOf(IllegalStateException.class)
.hasMessageContaining("Failed to connect to " + server.getHostName());
assertThat(logTester.logs(Level.INFO).get(0))
.contains("Gitlab API call to [" + server.url("/projects/0/repository/branches") + "] " +
"failed with error message : [Failed to connect to " + server.getHostName());
}
@Test
public void fail_search_projects_with_unexpected_io_exception_with_detailed_log() throws IOException {
server.shutdown();
assertThatThrownBy(() -> underTest.searchProjects(gitlabUrl, "token", null, 1, 1))
.isInstanceOf(IllegalStateException.class)
.hasMessageContaining("Failed to connect to");
assertThat(logTester.logs(Level.INFO).get(0))
.contains(
"Gitlab API call to [" + server.url("/projects?archived=false&simple=true&membership=true&order_by=name&sort=asc&search=&page=1&per_page=1")
+ "] " +
"failed with error message : [Failed to connect to " + server.getHostName());
}
@Test
public void getGroups_whenCallIsInError_rethrows() throws IOException {
String token = "token-toto";
GitlabToken gitlabToken = new GitlabToken(token);
when(gitlabPaginatedHttpClient.get(eq(gitlabUrl), eq(gitlabToken), eq("/groups"), any())).thenThrow(new IllegalStateException("exception"));
assertThatIllegalStateException()
.isThrownBy(() -> underTest.getGroups(gitlabUrl, token))
.withMessage("exception");
}
@Test
public void getGroups_whenCallIsSuccessful_deserializesAndReturnsCorrectlyGroups() throws IOException {
ArgumentCaptor>> deserializerCaptor = ArgumentCaptor.forClass(Function.class);
String token = "token-toto";
GitlabToken gitlabToken = new GitlabToken(token);
List expectedGroups = expectedGroups();
when(gitlabPaginatedHttpClient.get(eq(gitlabUrl), eq(gitlabToken), eq("/groups"), deserializerCaptor.capture())).thenReturn(expectedGroups);
Set groups = underTest.getGroups(gitlabUrl, token);
assertThat(groups).containsExactlyInAnyOrderElementsOf(expectedGroups);
String responseContent = getResponseContent("groups-full-response.json");
List deserializedGroups = deserializerCaptor.getValue().apply(responseContent);
assertThat(deserializedGroups).usingRecursiveComparison().isEqualTo(expectedGroups);
}
private static List expectedGroups() {
GsonGroup gsonGroup = createGsonGroup("56232243", "sonarsource/cfamily", "this is a long description");
GsonGroup gsonGroup2 = createGsonGroup("78902256", "sonarsource/sonarqube/mmf-3052-ant1", "");
return List.of(gsonGroup, gsonGroup2);
}
private static GsonGroup createGsonGroup(String number, String fullPath, String description) {
GsonGroup gsonGroup = mock(GsonGroup.class);
when(gsonGroup.getId()).thenReturn(number);
when(gsonGroup.getFullPath()).thenReturn(fullPath);
return gsonGroup;
}
@Test
public void getDirectGroupMembers_whenCallIsInError_rethrows() {
String token = "token-toto";
GitlabToken gitlabToken = new GitlabToken(token);
when(gitlabPaginatedHttpClient.get(eq(gitlabUrl), eq(gitlabToken), eq("/groups/42/members"), any())).thenThrow(new IllegalStateException("exception"));
assertThatIllegalStateException()
.isThrownBy(() -> underTest.getDirectGroupMembers(gitlabUrl, token, "42"))
.withMessage("exception");
}
@Test
public void getDirectGroupMembers_whenCallIsSuccessful_deserializesAndReturnsCorrectlyGroupMembers() throws IOException {
ArgumentCaptor>> deserializerCaptor = ArgumentCaptor.forClass(Function.class);
String token = "token-toto";
GitlabToken gitlabToken = new GitlabToken(token);
List expectedGroupMembers = expectedGroupMembers();
when(gitlabPaginatedHttpClient.get(eq(gitlabUrl), eq(gitlabToken), eq("/groups/42/members"), deserializerCaptor.capture())).thenReturn(expectedGroupMembers);
Set actualGroupMembers = underTest.getDirectGroupMembers(gitlabUrl, token, "42");
assertThat(actualGroupMembers).containsExactlyInAnyOrderElementsOf(expectedGroupMembers);
String responseContent = getResponseContent("group-members-full-response.json");
List deserializedUsers = deserializerCaptor.getValue().apply(responseContent);
assertThat(deserializedUsers).usingRecursiveComparison().isEqualTo(expectedGroupMembers);
}
@Test
public void getDirectGroupMembersWithInheritedMembers_whenCallIsInError_rethrows() {
String token = "token-toto";
GitlabToken gitlabToken = new GitlabToken(token);
when(gitlabPaginatedHttpClient.get(eq(gitlabUrl), eq(gitlabToken), eq("/groups/42/members/all"), any())).thenThrow(new IllegalStateException("exception"));
assertThatIllegalStateException()
.isThrownBy(() -> underTest.getAllGroupMembers(gitlabUrl, token, "42"))
.withMessage("exception");
}
@Test
public void getAllGroupMembers_whenCallIsSuccessful_deserializesAndReturnsCorrectlyGroupMembers() throws IOException {
ArgumentCaptor>> deserializerCaptor = ArgumentCaptor.forClass(Function.class);
String token = "token-toto";
GitlabToken gitlabToken = new GitlabToken(token);
List expectedGroupMembers = expectedGroupMembers();
when(gitlabPaginatedHttpClient.get(eq(gitlabUrl), eq(gitlabToken), eq("/groups/42/members/all"), deserializerCaptor.capture())).thenReturn(expectedGroupMembers);
Set actualGroupMembers = underTest.getAllGroupMembers(gitlabUrl, token, "42");
assertThat(actualGroupMembers).containsExactlyInAnyOrderElementsOf(expectedGroupMembers);
String responseContent = getResponseContent("group-members-full-response.json");
List deserializedUsers = deserializerCaptor.getValue().apply(responseContent);
assertThat(deserializedUsers).usingRecursiveComparison().isEqualTo(expectedGroupMembers);
}
private static List expectedGroupMembers() {
GsonUser user1 = createGsonUser(12818153, "aurelien-poscia-sonarsource", "Aurelien", 50);
GsonUser user2 = createGsonUser(10941672, "antoine.vigneau", "Antoine Vigneau", 30);
GsonUser user3 = createGsonUser(13569073, "wojciech.wajerowicz.sonarsource", "Wojciech Wajerowicz", 30);
return List.of(user1, user2, user3);
}
private static GsonUser createGsonUser(int id, String username, String name, int accessLevel) {
GsonUser gsonUser = mock();
when(gsonUser.getId()).thenReturn((long) id);
when(gsonUser.getUsername()).thenReturn(username);
when(gsonUser.getName()).thenReturn(name);
when(gsonUser.getAccessLevel()).thenReturn(accessLevel);
return gsonUser;
}
@Test
public void getAllProjectMembers_whenCallIsSuccesfull_deserializesAndReturnsCorrectlyProjectsMembers() throws IOException {
ArgumentCaptor>> deserializerCaptor = ArgumentCaptor.forClass(Function.class);
String token = "token-toto";
GitlabToken gitlabToken = new GitlabToken(token);
List expectedProjectMembers = expectedProjectMembers();
when(gitlabPaginatedHttpClient.get(eq(gitlabUrl), eq(gitlabToken), eq("/projects/42/members/all"), deserializerCaptor.capture())).thenReturn(expectedProjectMembers);
Set actualProjectMembers = underTest.getAllProjectMembers(gitlabUrl, token, 42);
assertThat(actualProjectMembers).containsExactlyInAnyOrderElementsOf(expectedProjectMembers);
String responseContent = getResponseContent("project-members-full-response.json");
List deserializedProjectMembers = deserializerCaptor.getValue().apply(responseContent);
assertThat(deserializedProjectMembers).isEqualTo(expectedProjectMembers);
}
@Test
public void getAllProjectMembers_whenCallIsInError_rethrows() {
String token = "token-toto";
GitlabToken gitlabToken = new GitlabToken(token);
when(gitlabPaginatedHttpClient.get(eq(gitlabUrl), eq(gitlabToken), eq("/projects/42/members/all"), any())).thenThrow(new IllegalStateException("exception"));
assertThatIllegalStateException()
.isThrownBy(() -> underTest.getAllProjectMembers(gitlabUrl, token, 42))
.withMessage("exception");
}
private static List expectedProjectMembers() {
GsonProjectMember user1 = createGsonProjectMember(12818153, 5, null);
GsonProjectMember user2 = createGsonProjectMember(22330087, 50, null);
GsonProjectMember user3 = createGsonProjectMember(20824381, 40, new GsonMemberRole("custom-role"));
return List.of(user1, user2, user3);
}
private static GsonProjectMember createGsonProjectMember(int id, int accessLevel, @Nullable GsonMemberRole gsonMemberRole) {
return new GsonProjectMember(id, accessLevel, gsonMemberRole);
}
private static String getResponseContent(String path) throws IOException {
return IOUtils.toString(GitlabApplicationClientTest.class.getResourceAsStream(path), StandardCharsets.UTF_8);
}
}