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.tngtech.java.junit.dataprovider.DataProvider;
23 import com.tngtech.java.junit.dataprovider.DataProviderRunner;
24 import com.tngtech.java.junit.dataprovider.UseDataProvider;
25 import java.io.IOException;
26 import java.net.SocketTimeoutException;
27 import java.util.concurrent.Callable;
28 import okhttp3.mockwebserver.MockResponse;
29 import okhttp3.mockwebserver.MockWebServer;
30 import okhttp3.mockwebserver.RecordedRequest;
31 import okhttp3.mockwebserver.SocketPolicy;
32 import org.junit.Before;
33 import org.junit.ClassRule;
34 import org.junit.Rule;
35 import org.junit.Test;
36 import org.junit.runner.RunWith;
37 import org.slf4j.event.Level;
38 import org.sonar.alm.client.ConstantTimeoutConfiguration;
39 import org.sonar.alm.client.TimeoutConfiguration;
40 import org.sonar.alm.client.github.ApplicationHttpClient.GetResponse;
41 import org.sonar.alm.client.github.ApplicationHttpClient.Response;
42 import org.sonar.alm.client.github.security.AccessToken;
43 import org.sonar.alm.client.github.security.UserAccessToken;
44 import org.sonar.api.testfixtures.log.LogTester;
45 import org.sonar.api.utils.log.LoggerLevel;
47 import static java.lang.String.format;
48 import static org.apache.commons.lang.RandomStringUtils.randomAlphabetic;
49 import static org.assertj.core.api.Assertions.assertThat;
50 import static org.assertj.core.api.Assertions.assertThatThrownBy;
51 import static org.junit.Assert.fail;
52 import static org.sonar.alm.client.github.ApplicationHttpClient.RateLimit;
54 @RunWith(DataProviderRunner.class)
55 public class GenericApplicationHttpClientTest {
56 private static final String GH_API_VERSION_HEADER = "X-GitHub-Api-Version";
57 private static final String GH_API_VERSION = "2022-11-28";
60 public MockWebServer server = new MockWebServer();
63 public static LogTester logTester = new LogTester().setLevel(LoggerLevel.WARN);
65 private GenericApplicationHttpClient underTest;
67 private final AccessToken accessToken = new UserAccessToken(randomAlphabetic(10));
68 private final String randomEndPoint = "/" + randomAlphabetic(10);
69 private final String randomBody = randomAlphabetic(40);
70 private String appUrl;
74 this.appUrl = format("http://%s:%s", server.getHostName(), server.getPort());
75 this.underTest = new TestApplicationHttpClient(new GithubHeaders(), new ConstantTimeoutConfiguration(500));
79 private class TestApplicationHttpClient extends GenericApplicationHttpClient {
80 public TestApplicationHttpClient(DevopsPlatformHeaders devopsPlatformHeaders, TimeoutConfiguration timeoutConfiguration) {
81 super(devopsPlatformHeaders, timeoutConfiguration);
86 public void get_fails_if_endpoint_does_not_start_with_slash() throws IOException {
87 assertThatThrownBy(() -> underTest.get(appUrl, accessToken, "api/foo/bar"))
88 .hasMessage("endpoint must start with '/' or 'http'")
89 .isInstanceOf(IllegalArgumentException.class);
93 public void get_fails_if_endpoint_does_not_start_with_http() throws IOException {
94 assertThatThrownBy(() -> underTest.get(appUrl, accessToken, "ttp://api/foo/bar"))
95 .isInstanceOf(IllegalArgumentException.class)
96 .hasMessage("endpoint must start with '/' or 'http'");
100 public void get_fails_if_github_endpoint_is_invalid() throws IOException {
101 assertThatThrownBy(() -> underTest.get("invalidUrl", accessToken, "/endpoint"))
102 .isInstanceOf(IllegalArgumentException.class)
103 .hasMessage("invalidUrl/endpoint is not a valid url");
107 public void getSilent_no_log_if_code_is_not_200() throws IOException {
108 server.enqueue(new MockResponse().setResponseCode(403));
110 GetResponse response = underTest.getSilent(appUrl, accessToken, randomEndPoint);
112 assertThat(logTester.logs()).isEmpty();
113 assertThat(response.getContent()).isEmpty();
118 public void get_log_if_code_is_not_200() throws IOException {
119 server.enqueue(new MockResponse().setResponseCode(403));
121 GetResponse response = underTest.get(appUrl, accessToken, randomEndPoint);
123 assertThat(logTester.logs(Level.WARN)).isNotEmpty();
124 assertThat(response.getContent()).isEmpty();
129 public void get_adds_authentication_header_with_Bearer_type_and_Accept_header() throws IOException, InterruptedException {
130 server.enqueue(new MockResponse());
132 GetResponse response = underTest.get(appUrl, accessToken, randomEndPoint);
134 assertThat(response).isNotNull();
135 RecordedRequest recordedRequest = server.takeRequest();
136 assertThat(recordedRequest.getMethod()).isEqualTo("GET");
137 assertThat(recordedRequest.getPath()).isEqualTo(randomEndPoint);
138 assertThat(recordedRequest.getHeader("Authorization")).isEqualTo("token " + accessToken.getValue());
139 assertThat(recordedRequest.getHeader(GH_API_VERSION_HEADER)).isEqualTo(GH_API_VERSION);
143 public void get_returns_body_as_response_if_code_is_200() throws IOException {
144 server.enqueue(new MockResponse().setResponseCode(200).setBody(randomBody));
146 GetResponse response = underTest.get(appUrl, accessToken, randomEndPoint);
148 assertThat(response.getContent()).contains(randomBody);
152 public void get_timeout() {
153 server.enqueue(new MockResponse().setSocketPolicy(SocketPolicy.NO_RESPONSE));
156 underTest.get(appUrl, accessToken, randomEndPoint);
157 fail("Expected timeout");
158 } catch (Exception e) {
159 assertThat(e).isInstanceOf(SocketTimeoutException.class);
164 @UseDataProvider("someHttpCodesWithContentBut200")
165 public void get_empty_response_if_code_is_not_200(int code) throws IOException {
166 server.enqueue(new MockResponse().setResponseCode(code).setBody(randomBody));
168 GetResponse response = underTest.get(appUrl, accessToken, randomEndPoint);
170 assertThat(response.getContent()).contains(randomBody);
174 public void get_returns_empty_endPoint_when_no_link_header() throws IOException {
175 server.enqueue(new MockResponse().setBody(randomBody));
177 GetResponse response = underTest.get(appUrl, accessToken, randomEndPoint);
179 assertThat(response.getNextEndPoint()).isEmpty();
183 public void get_returns_empty_endPoint_when_link_header_does_not_have_next_rel() throws IOException {
184 server.enqueue(new MockResponse().setBody(randomBody)
185 .setHeader("link", "<https://api.github.com/installation/repositories?per_page=5&page=4>; rel=\"prev\", " +
186 "<https://api.github.com/installation/repositories?per_page=5&page=1>; rel=\"first\""));
188 GetResponse response = underTest.get(appUrl, accessToken, randomEndPoint);
190 assertThat(response.getNextEndPoint()).isEmpty();
194 @UseDataProvider("linkHeadersWithNextRel")
195 public void get_returns_endPoint_when_link_header_has_next_rel(String linkHeader) throws IOException {
196 server.enqueue(new MockResponse().setBody(randomBody)
197 .setHeader("link", linkHeader));
199 GetResponse response = underTest.get(appUrl, accessToken, randomEndPoint);
201 assertThat(response.getNextEndPoint()).contains("https://api.github.com/installation/repositories?per_page=5&page=2");
205 public void get_returns_endPoint_when_link_header_has_next_rel_different_case() throws IOException {
206 String linkHeader = "<https://api.github.com/installation/repositories?per_page=5&page=2>; rel=\"next\"";
207 server.enqueue(new MockResponse().setBody(randomBody)
208 .setHeader("Link", linkHeader));
210 GetResponse response = underTest.get(appUrl, accessToken, randomEndPoint);
212 assertThat(response.getNextEndPoint()).contains("https://api.github.com/installation/repositories?per_page=5&page=2");
216 public static Object[][] linkHeadersWithNextRel() {
217 String expected = "https://api.github.com/installation/repositories?per_page=5&page=2";
218 return new Object[][] {
219 {"<" + expected + ">; rel=\"next\""},
220 {"<" + expected + ">; rel=\"next\", " +
221 "<https://api.github.com/installation/repositories?per_page=5&page=1>; rel=\"first\""},
222 {"<https://api.github.com/installation/repositories?per_page=5&page=1>; rel=\"first\", " +
223 "<" + expected + ">; rel=\"next\""},
224 {"<https://api.github.com/installation/repositories?per_page=5&page=1>; rel=\"first\", " +
225 "<" + expected + ">; rel=\"next\", " +
226 "<https://api.github.com/installation/repositories?per_page=5&page=5>; rel=\"last\""},
231 public static Object[][] someHttpCodesWithContentBut200() {
232 return new Object[][] {
242 public void post_fails_if_endpoint_does_not_start_with_slash() throws IOException {
243 assertThatThrownBy(() -> underTest.post(appUrl, accessToken, "api/foo/bar"))
244 .isInstanceOf(IllegalArgumentException.class)
245 .hasMessage("endpoint must start with '/' or 'http'");
249 public void post_fails_if_endpoint_does_not_start_with_http() throws IOException {
250 assertThatThrownBy(() -> underTest.post(appUrl, accessToken, "ttp://api/foo/bar"))
251 .isInstanceOf(IllegalArgumentException.class)
252 .hasMessage("endpoint must start with '/' or 'http'");
256 public void post_fails_if_github_endpoint_is_invalid() throws IOException {
257 assertThatThrownBy(() -> underTest.post("invalidUrl", accessToken, "/endpoint"))
258 .isInstanceOf(IllegalArgumentException.class)
259 .hasMessage("invalidUrl/endpoint is not a valid url");
263 public void post_adds_authentication_header_with_Bearer_type_and_Accept_header() throws IOException, InterruptedException {
264 server.enqueue(new MockResponse());
266 Response response = underTest.post(appUrl, accessToken, randomEndPoint);
268 assertThat(response).isNotNull();
269 RecordedRequest recordedRequest = server.takeRequest();
270 assertThat(recordedRequest.getMethod()).isEqualTo("POST");
271 assertThat(recordedRequest.getPath()).isEqualTo(randomEndPoint);
272 assertThat(recordedRequest.getHeader("Authorization")).isEqualTo("token " + accessToken.getValue());
273 assertThat(recordedRequest.getHeader(GH_API_VERSION_HEADER)).isEqualTo(GH_API_VERSION);
277 @DataProvider({"200", "201", "202"})
278 public void post_returns_body_as_response_if_success(int code) throws IOException {
279 server.enqueue(new MockResponse().setResponseCode(code).setBody(randomBody));
281 Response response = underTest.post(appUrl, accessToken, randomEndPoint);
283 assertThat(response.getContent()).contains(randomBody);
287 public void post_returns_empty_response_if_code_is_204() throws IOException {
288 server.enqueue(new MockResponse().setResponseCode(204));
290 Response response = underTest.post(appUrl, accessToken, randomEndPoint);
292 assertThat(response.getContent()).isEmpty();
296 @UseDataProvider("httpCodesBut200_201And204")
297 public void post_has_json_error_in_body_if_code_is_neither_200_201_nor_204(int code) throws IOException {
298 server.enqueue(new MockResponse().setResponseCode(code).setBody(randomBody));
300 Response response = underTest.post(appUrl, accessToken, randomEndPoint);
302 assertThat(response.getContent()).contains(randomBody);
306 public static Object[][] httpCodesBut200_201And204() {
307 return new Object[][] {
319 public void post_with_json_body_adds_json_to_body_request() throws IOException, InterruptedException {
320 server.enqueue(new MockResponse());
321 String jsonBody = "{\"foo\": \"bar\"}";
322 Response response = underTest.post(appUrl, accessToken, randomEndPoint, jsonBody);
324 assertThat(response).isNotNull();
325 RecordedRequest recordedRequest = server.takeRequest();
326 assertThat(recordedRequest.getBody().readUtf8()).isEqualTo(jsonBody);
330 public void patch_with_json_body_adds_json_to_body_request() throws IOException, InterruptedException {
331 server.enqueue(new MockResponse());
332 String jsonBody = "{\"foo\": \"bar\"}";
334 Response response = underTest.patch(appUrl, accessToken, randomEndPoint, jsonBody);
336 assertThat(response).isNotNull();
337 RecordedRequest recordedRequest = server.takeRequest();
338 assertThat(recordedRequest.getBody().readUtf8()).isEqualTo(jsonBody);
342 public void patch_returns_body_as_response_if_code_is_200() throws IOException {
343 server.enqueue(new MockResponse().setResponseCode(200).setBody(randomBody));
345 Response response = underTest.patch(appUrl, accessToken, randomEndPoint, "{}");
347 assertThat(response.getContent()).contains(randomBody);
351 public void patch_returns_empty_response_if_code_is_204() throws IOException {
352 server.enqueue(new MockResponse().setResponseCode(204));
354 Response response = underTest.patch(appUrl, accessToken, randomEndPoint, "{}");
356 assertThat(response.getContent()).isEmpty();
360 public void delete_returns_empty_response_if_code_is_204() throws IOException {
361 server.enqueue(new MockResponse().setResponseCode(204));
363 Response response = underTest.delete(appUrl, accessToken, randomEndPoint);
365 assertThat(response.getContent()).isEmpty();
369 public static Object[][] httpCodesBut204() {
370 return new Object[][] {
384 @UseDataProvider("httpCodesBut204")
385 public void delete_returns_response_if_code_is_not_204(int code) throws IOException {
386 server.enqueue(new MockResponse().setResponseCode(code).setBody(randomBody));
388 Response response = underTest.delete(appUrl, accessToken, randomEndPoint);
390 assertThat(response.getContent()).hasValue(randomBody);
394 public static Object[][] httpCodesBut200And204() {
395 return new Object[][] {
408 @UseDataProvider("httpCodesBut200And204")
409 public void patch_has_json_error_in_body_if_code_is_neither_200_nor_204(int code) throws IOException {
410 server.enqueue(new MockResponse().setResponseCode(code).setBody(randomBody));
412 Response response = underTest.patch(appUrl, accessToken, randomEndPoint, "{}");
414 assertThat(response.getContent()).contains(randomBody);
418 public void get_whenRateLimitHeadersArePresent_returnsRateLimit() throws Exception {
419 testRateLimitHeader(() -> underTest.get(appUrl, accessToken, randomEndPoint));
422 private void testRateLimitHeader(Callable<Response> request ) throws Exception {
423 server.enqueue(new MockResponse().setBody(randomBody)
424 .setHeader("x-ratelimit-remaining", "1")
425 .setHeader("x-ratelimit-limit", "10")
426 .setHeader("x-ratelimit-reset", "1000"));
428 Response response = request.call();
430 assertThat(response.getRateLimit())
431 .isEqualTo(new RateLimit(1, 10, 1000L));
435 public void get_whenRateLimitHeadersAreMissing_returnsNull() throws Exception {
437 testMissingRateLimitHeader(() -> underTest.get(appUrl, accessToken, randomEndPoint));
441 private void testMissingRateLimitHeader(Callable<Response> request ) throws Exception {
442 server.enqueue(new MockResponse().setBody(randomBody));
444 Response response = request.call();
445 assertThat(response.getRateLimit())
450 public void delete_whenRateLimitHeadersArePresent_returnsRateLimit() throws Exception {
451 testRateLimitHeader(() -> underTest.delete(appUrl, accessToken, randomEndPoint));
456 public void delete_whenRateLimitHeadersAreMissing_returnsNull() throws Exception {
457 testMissingRateLimitHeader(() -> underTest.delete(appUrl, accessToken, randomEndPoint));
462 public void patch_whenRateLimitHeadersArePresent_returnsRateLimit() throws Exception {
463 testRateLimitHeader(() -> underTest.patch(appUrl, accessToken, randomEndPoint, "body"));
467 public void patch_whenRateLimitHeadersAreMissing_returnsNull() throws Exception {
468 testMissingRateLimitHeader(() -> underTest.patch(appUrl, accessToken, randomEndPoint, "body"));
472 public void post_whenRateLimitHeadersArePresent_returnsRateLimit() throws Exception {
473 testRateLimitHeader(() -> underTest.post(appUrl, accessToken, randomEndPoint));
477 public void post_whenRateLimitHeadersAreMissing_returnsNull() throws Exception {
478 testMissingRateLimitHeader(() -> underTest.post(appUrl, accessToken, randomEndPoint));