]> source.dussan.org Git - sonarqube.git/blob
53a6fcf08c94a462ee15c7b9d4ab1552de55c4af
[sonarqube.git] /
1 /*
2  * SonarQube
3  * Copyright (C) 2009-2023 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.github;
21
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;
46
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;
53
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";
58
59   @Rule
60   public MockWebServer server = new MockWebServer();
61
62   @ClassRule
63   public static LogTester logTester = new LogTester().setLevel(LoggerLevel.WARN);
64
65   private GenericApplicationHttpClient underTest;
66
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;
71
72   @Before
73   public void setUp() {
74     this.appUrl = format("http://%s:%s", server.getHostName(), server.getPort());
75     this.underTest = new TestApplicationHttpClient(new GithubHeaders(), new ConstantTimeoutConfiguration(500));
76     logTester.clear();
77   }
78
79   private class TestApplicationHttpClient extends GenericApplicationHttpClient {
80     public TestApplicationHttpClient(DevopsPlatformHeaders devopsPlatformHeaders, TimeoutConfiguration timeoutConfiguration) {
81       super(devopsPlatformHeaders, timeoutConfiguration);
82     }
83   }
84
85   @Test
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);
90   }
91
92   @Test
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'");
97   }
98
99   @Test
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");
104   }
105
106   @Test
107   public void getSilent_no_log_if_code_is_not_200() throws IOException {
108     server.enqueue(new MockResponse().setResponseCode(403));
109
110     GetResponse response = underTest.getSilent(appUrl, accessToken, randomEndPoint);
111
112     assertThat(logTester.logs()).isEmpty();
113     assertThat(response.getContent()).isEmpty();
114
115   }
116
117   @Test
118   public void get_log_if_code_is_not_200() throws IOException {
119     server.enqueue(new MockResponse().setResponseCode(403));
120
121     GetResponse response = underTest.get(appUrl, accessToken, randomEndPoint);
122
123     assertThat(logTester.logs(Level.WARN)).isNotEmpty();
124     assertThat(response.getContent()).isEmpty();
125
126   }
127
128   @Test
129   public void get_adds_authentication_header_with_Bearer_type_and_Accept_header() throws IOException, InterruptedException {
130     server.enqueue(new MockResponse());
131
132     GetResponse response = underTest.get(appUrl, accessToken, randomEndPoint);
133
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);
140   }
141
142   @Test
143   public void get_returns_body_as_response_if_code_is_200() throws IOException {
144     server.enqueue(new MockResponse().setResponseCode(200).setBody(randomBody));
145
146     GetResponse response = underTest.get(appUrl, accessToken, randomEndPoint);
147
148     assertThat(response.getContent()).contains(randomBody);
149   }
150
151   @Test
152   public void get_timeout() {
153     server.enqueue(new MockResponse().setSocketPolicy(SocketPolicy.NO_RESPONSE));
154
155     try {
156       underTest.get(appUrl, accessToken, randomEndPoint);
157       fail("Expected timeout");
158     } catch (Exception e) {
159       assertThat(e).isInstanceOf(SocketTimeoutException.class);
160     }
161   }
162
163   @Test
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));
167
168     GetResponse response = underTest.get(appUrl, accessToken, randomEndPoint);
169
170     assertThat(response.getContent()).contains(randomBody);
171   }
172
173   @Test
174   public void get_returns_empty_endPoint_when_no_link_header() throws IOException {
175     server.enqueue(new MockResponse().setBody(randomBody));
176
177     GetResponse response = underTest.get(appUrl, accessToken, randomEndPoint);
178
179     assertThat(response.getNextEndPoint()).isEmpty();
180   }
181
182   @Test
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\""));
187
188     GetResponse response = underTest.get(appUrl, accessToken, randomEndPoint);
189
190     assertThat(response.getNextEndPoint()).isEmpty();
191   }
192
193   @Test
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));
198
199     GetResponse response = underTest.get(appUrl, accessToken, randomEndPoint);
200
201     assertThat(response.getNextEndPoint()).contains("https://api.github.com/installation/repositories?per_page=5&page=2");
202   }
203
204   @Test
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));
209
210     GetResponse response = underTest.get(appUrl, accessToken, randomEndPoint);
211
212     assertThat(response.getNextEndPoint()).contains("https://api.github.com/installation/repositories?per_page=5&page=2");
213   }
214
215   @DataProvider
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\""},
227     };
228   }
229
230   @DataProvider
231   public static Object[][] someHttpCodesWithContentBut200() {
232     return new Object[][] {
233       {201},
234       {202},
235       {203},
236       {404},
237       {500}
238     };
239   }
240
241   @Test
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'");
246   }
247
248   @Test
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'");
253   }
254
255   @Test
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");
260   }
261
262   @Test
263   public void post_adds_authentication_header_with_Bearer_type_and_Accept_header() throws IOException, InterruptedException {
264     server.enqueue(new MockResponse());
265
266     Response response = underTest.post(appUrl, accessToken, randomEndPoint);
267
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);
274   }
275
276   @Test
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));
280
281     Response response = underTest.post(appUrl, accessToken, randomEndPoint);
282
283     assertThat(response.getContent()).contains(randomBody);
284   }
285
286   @Test
287   public void post_returns_empty_response_if_code_is_204() throws IOException {
288     server.enqueue(new MockResponse().setResponseCode(204));
289
290     Response response = underTest.post(appUrl, accessToken, randomEndPoint);
291
292     assertThat(response.getContent()).isEmpty();
293   }
294
295   @Test
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));
299
300     Response response = underTest.post(appUrl, accessToken, randomEndPoint);
301
302     assertThat(response.getContent()).contains(randomBody);
303   }
304
305   @DataProvider
306   public static Object[][] httpCodesBut200_201And204() {
307     return new Object[][] {
308       {202},
309       {203},
310       {400},
311       {401},
312       {403},
313       {404},
314       {500}
315     };
316   }
317
318   @Test
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);
323
324     assertThat(response).isNotNull();
325     RecordedRequest recordedRequest = server.takeRequest();
326     assertThat(recordedRequest.getBody().readUtf8()).isEqualTo(jsonBody);
327   }
328
329   @Test
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\"}";
333
334     Response response = underTest.patch(appUrl, accessToken, randomEndPoint, jsonBody);
335
336     assertThat(response).isNotNull();
337     RecordedRequest recordedRequest = server.takeRequest();
338     assertThat(recordedRequest.getBody().readUtf8()).isEqualTo(jsonBody);
339   }
340
341   @Test
342   public void patch_returns_body_as_response_if_code_is_200() throws IOException {
343     server.enqueue(new MockResponse().setResponseCode(200).setBody(randomBody));
344
345     Response response = underTest.patch(appUrl, accessToken, randomEndPoint, "{}");
346
347     assertThat(response.getContent()).contains(randomBody);
348   }
349
350   @Test
351   public void patch_returns_empty_response_if_code_is_204() throws IOException {
352     server.enqueue(new MockResponse().setResponseCode(204));
353
354     Response response = underTest.patch(appUrl, accessToken, randomEndPoint, "{}");
355
356     assertThat(response.getContent()).isEmpty();
357   }
358
359   @Test
360   public void delete_returns_empty_response_if_code_is_204() throws IOException {
361     server.enqueue(new MockResponse().setResponseCode(204));
362
363     Response response = underTest.delete(appUrl, accessToken, randomEndPoint);
364
365     assertThat(response.getContent()).isEmpty();
366   }
367
368   @DataProvider
369   public static Object[][] httpCodesBut204() {
370     return new Object[][] {
371       {200},
372       {201},
373       {202},
374       {203},
375       {400},
376       {401},
377       {403},
378       {404},
379       {500}
380     };
381   }
382
383   @Test
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));
387
388     Response response = underTest.delete(appUrl, accessToken, randomEndPoint);
389
390     assertThat(response.getContent()).hasValue(randomBody);
391   }
392
393   @DataProvider
394   public static Object[][] httpCodesBut200And204() {
395     return new Object[][] {
396       {201},
397       {202},
398       {203},
399       {400},
400       {401},
401       {403},
402       {404},
403       {500}
404     };
405   }
406
407   @Test
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));
411
412     Response response = underTest.patch(appUrl, accessToken, randomEndPoint, "{}");
413
414     assertThat(response.getContent()).contains(randomBody);
415   }
416
417   @Test
418   public void get_whenRateLimitHeadersArePresent_returnsRateLimit() throws Exception {
419     testRateLimitHeader(() -> underTest.get(appUrl, accessToken, randomEndPoint));
420   }
421
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"));
427
428     Response response = request.call();
429
430     assertThat(response.getRateLimit())
431       .isEqualTo(new RateLimit(1, 10, 1000L));
432   }
433
434   @Test
435   public void get_whenRateLimitHeadersAreMissing_returnsNull() throws Exception {
436
437     testMissingRateLimitHeader(() -> underTest.get(appUrl, accessToken, randomEndPoint));
438
439   }
440
441   private void testMissingRateLimitHeader(Callable<Response> request ) throws Exception {
442     server.enqueue(new MockResponse().setBody(randomBody));
443
444     Response response = request.call();
445     assertThat(response.getRateLimit())
446       .isNull();
447   }
448
449   @Test
450   public void delete_whenRateLimitHeadersArePresent_returnsRateLimit() throws Exception {
451     testRateLimitHeader(() -> underTest.delete(appUrl, accessToken, randomEndPoint));
452
453   }
454
455   @Test
456   public void delete_whenRateLimitHeadersAreMissing_returnsNull() throws Exception {
457     testMissingRateLimitHeader(() -> underTest.delete(appUrl, accessToken, randomEndPoint));
458
459   }
460
461   @Test
462   public void patch_whenRateLimitHeadersArePresent_returnsRateLimit() throws Exception {
463     testRateLimitHeader(() -> underTest.patch(appUrl, accessToken, randomEndPoint, "body"));
464   }
465
466   @Test
467   public void patch_whenRateLimitHeadersAreMissing_returnsNull() throws Exception {
468     testMissingRateLimitHeader(() -> underTest.patch(appUrl, accessToken, randomEndPoint, "body"));
469   }
470
471   @Test
472   public void post_whenRateLimitHeadersArePresent_returnsRateLimit() throws Exception {
473     testRateLimitHeader(() -> underTest.post(appUrl, accessToken, randomEndPoint));
474   }
475
476   @Test
477   public void post_whenRateLimitHeadersAreMissing_returnsNull() throws Exception {
478     testMissingRateLimitHeader(() -> underTest.post(appUrl, accessToken, randomEndPoint));
479   }
480 }