]> source.dussan.org Git - sonarqube.git/blob
89fea3115ce20bf174f098f20f6f5d7307d57928
[sonarqube.git] /
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.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.DevopsPlatformHeaders;
40 import org.sonar.alm.client.GenericApplicationHttpClient;
41 import org.sonar.alm.client.TimeoutConfiguration;
42 import org.sonar.alm.client.ApplicationHttpClient.GetResponse;
43 import org.sonar.alm.client.ApplicationHttpClient.Response;
44 import org.sonar.auth.github.security.AccessToken;
45 import org.sonar.auth.github.security.UserAccessToken;
46 import org.sonar.api.testfixtures.log.LogTester;
47 import org.sonar.api.utils.log.LoggerLevel;
48
49 import static java.lang.String.format;
50 import static org.apache.commons.lang.RandomStringUtils.randomAlphabetic;
51 import static org.assertj.core.api.Assertions.assertThat;
52 import static org.assertj.core.api.Assertions.assertThatThrownBy;
53 import static org.junit.Assert.fail;
54 import static org.sonar.alm.client.ApplicationHttpClient.RateLimit;
55
56 @RunWith(DataProviderRunner.class)
57 public class GenericApplicationHttpClientTest {
58   private static final String GH_API_VERSION_HEADER = "X-GitHub-Api-Version";
59   private static final String GH_API_VERSION = "2022-11-28";
60
61   @Rule
62   public MockWebServer server = new MockWebServer();
63
64   @ClassRule
65   public static LogTester logTester = new LogTester().setLevel(LoggerLevel.WARN);
66
67   private GenericApplicationHttpClient underTest;
68
69   private final AccessToken accessToken = new UserAccessToken(randomAlphabetic(10));
70   private final String randomEndPoint = "/" + randomAlphabetic(10);
71   private final String randomBody = randomAlphabetic(40);
72   private String appUrl;
73
74   @Before
75   public void setUp() {
76     this.appUrl = format("http://%s:%s", server.getHostName(), server.getPort());
77     this.underTest = new TestApplicationHttpClient(new GithubHeaders(), new ConstantTimeoutConfiguration(500));
78     logTester.clear();
79   }
80
81   private static class TestApplicationHttpClient extends GenericApplicationHttpClient {
82     public TestApplicationHttpClient(DevopsPlatformHeaders devopsPlatformHeaders, TimeoutConfiguration timeoutConfiguration) {
83       super(devopsPlatformHeaders, timeoutConfiguration);
84     }
85   }
86
87   @Test
88   public void get_fails_if_endpoint_does_not_start_with_slash() throws IOException {
89     assertThatThrownBy(() -> underTest.get(appUrl, accessToken, "api/foo/bar"))
90       .hasMessage("endpoint must start with '/' or 'http'")
91       .isInstanceOf(IllegalArgumentException.class);
92   }
93
94   @Test
95   public void get_fails_if_endpoint_does_not_start_with_http() throws IOException {
96     assertThatThrownBy(() -> underTest.get(appUrl, accessToken, "ttp://api/foo/bar"))
97       .isInstanceOf(IllegalArgumentException.class)
98       .hasMessage("endpoint must start with '/' or 'http'");
99   }
100
101   @Test
102   public void get_fails_if_github_endpoint_is_invalid() throws IOException {
103     assertThatThrownBy(() -> underTest.get("invalidUrl", accessToken, "/endpoint"))
104       .isInstanceOf(IllegalArgumentException.class)
105       .hasMessage("invalidUrl/endpoint is not a valid url");
106   }
107
108   @Test
109   public void getSilent_no_log_if_code_is_not_200() throws IOException {
110     server.enqueue(new MockResponse().setResponseCode(403));
111
112     GetResponse response = underTest.getSilent(appUrl, accessToken, randomEndPoint);
113
114     assertThat(logTester.logs()).isEmpty();
115     assertThat(response.getContent()).isEmpty();
116
117   }
118
119   @Test
120   public void get_log_if_code_is_not_200() throws IOException {
121     server.enqueue(new MockResponse().setResponseCode(403));
122
123     GetResponse response = underTest.get(appUrl, accessToken, randomEndPoint);
124
125     assertThat(logTester.logs(Level.WARN)).isNotEmpty();
126     assertThat(response.getContent()).isEmpty();
127
128   }
129
130   @Test
131   public void get_adds_authentication_header_with_Bearer_type_and_Accept_header() throws IOException, InterruptedException {
132     server.enqueue(new MockResponse());
133
134     GetResponse response = underTest.get(appUrl, accessToken, randomEndPoint);
135
136     assertThat(response).isNotNull();
137     RecordedRequest recordedRequest = server.takeRequest();
138     assertThat(recordedRequest.getMethod()).isEqualTo("GET");
139     assertThat(recordedRequest.getPath()).isEqualTo(randomEndPoint);
140     assertThat(recordedRequest.getHeader("Authorization")).isEqualTo("token " + accessToken.getValue());
141     assertThat(recordedRequest.getHeader(GH_API_VERSION_HEADER)).isEqualTo(GH_API_VERSION);
142   }
143
144   @Test
145   public void get_returns_body_as_response_if_code_is_200() throws IOException {
146     server.enqueue(new MockResponse().setResponseCode(200).setBody(randomBody));
147
148     GetResponse response = underTest.get(appUrl, accessToken, randomEndPoint);
149
150     assertThat(response.getContent()).contains(randomBody);
151   }
152
153   @Test
154   public void get_timeout() {
155     server.enqueue(new MockResponse().setSocketPolicy(SocketPolicy.NO_RESPONSE));
156
157     try {
158       underTest.get(appUrl, accessToken, randomEndPoint);
159       fail("Expected timeout");
160     } catch (Exception e) {
161       assertThat(e).isInstanceOf(SocketTimeoutException.class);
162     }
163   }
164
165   @Test
166   @UseDataProvider("someHttpCodesWithContentBut200")
167   public void get_empty_response_if_code_is_not_200(int code) throws IOException {
168     server.enqueue(new MockResponse().setResponseCode(code).setBody(randomBody));
169
170     GetResponse response = underTest.get(appUrl, accessToken, randomEndPoint);
171
172     assertThat(response.getContent()).contains(randomBody);
173   }
174
175   @Test
176   public void get_returns_empty_endPoint_when_no_link_header() throws IOException {
177     server.enqueue(new MockResponse().setBody(randomBody));
178
179     GetResponse response = underTest.get(appUrl, accessToken, randomEndPoint);
180
181     assertThat(response.getNextEndPoint()).isEmpty();
182   }
183
184   @Test
185   public void get_returns_empty_endPoint_when_link_header_does_not_have_next_rel() throws IOException {
186     server.enqueue(new MockResponse().setBody(randomBody)
187       .setHeader("link", "<https://api.github.com/installation/repositories?per_page=5&page=4>; rel=\"prev\", " +
188                          "<https://api.github.com/installation/repositories?per_page=5&page=1>; rel=\"first\""));
189
190     GetResponse response = underTest.get(appUrl, accessToken, randomEndPoint);
191
192     assertThat(response.getNextEndPoint()).isEmpty();
193   }
194
195   @Test
196   @UseDataProvider("linkHeadersWithNextRel")
197   public void get_returns_endPoint_when_link_header_has_next_rel(String linkHeader) throws IOException {
198     server.enqueue(new MockResponse().setBody(randomBody)
199       .setHeader("link", linkHeader));
200
201     GetResponse response = underTest.get(appUrl, accessToken, randomEndPoint);
202
203     assertThat(response.getNextEndPoint()).contains("https://api.github.com/installation/repositories?per_page=5&page=2");
204   }
205
206   @Test
207   public void get_returns_endPoint_when_link_header_has_next_rel_different_case() throws IOException {
208     String linkHeader = "<https://api.github.com/installation/repositories?per_page=5&page=2>; rel=\"next\"";
209     server.enqueue(new MockResponse().setBody(randomBody)
210       .setHeader("Link", linkHeader));
211
212     GetResponse response = underTest.get(appUrl, accessToken, randomEndPoint);
213
214     assertThat(response.getNextEndPoint()).contains("https://api.github.com/installation/repositories?per_page=5&page=2");
215   }
216
217   @Test
218   public void get_returns_endPoint_when_link_header_is_from_gitlab() throws IOException {
219     String linkHeader = "<https://gitlab.com/api/v4/groups?all_available=false&order_by=name&owned=false&page=2&per_page=2&sort=asc&statistics=false&with_custom_attributes=false>; rel=\"next\", <https://gitlab.com/api/v4/groups?all_available=false&order_by=name&owned=false&page=1&per_page=2&sort=asc&statistics=false&with_custom_attributes=false>; rel=\"first\", <https://gitlab.com/api/v4/groups?all_available=false&order_by=name&owned=false&page=8&per_page=2&sort=asc&statistics=false&with_custom_attributes=false>; rel=\"last\"";
220     server.enqueue(new MockResponse().setBody(randomBody)
221       .setHeader("link", linkHeader));
222
223     GetResponse response = underTest.get(appUrl, accessToken, randomEndPoint);
224
225     assertThat(response.getNextEndPoint()).contains("https://gitlab.com/api/v4/groups?all_available=false"
226                                                     + "&order_by=name&owned=false&page=2&per_page=2&sort=asc&statistics=false&with_custom_attributes=false");
227   }
228
229   @DataProvider
230   public static Object[][] linkHeadersWithNextRel() {
231     String expected = "https://api.github.com/installation/repositories?per_page=5&page=2";
232     return new Object[][] {
233       {"<" + expected + ">; rel=\"next\""},
234       {"<" + expected + ">; rel=\"next\", " +
235        "<https://api.github.com/installation/repositories?per_page=5&page=1>; rel=\"first\""},
236       {"<https://api.github.com/installation/repositories?per_page=5&page=1>; rel=\"first\", " +
237        "<" + expected + ">; rel=\"next\""},
238       {"<https://api.github.com/installation/repositories?per_page=5&page=1>; rel=\"first\", " +
239        "<" + expected + ">; rel=\"next\", " +
240        "<https://api.github.com/installation/repositories?per_page=5&page=5>; rel=\"last\""},
241     };
242   }
243
244   @DataProvider
245   public static Object[][] someHttpCodesWithContentBut200() {
246     return new Object[][] {
247       {201},
248       {202},
249       {203},
250       {404},
251       {500}
252     };
253   }
254
255   @Test
256   public void post_fails_if_endpoint_does_not_start_with_slash() throws IOException {
257     assertThatThrownBy(() -> underTest.post(appUrl, accessToken, "api/foo/bar"))
258       .isInstanceOf(IllegalArgumentException.class)
259       .hasMessage("endpoint must start with '/' or 'http'");
260   }
261
262   @Test
263   public void post_fails_if_endpoint_does_not_start_with_http() throws IOException {
264     assertThatThrownBy(() -> underTest.post(appUrl, accessToken, "ttp://api/foo/bar"))
265       .isInstanceOf(IllegalArgumentException.class)
266       .hasMessage("endpoint must start with '/' or 'http'");
267   }
268
269   @Test
270   public void post_fails_if_github_endpoint_is_invalid() throws IOException {
271     assertThatThrownBy(() -> underTest.post("invalidUrl", accessToken, "/endpoint"))
272       .isInstanceOf(IllegalArgumentException.class)
273       .hasMessage("invalidUrl/endpoint is not a valid url");
274   }
275
276   @Test
277   public void post_adds_authentication_header_with_Bearer_type_and_Accept_header() throws IOException, InterruptedException {
278     server.enqueue(new MockResponse());
279
280     Response response = underTest.post(appUrl, accessToken, randomEndPoint);
281
282     assertThat(response).isNotNull();
283     RecordedRequest recordedRequest = server.takeRequest();
284     assertThat(recordedRequest.getMethod()).isEqualTo("POST");
285     assertThat(recordedRequest.getPath()).isEqualTo(randomEndPoint);
286     assertThat(recordedRequest.getHeader("Authorization")).isEqualTo("token " + accessToken.getValue());
287     assertThat(recordedRequest.getHeader(GH_API_VERSION_HEADER)).isEqualTo(GH_API_VERSION);
288   }
289
290   @Test
291   @DataProvider({"200", "201", "202"})
292   public void post_returns_body_as_response_if_success(int code) throws IOException {
293     server.enqueue(new MockResponse().setResponseCode(code).setBody(randomBody));
294
295     Response response = underTest.post(appUrl, accessToken, randomEndPoint);
296
297     assertThat(response.getContent()).contains(randomBody);
298   }
299
300   @Test
301   public void post_returns_empty_response_if_code_is_204() throws IOException {
302     server.enqueue(new MockResponse().setResponseCode(204));
303
304     Response response = underTest.post(appUrl, accessToken, randomEndPoint);
305
306     assertThat(response.getContent()).isEmpty();
307   }
308
309   @Test
310   @UseDataProvider("httpCodesBut200_201And204")
311   public void post_has_json_error_in_body_if_code_is_neither_200_201_nor_204(int code) throws IOException {
312     server.enqueue(new MockResponse().setResponseCode(code).setBody(randomBody));
313
314     Response response = underTest.post(appUrl, accessToken, randomEndPoint);
315
316     assertThat(response.getContent()).contains(randomBody);
317   }
318
319   @DataProvider
320   public static Object[][] httpCodesBut200_201And204() {
321     return new Object[][] {
322       {202},
323       {203},
324       {400},
325       {401},
326       {403},
327       {404},
328       {500}
329     };
330   }
331
332   @Test
333   public void post_with_json_body_adds_json_to_body_request() throws IOException, InterruptedException {
334     server.enqueue(new MockResponse());
335     String jsonBody = "{\"foo\": \"bar\"}";
336     Response response = underTest.post(appUrl, accessToken, randomEndPoint, jsonBody);
337
338     assertThat(response).isNotNull();
339     RecordedRequest recordedRequest = server.takeRequest();
340     assertThat(recordedRequest.getBody().readUtf8()).isEqualTo(jsonBody);
341   }
342
343   @Test
344   public void patch_with_json_body_adds_json_to_body_request() throws IOException, InterruptedException {
345     server.enqueue(new MockResponse());
346     String jsonBody = "{\"foo\": \"bar\"}";
347
348     Response response = underTest.patch(appUrl, accessToken, randomEndPoint, jsonBody);
349
350     assertThat(response).isNotNull();
351     RecordedRequest recordedRequest = server.takeRequest();
352     assertThat(recordedRequest.getBody().readUtf8()).isEqualTo(jsonBody);
353   }
354
355   @Test
356   public void patch_returns_body_as_response_if_code_is_200() throws IOException {
357     server.enqueue(new MockResponse().setResponseCode(200).setBody(randomBody));
358
359     Response response = underTest.patch(appUrl, accessToken, randomEndPoint, "{}");
360
361     assertThat(response.getContent()).contains(randomBody);
362   }
363
364   @Test
365   public void patch_returns_empty_response_if_code_is_204() throws IOException {
366     server.enqueue(new MockResponse().setResponseCode(204));
367
368     Response response = underTest.patch(appUrl, accessToken, randomEndPoint, "{}");
369
370     assertThat(response.getContent()).isEmpty();
371   }
372
373   @Test
374   public void delete_returns_empty_response_if_code_is_204() throws IOException {
375     server.enqueue(new MockResponse().setResponseCode(204));
376
377     Response response = underTest.delete(appUrl, accessToken, randomEndPoint);
378
379     assertThat(response.getContent()).isEmpty();
380   }
381
382   @DataProvider
383   public static Object[][] httpCodesBut204() {
384     return new Object[][] {
385       {200},
386       {201},
387       {202},
388       {203},
389       {400},
390       {401},
391       {403},
392       {404},
393       {500}
394     };
395   }
396
397   @Test
398   @UseDataProvider("httpCodesBut204")
399   public void delete_returns_response_if_code_is_not_204(int code) throws IOException {
400     server.enqueue(new MockResponse().setResponseCode(code).setBody(randomBody));
401
402     Response response = underTest.delete(appUrl, accessToken, randomEndPoint);
403
404     assertThat(response.getContent()).hasValue(randomBody);
405   }
406
407   @DataProvider
408   public static Object[][] httpCodesBut200And204() {
409     return new Object[][] {
410       {201},
411       {202},
412       {203},
413       {400},
414       {401},
415       {403},
416       {404},
417       {500}
418     };
419   }
420
421   @Test
422   @UseDataProvider("httpCodesBut200And204")
423   public void patch_has_json_error_in_body_if_code_is_neither_200_nor_204(int code) throws IOException {
424     server.enqueue(new MockResponse().setResponseCode(code).setBody(randomBody));
425
426     Response response = underTest.patch(appUrl, accessToken, randomEndPoint, "{}");
427
428     assertThat(response.getContent()).contains(randomBody);
429   }
430
431   @Test
432   public void get_whenRateLimitHeadersArePresent_returnsRateLimit() throws Exception {
433     testRateLimitHeader(() -> underTest.get(appUrl, accessToken, randomEndPoint), false);
434   }
435
436   @Test
437   public void get_whenRateLimitHeadersArePresentAndUppercased_returnsRateLimit() throws Exception {
438     testRateLimitHeader(() -> underTest.get(appUrl, accessToken, randomEndPoint), true);
439   }
440
441   private void testRateLimitHeader(Callable<Response> request, boolean uppercasedHeaders) throws Exception {
442     server.enqueue(new MockResponse().setBody(randomBody)
443       .setHeader(uppercasedHeaders ? "x-ratelimit-remaining" : "x-ratelimit-REMAINING", "1")
444       .setHeader(uppercasedHeaders ? "x-ratelimit-limit" : "X-RATELIMIT-LIMIT", "10")
445       .setHeader(uppercasedHeaders ? "x-ratelimit-reset" : "X-ratelimit-reset", "1000"));
446
447     Response response = request.call();
448
449     assertThat(response.getRateLimit())
450       .isEqualTo(new RateLimit(1, 10, 1000L));
451   }
452
453   @Test
454   public void get_whenRateLimitHeadersAreMissing_returnsNull() throws Exception {
455
456     testMissingRateLimitHeader(() -> underTest.get(appUrl, accessToken, randomEndPoint));
457
458   }
459
460   private void testMissingRateLimitHeader(Callable<Response> request) throws Exception {
461     server.enqueue(new MockResponse().setBody(randomBody));
462
463     Response response = request.call();
464     assertThat(response.getRateLimit())
465       .isNull();
466   }
467
468   @Test
469   public void delete_whenRateLimitHeadersArePresent_returnsRateLimit() throws Exception {
470     testRateLimitHeader(() -> underTest.delete(appUrl, accessToken, randomEndPoint), false);
471
472   }
473
474   @Test
475   public void delete_whenRateLimitHeadersAreMissing_returnsNull() throws Exception {
476     testMissingRateLimitHeader(() -> underTest.delete(appUrl, accessToken, randomEndPoint));
477
478   }
479
480   @Test
481   public void patch_whenRateLimitHeadersArePresent_returnsRateLimit() throws Exception {
482     testRateLimitHeader(() -> underTest.patch(appUrl, accessToken, randomEndPoint, "body"), false);
483   }
484
485   @Test
486   public void patch_whenRateLimitHeadersAreMissing_returnsNull() throws Exception {
487     testMissingRateLimitHeader(() -> underTest.patch(appUrl, accessToken, randomEndPoint, "body"));
488   }
489
490   @Test
491   public void post_whenRateLimitHeadersArePresent_returnsRateLimit() throws Exception {
492     testRateLimitHeader(() -> underTest.post(appUrl, accessToken, randomEndPoint), false);
493   }
494
495   @Test
496   public void post_whenRateLimitHeadersAreMissing_returnsNull() throws Exception {
497     testMissingRateLimitHeader(() -> underTest.post(appUrl, accessToken, randomEndPoint));
498   }
499 }