]> source.dussan.org Git - sonarqube.git/blob
5b41d67b2e6eb1a5033135680a96560ed2c63a47
[sonarqube.git] /
1 /*
2  * SonarQube
3  * Copyright (C) 2009-2022 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 okhttp3.mockwebserver.MockResponse;
28 import okhttp3.mockwebserver.MockWebServer;
29 import okhttp3.mockwebserver.RecordedRequest;
30 import okhttp3.mockwebserver.SocketPolicy;
31 import org.junit.Before;
32 import org.junit.ClassRule;
33 import org.junit.Rule;
34 import org.junit.Test;
35 import org.junit.runner.RunWith;
36 import org.sonar.alm.client.ConstantTimeoutConfiguration;
37 import org.sonar.alm.client.github.GithubApplicationHttpClient.GetResponse;
38 import org.sonar.alm.client.github.GithubApplicationHttpClient.Response;
39 import org.sonar.alm.client.github.security.AccessToken;
40 import org.sonar.alm.client.github.security.UserAccessToken;
41 import org.sonar.api.utils.log.LogTester;
42 import org.sonar.api.utils.log.LoggerLevel;
43
44 import static java.lang.String.format;
45 import static org.apache.commons.lang.RandomStringUtils.randomAlphabetic;
46 import static org.assertj.core.api.Assertions.assertThat;
47 import static org.assertj.core.api.Assertions.assertThatThrownBy;
48 import static org.junit.Assert.fail;
49
50 @RunWith(DataProviderRunner.class)
51 public class GithubApplicationHttpClientImplTest {
52   private static final String BETA_API_HEADER = "application/vnd.github.antiope-preview+json, " +
53     "application/vnd.github.machine-man-preview+json, " +
54     "application/vnd.github.v3+json";
55   @Rule
56   public MockWebServer server = new MockWebServer();
57
58   @ClassRule
59   public static LogTester logTester = new LogTester().setLevel(LoggerLevel.WARN);
60
61   private GithubApplicationHttpClientImpl underTest;
62
63   private final AccessToken accessToken = new UserAccessToken(randomAlphabetic(10));
64   private final String randomEndPoint = "/" + randomAlphabetic(10);
65   private final String randomBody = randomAlphabetic(40);
66   private String appUrl;
67
68   @Before
69   public void setUp() {
70     this.appUrl = format("http://%s:%s", server.getHostName(), server.getPort());
71     this.underTest = new GithubApplicationHttpClientImpl(new ConstantTimeoutConfiguration(500));
72     logTester.clear();
73   }
74
75   @Test
76   public void get_fails_if_endpoint_does_not_start_with_slash() throws IOException {
77     assertThatThrownBy(() -> underTest.get(appUrl, accessToken, "api/foo/bar"))
78       .hasMessage("endpoint must start with '/' or 'http'")
79       .isInstanceOf(IllegalArgumentException.class);
80   }
81
82   @Test
83   public void get_fails_if_endpoint_does_not_start_with_http() throws IOException {
84     assertThatThrownBy(() -> underTest.get(appUrl, accessToken, "ttp://api/foo/bar"))
85       .isInstanceOf(IllegalArgumentException.class)
86       .hasMessage("endpoint must start with '/' or 'http'");
87   }
88
89   @Test
90   public void get_fails_if_github_endpoint_is_invalid() throws IOException {
91     assertThatThrownBy(() -> underTest.get("invalidUrl", accessToken, "/endpoint"))
92       .isInstanceOf(IllegalArgumentException.class)
93       .hasMessage("invalidUrl/endpoint is not a valid url");
94   }
95
96   @Test
97   public void getSilent_no_log_if_code_is_not_200() throws IOException {
98     server.enqueue(new MockResponse().setResponseCode(403));
99
100     GetResponse response = underTest.getSilent(appUrl, accessToken, randomEndPoint);
101
102     assertThat(logTester.logs()).isEmpty();
103     assertThat(response.getContent()).isEmpty();
104
105   }
106
107   @Test
108   public void get_log_if_code_is_not_200() throws IOException {
109     server.enqueue(new MockResponse().setResponseCode(403));
110
111     GetResponse response = underTest.get(appUrl, accessToken, randomEndPoint);
112
113     assertThat(logTester.logs(LoggerLevel.WARN)).isNotEmpty();
114     assertThat(response.getContent()).isEmpty();
115
116   }
117
118   @Test
119   public void get_adds_authentication_header_with_Bearer_type_and_Accept_header() throws IOException, InterruptedException {
120     server.enqueue(new MockResponse());
121
122     GetResponse response = underTest.get(appUrl, accessToken, randomEndPoint);
123
124     assertThat(response).isNotNull();
125     RecordedRequest recordedRequest = server.takeRequest();
126     assertThat(recordedRequest.getMethod()).isEqualTo("GET");
127     assertThat(recordedRequest.getPath()).isEqualTo(randomEndPoint);
128     assertThat(recordedRequest.getHeader("Authorization")).isEqualTo("token " + accessToken.getValue());
129     assertThat(recordedRequest.getHeader("Accept")).isEqualTo(BETA_API_HEADER);
130   }
131
132   @Test
133   public void get_returns_body_as_response_if_code_is_200() throws IOException {
134     server.enqueue(new MockResponse().setResponseCode(200).setBody(randomBody));
135
136     GetResponse response = underTest.get(appUrl, accessToken, randomEndPoint);
137
138     assertThat(response.getContent()).contains(randomBody);
139   }
140
141   @Test
142   public void get_timeout() {
143     server.enqueue(new MockResponse().setSocketPolicy(SocketPolicy.NO_RESPONSE));
144
145     try {
146       underTest.get(appUrl, accessToken, randomEndPoint);
147       fail("Expected timeout");
148     } catch (Exception e) {
149       assertThat(e).isInstanceOf(SocketTimeoutException.class);
150     }
151   }
152
153   @Test
154   @UseDataProvider("someHttpCodesWithContentBut200")
155   public void get_empty_response_if_code_is_not_200(int code) throws IOException {
156     server.enqueue(new MockResponse().setResponseCode(code).setBody(randomBody));
157
158     GetResponse response = underTest.get(appUrl, accessToken, randomEndPoint);
159
160     assertThat(response.getContent()).contains(randomBody);
161   }
162
163   @Test
164   public void get_returns_empty_endPoint_when_no_link_header() throws IOException {
165     server.enqueue(new MockResponse().setBody(randomBody));
166
167     GetResponse response = underTest.get(appUrl, accessToken, randomEndPoint);
168
169     assertThat(response.getNextEndPoint()).isEmpty();
170   }
171
172   @Test
173   public void get_returns_empty_endPoint_when_link_header_does_not_have_next_rel() throws IOException {
174     server.enqueue(new MockResponse().setBody(randomBody)
175       .setHeader("link", "<https://api.github.com/installation/repositories?per_page=5&page=4>; rel=\"prev\", " +
176         "<https://api.github.com/installation/repositories?per_page=5&page=1>; rel=\"first\""));
177
178     GetResponse response = underTest.get(appUrl, accessToken, randomEndPoint);
179
180     assertThat(response.getNextEndPoint()).isEmpty();
181   }
182
183   @Test
184   @UseDataProvider("linkHeadersWithNextRel")
185   public void get_returns_endPoint_when_link_header_has_next_rel(String linkHeader) throws IOException {
186     server.enqueue(new MockResponse().setBody(randomBody)
187       .setHeader("link", linkHeader));
188
189     GetResponse response = underTest.get(appUrl, accessToken, randomEndPoint);
190
191     assertThat(response.getNextEndPoint()).contains("https://api.github.com/installation/repositories?per_page=5&page=2");
192   }
193
194   @Test
195   public void get_returns_endPoint_when_link_header_has_next_rel_different_case() throws IOException {
196     String linkHeader = "<https://api.github.com/installation/repositories?per_page=5&page=2>; rel=\"next\"";
197     server.enqueue(new MockResponse().setBody(randomBody)
198       .setHeader("Link", linkHeader));
199
200     GetResponse response = underTest.get(appUrl, accessToken, randomEndPoint);
201
202     assertThat(response.getNextEndPoint()).contains("https://api.github.com/installation/repositories?per_page=5&page=2");
203   }
204
205   @DataProvider
206   public static Object[][] linkHeadersWithNextRel() {
207     String expected = "https://api.github.com/installation/repositories?per_page=5&page=2";
208     return new Object[][] {
209       {"<" + expected + ">; rel=\"next\""},
210       {"<" + expected + ">; rel=\"next\", " +
211         "<https://api.github.com/installation/repositories?per_page=5&page=1>; rel=\"first\""},
212       {"<https://api.github.com/installation/repositories?per_page=5&page=1>; rel=\"first\", " +
213         "<" + expected + ">; rel=\"next\""},
214       {"<https://api.github.com/installation/repositories?per_page=5&page=1>; rel=\"first\", " +
215         "<" + expected + ">; rel=\"next\", " +
216         "<https://api.github.com/installation/repositories?per_page=5&page=5>; rel=\"last\""},
217     };
218   }
219
220   @DataProvider
221   public static Object[][] someHttpCodesWithContentBut200() {
222     return new Object[][] {
223       {201},
224       {202},
225       {203},
226       {404},
227       {500}
228     };
229   }
230
231   @Test
232   public void post_fails_if_endpoint_does_not_start_with_slash() throws IOException {
233     assertThatThrownBy(() -> underTest.post(appUrl, accessToken, "api/foo/bar"))
234       .isInstanceOf(IllegalArgumentException.class)
235       .hasMessage("endpoint must start with '/' or 'http'");
236   }
237
238   @Test
239   public void post_fails_if_endpoint_does_not_start_with_http() throws IOException {
240     assertThatThrownBy(() -> underTest.post(appUrl, accessToken, "ttp://api/foo/bar"))
241       .isInstanceOf(IllegalArgumentException.class)
242       .hasMessage("endpoint must start with '/' or 'http'");
243   }
244
245   @Test
246   public void post_fails_if_github_endpoint_is_invalid() throws IOException {
247     assertThatThrownBy(() -> underTest.post("invalidUrl", accessToken, "/endpoint"))
248       .isInstanceOf(IllegalArgumentException.class)
249       .hasMessage("invalidUrl/endpoint is not a valid url");
250   }
251
252   @Test
253   public void post_adds_authentication_header_with_Bearer_type_and_Accept_header() throws IOException, InterruptedException {
254     server.enqueue(new MockResponse());
255
256     Response response = underTest.post(appUrl, accessToken, randomEndPoint);
257
258     assertThat(response).isNotNull();
259     RecordedRequest recordedRequest = server.takeRequest();
260     assertThat(recordedRequest.getMethod()).isEqualTo("POST");
261     assertThat(recordedRequest.getPath()).isEqualTo(randomEndPoint);
262     assertThat(recordedRequest.getHeader("Authorization")).isEqualTo("token " + accessToken.getValue());
263     assertThat(recordedRequest.getHeader("Accept")).isEqualTo(BETA_API_HEADER);
264   }
265
266   @Test
267   @DataProvider({"200", "201", "202"})
268   public void post_returns_body_as_response_if_success(int code) throws IOException {
269     server.enqueue(new MockResponse().setResponseCode(code).setBody(randomBody));
270
271     Response response = underTest.post(appUrl, accessToken, randomEndPoint);
272
273     assertThat(response.getContent()).contains(randomBody);
274   }
275
276   @Test
277   public void post_returns_empty_response_if_code_is_204() throws IOException {
278     server.enqueue(new MockResponse().setResponseCode(204));
279
280     Response response = underTest.post(appUrl, accessToken, randomEndPoint);
281
282     assertThat(response.getContent()).isEmpty();
283   }
284
285   @Test
286   @UseDataProvider("httpCodesBut200_201And204")
287   public void post_has_json_error_in_body_if_code_is_neither_200_201_nor_204(int code) throws IOException {
288     server.enqueue(new MockResponse().setResponseCode(code).setBody(randomBody));
289
290     Response response = underTest.post(appUrl, accessToken, randomEndPoint);
291
292     assertThat(response.getContent()).contains(randomBody);
293   }
294
295   @DataProvider
296   public static Object[][] httpCodesBut200_201And204() {
297     return new Object[][] {
298       {202},
299       {203},
300       {400},
301       {401},
302       {403},
303       {404},
304       {500}
305     };
306   }
307
308   @Test
309   public void post_with_json_body_adds_json_to_body_request() throws IOException, InterruptedException {
310     server.enqueue(new MockResponse());
311     String jsonBody = "{\"foo\": \"bar\"}";
312     Response response = underTest.post(appUrl, accessToken, randomEndPoint, jsonBody);
313
314     assertThat(response).isNotNull();
315     RecordedRequest recordedRequest = server.takeRequest();
316     assertThat(recordedRequest.getBody().readUtf8()).isEqualTo(jsonBody);
317   }
318
319   @Test
320   public void patch_with_json_body_adds_json_to_body_request() throws IOException, InterruptedException {
321     server.enqueue(new MockResponse());
322     String jsonBody = "{\"foo\": \"bar\"}";
323
324     Response response = underTest.patch(appUrl, accessToken, randomEndPoint, jsonBody);
325
326     assertThat(response).isNotNull();
327     RecordedRequest recordedRequest = server.takeRequest();
328     assertThat(recordedRequest.getBody().readUtf8()).isEqualTo(jsonBody);
329   }
330
331   @Test
332   public void patch_returns_body_as_response_if_code_is_200() throws IOException {
333     server.enqueue(new MockResponse().setResponseCode(200).setBody(randomBody));
334
335     Response response = underTest.patch(appUrl, accessToken, randomEndPoint, "{}");
336
337     assertThat(response.getContent()).contains(randomBody);
338   }
339
340   @Test
341   public void patch_returns_empty_response_if_code_is_204() throws IOException {
342     server.enqueue(new MockResponse().setResponseCode(204));
343
344     Response response = underTest.patch(appUrl, accessToken, randomEndPoint, "{}");
345
346     assertThat(response.getContent()).isEmpty();
347   }
348
349   @Test
350   public void delete_returns_empty_response_if_code_is_204() throws IOException {
351     server.enqueue(new MockResponse().setResponseCode(204));
352
353     Response response = underTest.delete(appUrl, accessToken, randomEndPoint);
354
355     assertThat(response.getContent()).isEmpty();
356   }
357
358   @DataProvider
359   public static Object[][] httpCodesBut204() {
360     return new Object[][] {
361       {200},
362       {201},
363       {202},
364       {203},
365       {400},
366       {401},
367       {403},
368       {404},
369       {500}
370     };
371   }
372
373   @Test
374   @UseDataProvider("httpCodesBut204")
375   public void delete_returns_response_if_code_is_not_204(int code) throws IOException {
376     server.enqueue(new MockResponse().setResponseCode(code).setBody(randomBody));
377
378     Response response = underTest.delete(appUrl, accessToken, randomEndPoint);
379
380     assertThat(response.getContent()).hasValue(randomBody);
381   }
382
383   @DataProvider
384   public static Object[][] httpCodesBut200And204() {
385     return new Object[][] {
386       {201},
387       {202},
388       {203},
389       {400},
390       {401},
391       {403},
392       {404},
393       {500}
394     };
395   }
396
397   @Test
398   @UseDataProvider("httpCodesBut200And204")
399   public void patch_has_json_error_in_body_if_code_is_neither_200_nor_204(int code) throws IOException {
400     server.enqueue(new MockResponse().setResponseCode(code).setBody(randomBody));
401
402     Response response = underTest.patch(appUrl, accessToken, randomEndPoint, "{}");
403
404     assertThat(response.getContent()).contains(randomBody);
405   }
406
407 }