3 * Copyright (C) 2009-2023 SonarSource SA
4 * mailto:info AT sonarsource DOT com
6 * This program is free software; you can redistribute it and/or
7 * modify it under the terms of the GNU Lesser General Public
8 * License as published by the Free Software Foundation; either
9 * version 3 of the License, or (at your option) any later version.
11 * This program is distributed in the hope that it will be useful,
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
14 * Lesser General Public License for more details.
16 * You should have received a copy of the GNU Lesser General Public License
17 * along with this program; if not, write to the Free Software Foundation,
18 * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
20 package org.sonar.alm.client.github;
22 import com.google.gson.Gson;
23 import com.google.gson.reflect.TypeToken;
24 import java.io.IOException;
25 import java.lang.reflect.Type;
26 import java.util.List;
27 import java.util.Optional;
28 import org.junit.Rule;
29 import org.junit.Test;
30 import org.junit.runner.RunWith;
31 import org.mockito.ArgumentCaptor;
32 import org.mockito.InjectMocks;
33 import org.mockito.Mock;
34 import org.mockito.junit.MockitoJUnitRunner;
35 import org.slf4j.event.Level;
36 import org.sonar.alm.client.github.security.AccessToken;
37 import org.sonar.api.testfixtures.log.LogTester;
39 import static org.assertj.core.api.Assertions.assertThat;
40 import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
41 import static org.assertj.core.api.Assertions.assertThatNoException;
42 import static org.mockito.ArgumentMatchers.any;
43 import static org.mockito.ArgumentMatchers.eq;
44 import static org.mockito.Mockito.doThrow;
45 import static org.mockito.Mockito.mock;
46 import static org.mockito.Mockito.verify;
47 import static org.mockito.Mockito.when;
48 import static org.sonar.alm.client.github.ApplicationHttpClient.GetResponse;
50 @RunWith(MockitoJUnitRunner.class)
51 public class GithubPaginatedHttpClientImplTest {
53 private static final String APP_URL = "https://github.com/";
55 private static final String ENDPOINT = "/test-endpoint";
57 private static final Type STRING_LIST_TYPE = TypeToken.getParameterized(List.class, String.class).getType();
59 private Gson gson = new Gson();
62 public LogTester logTester = new LogTester();
65 private AccessToken accessToken;
68 RatioBasedRateLimitChecker rateLimitChecker;
71 ApplicationHttpClient appHttpClient;
74 private GithubPaginatedHttpClient underTest;
77 public void get_whenNoPagination_ReturnsCorrectResponse() throws IOException {
79 GetResponse response = mockResponseWithoutPagination("[\"result1\", \"result2\"]");
80 when(appHttpClient.get(APP_URL, accessToken, ENDPOINT + "?per_page=100")).thenReturn(response);
82 List<String> results = underTest.get(APP_URL, accessToken, ENDPOINT, result -> gson.fromJson(result, STRING_LIST_TYPE));
85 .containsExactly("result1", "result2");
89 public void get_whenEndpointAlreadyContainsPathParameter_shouldAddANewParameter() throws IOException {
90 ArgumentCaptor<String> urlCaptor = ArgumentCaptor.forClass(String.class);
92 GetResponse response = mockResponseWithoutPagination("[\"result1\", \"result2\"]");
93 when(appHttpClient.get(eq(APP_URL), eq(accessToken), urlCaptor.capture())).thenReturn(response);
95 underTest.get(APP_URL, accessToken, ENDPOINT + "?alreadyExistingArg=2", result -> gson.fromJson(result, STRING_LIST_TYPE));
97 assertThat(urlCaptor.getValue()).isEqualTo(ENDPOINT + "?alreadyExistingArg=2&per_page=100");
100 private static GetResponse mockResponseWithoutPagination(String content) {
101 GetResponse response = mock(GetResponse.class);
102 when(response.getCode()).thenReturn(200);
103 when(response.getContent()).thenReturn(Optional.of(content));
108 public void get_whenPaginationAndRateLimiting_returnsResponseFromAllPages() throws IOException, InterruptedException {
109 GetResponse response1 = mockResponseWithPaginationAndRateLimit("[\"result1\", \"result2\"]", "/next-endpoint");
110 GetResponse response2 = mockResponseWithoutPagination("[\"result3\"]");
111 when(appHttpClient.get(APP_URL, accessToken, ENDPOINT + "?per_page=100")).thenReturn(response1);
112 when(appHttpClient.get(APP_URL, accessToken, "/next-endpoint")).thenReturn(response2);
114 List<String> results = underTest.get(APP_URL, accessToken, ENDPOINT, result -> gson.fromJson(result, STRING_LIST_TYPE));
117 .containsExactly("result1", "result2", "result3");
119 ArgumentCaptor<ApplicationHttpClient.RateLimit> rateLimitRecordCaptor = ArgumentCaptor.forClass(ApplicationHttpClient.RateLimit.class);
120 verify(rateLimitChecker).checkRateLimit(rateLimitRecordCaptor.capture());
121 ApplicationHttpClient.RateLimit rateLimitRecord = rateLimitRecordCaptor.getValue();
122 assertThat(rateLimitRecord.limit()).isEqualTo(10);
123 assertThat(rateLimitRecord.remaining()).isEqualTo(1);
124 assertThat(rateLimitRecord.reset()).isZero();
127 private static GetResponse mockResponseWithPaginationAndRateLimit(String content, String nextEndpoint) {
128 GetResponse response = mockResponseWithoutPagination(content);
129 when(response.getCode()).thenReturn(200);
130 when(response.getNextEndPoint()).thenReturn(Optional.of(nextEndpoint));
131 when(response.getRateLimit()).thenReturn(new ApplicationHttpClient.RateLimit(1, 10, 0L));
136 public void get_whenGitHubReturnsNonSuccessCode_shouldThrow() throws IOException {
137 GetResponse response1 = mockResponseWithPaginationAndRateLimit("[\"result1\", \"result2\"]", "/next-endpoint");
138 GetResponse response2 = mockFailedResponse("failed");
139 when(appHttpClient.get(APP_URL, accessToken, ENDPOINT + "?per_page=100")).thenReturn(response1);
140 when(appHttpClient.get(APP_URL, accessToken, "/next-endpoint")).thenReturn(response2);
142 assertThatIllegalStateException()
143 .isThrownBy(() -> underTest.get(APP_URL, accessToken, ENDPOINT, result -> gson.fromJson(result, STRING_LIST_TYPE)))
144 .withMessage("Error while executing a call to GitHub. Return code 400. Error message: failed.");
147 private static GetResponse mockFailedResponse(String content) {
148 GetResponse response = mock(GetResponse.class);
149 when(response.getCode()).thenReturn(400);
150 when(response.getContent()).thenReturn(Optional.of(content));
155 public void getRepositoryTeams_whenRateLimitCheckerThrowsInterruptedException_shouldSucceed() throws IOException, InterruptedException {
156 GetResponse response1 = mockResponseWithPaginationAndRateLimit("[\"result1\", \"result2\"]", "/next-endpoint");
157 GetResponse response2 = mockResponseWithoutPagination("[\"result3\"]");
158 when(appHttpClient.get(APP_URL, accessToken, ENDPOINT + "?per_page=100")).thenReturn(response1);
159 when(appHttpClient.get(APP_URL, accessToken, "/next-endpoint")).thenReturn(response2);
160 doThrow(new InterruptedException("interrupted")).when(rateLimitChecker).checkRateLimit(any(ApplicationHttpClient.RateLimit.class));
162 assertThatNoException()
163 .isThrownBy(() -> underTest.get(APP_URL, accessToken, ENDPOINT, result -> gson.fromJson(result, STRING_LIST_TYPE)));
165 assertThat(logTester.logs()).hasSize(1);
166 assertThat(logTester.logs(Level.WARN))
167 .containsExactly("Thread interrupted: interrupted");