You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

GenericPaginatedHttpClientImplTest.java 8.7KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194
  1. /*
  2. * SonarQube
  3. * Copyright (C) 2009-2024 SonarSource SA
  4. * mailto:info AT sonarsource DOT com
  5. *
  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.
  10. *
  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.
  15. *
  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.
  19. */
  20. package org.sonar.alm.client;
  21. import com.google.gson.Gson;
  22. import com.google.gson.reflect.TypeToken;
  23. import java.io.IOException;
  24. import java.lang.reflect.Type;
  25. import java.util.List;
  26. import java.util.Optional;
  27. import org.junit.Rule;
  28. import org.junit.Test;
  29. import org.junit.runner.RunWith;
  30. import org.mockito.ArgumentCaptor;
  31. import org.mockito.InjectMocks;
  32. import org.mockito.Mock;
  33. import org.mockito.junit.MockitoJUnitRunner;
  34. import org.slf4j.event.Level;
  35. import org.sonar.auth.github.security.AccessToken;
  36. import org.sonar.api.testfixtures.log.LogTester;
  37. import static org.assertj.core.api.Assertions.assertThat;
  38. import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
  39. import static org.assertj.core.api.Assertions.assertThatNoException;
  40. import static org.mockito.ArgumentMatchers.any;
  41. import static org.mockito.ArgumentMatchers.eq;
  42. import static org.mockito.Mockito.doThrow;
  43. import static org.mockito.Mockito.mock;
  44. import static org.mockito.Mockito.verify;
  45. import static org.mockito.Mockito.when;
  46. import static org.sonar.alm.client.ApplicationHttpClient.GetResponse;
  47. @RunWith(MockitoJUnitRunner.class)
  48. public class GenericPaginatedHttpClientImplTest {
  49. private static final String APP_URL = "https://github.com/";
  50. private static final String ENDPOINT = "/test-endpoint";
  51. private static final TypeToken<List<String>> STRING_LIST_TYPE = new TypeToken<>() {
  52. };
  53. private Gson gson = new Gson();
  54. @Rule
  55. public LogTester logTester = new LogTester();
  56. @Mock
  57. private AccessToken accessToken;
  58. @Mock
  59. RatioBasedRateLimitChecker rateLimitChecker;
  60. @Mock
  61. ApplicationHttpClient appHttpClient;
  62. @InjectMocks
  63. private TestPaginatedHttpClient underTest;
  64. private static class TestPaginatedHttpClient extends GenericPaginatedHttpClient {
  65. protected TestPaginatedHttpClient(ApplicationHttpClient appHttpClient, RatioBasedRateLimitChecker rateLimitChecker) {
  66. super(appHttpClient, rateLimitChecker);
  67. }
  68. }
  69. @Test
  70. public void get_whenNoPagination_ReturnsCorrectResponse() throws IOException {
  71. GetResponse response = mockResponseWithoutPagination("[\"result1\", \"result2\"]");
  72. when(appHttpClient.get(APP_URL, accessToken, ENDPOINT + "?per_page=100")).thenReturn(response);
  73. List<String> results = underTest.get(APP_URL, accessToken, ENDPOINT, result -> gson.fromJson(result, STRING_LIST_TYPE));
  74. assertThat(results)
  75. .containsExactly("result1", "result2");
  76. }
  77. @Test
  78. public void get_whenEndpointAlreadyContainsPathParameter_shouldAddANewParameter() throws IOException {
  79. ArgumentCaptor<String> urlCaptor = ArgumentCaptor.forClass(String.class);
  80. GetResponse response = mockResponseWithoutPagination("[\"result1\", \"result2\"]");
  81. when(appHttpClient.get(eq(APP_URL), eq(accessToken), urlCaptor.capture())).thenReturn(response);
  82. underTest.get(APP_URL, accessToken, ENDPOINT + "?alreadyExistingArg=2", result -> gson.fromJson(result, STRING_LIST_TYPE));
  83. assertThat(urlCaptor.getValue()).isEqualTo(ENDPOINT + "?alreadyExistingArg=2&per_page=100");
  84. }
  85. private static GetResponse mockResponseWithoutPagination(String content) {
  86. GetResponse response = mock(GetResponse.class);
  87. when(response.getCode()).thenReturn(200);
  88. when(response.getContent()).thenReturn(Optional.of(content));
  89. return response;
  90. }
  91. @Test
  92. public void get_whenPaginationAndRateLimiting_returnsResponseFromAllPages() throws IOException, InterruptedException {
  93. GetResponse response1 = mockResponseWithPaginationAndRateLimit("[\"result1\", \"result2\"]", "/next-endpoint");
  94. GetResponse response2 = mockResponseWithoutPagination("[\"result3\"]");
  95. when(appHttpClient.get(APP_URL, accessToken, ENDPOINT + "?per_page=100")).thenReturn(response1);
  96. when(appHttpClient.get(APP_URL, accessToken, "/next-endpoint")).thenReturn(response2);
  97. List<String> results = underTest.get(APP_URL, accessToken, ENDPOINT, result -> gson.fromJson(result, STRING_LIST_TYPE));
  98. assertThat(results)
  99. .containsExactly("result1", "result2", "result3");
  100. ArgumentCaptor<ApplicationHttpClient.RateLimit> rateLimitRecordCaptor = ArgumentCaptor.forClass(ApplicationHttpClient.RateLimit.class);
  101. verify(rateLimitChecker).checkRateLimit(rateLimitRecordCaptor.capture());
  102. ApplicationHttpClient.RateLimit rateLimitRecord = rateLimitRecordCaptor.getValue();
  103. assertThat(rateLimitRecord.limit()).isEqualTo(10);
  104. assertThat(rateLimitRecord.remaining()).isEqualTo(1);
  105. assertThat(rateLimitRecord.reset()).isZero();
  106. }
  107. private static GetResponse mockResponseWithPaginationAndRateLimit(String content, String nextEndpoint) {
  108. GetResponse response = mockResponseWithoutPagination(content);
  109. when(response.getCode()).thenReturn(200);
  110. when(response.getNextEndPoint()).thenReturn(Optional.of(nextEndpoint));
  111. when(response.getRateLimit()).thenReturn(new ApplicationHttpClient.RateLimit(1, 10, 0L));
  112. return response;
  113. }
  114. @Test
  115. public void get_whenGitHubReturnsNonSuccessCode_shouldThrow() throws IOException {
  116. GetResponse response1 = mockResponseWithPaginationAndRateLimit("[\"result1\", \"result2\"]", "/next-endpoint");
  117. GetResponse response2 = mockFailedResponse("failed");
  118. when(appHttpClient.get(APP_URL, accessToken, ENDPOINT + "?per_page=100")).thenReturn(response1);
  119. when(appHttpClient.get(APP_URL, accessToken, "/next-endpoint")).thenReturn(response2);
  120. assertThatIllegalStateException()
  121. .isThrownBy(() -> underTest.get(APP_URL, accessToken, ENDPOINT, result -> gson.fromJson(result, STRING_LIST_TYPE)))
  122. .withMessage("SonarQube was not able to retrieve resources from external system. Error while executing a paginated call to https://github.com/, endpoint:/next-endpoint. "
  123. + "Error while executing a call to https://github.com/. Return code 400. Error message: failed.");
  124. }
  125. private static GetResponse mockFailedResponse(String content) {
  126. GetResponse response = mock(GetResponse.class);
  127. when(response.getCode()).thenReturn(400);
  128. when(response.getContent()).thenReturn(Optional.of(content));
  129. return response;
  130. }
  131. @Test
  132. public void getRepositoryTeams_whenRateLimitCheckerThrowsInterruptedException_shouldSucceed() throws IOException, InterruptedException {
  133. GetResponse response1 = mockResponseWithPaginationAndRateLimit("[\"result1\", \"result2\"]", "/next-endpoint");
  134. GetResponse response2 = mockResponseWithoutPagination("[\"result3\"]");
  135. when(appHttpClient.get(APP_URL, accessToken, ENDPOINT + "?per_page=100")).thenReturn(response1);
  136. when(appHttpClient.get(APP_URL, accessToken, "/next-endpoint")).thenReturn(response2);
  137. doThrow(new InterruptedException("interrupted")).when(rateLimitChecker).checkRateLimit(any(ApplicationHttpClient.RateLimit.class));
  138. assertThatNoException()
  139. .isThrownBy(() -> underTest.get(APP_URL, accessToken, ENDPOINT, result -> gson.fromJson(result, STRING_LIST_TYPE)));
  140. assertThat(logTester.logs()).hasSize(1);
  141. assertThat(logTester.logs(Level.WARN))
  142. .containsExactly("Thread interrupted: interrupted");
  143. }
  144. @Test
  145. public void getRepositoryCollaborators_whenDevOpsPlatformCallThrowsIOException_shouldLogAndReThrow() throws IOException {
  146. AccessToken accessToken = mock();
  147. when(appHttpClient.get(APP_URL, accessToken, "query?per_page=100")).thenThrow(new IOException("error"));
  148. assertThatIllegalStateException()
  149. .isThrownBy(() -> underTest.get(APP_URL, accessToken, "query", mock()))
  150. .isInstanceOf(IllegalStateException.class)
  151. .withMessage("SonarQube was not able to retrieve resources from external system. Error while executing a paginated call to https://github.com/, "
  152. + "endpoint:query?per_page=100. error");
  153. assertThat(logTester.logs()).hasSize(1);
  154. assertThat(logTester.logs(Level.WARN))
  155. .containsExactly("SonarQube was not able to retrieve resources from external system. "
  156. + "Error while executing a paginated call to https://github.com/, endpoint:query?per_page=100.");
  157. }
  158. }