3 * Copyright (C) 2009-2022 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 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;
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;
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";
56 public MockWebServer server = new MockWebServer();
59 public static LogTester logTester = new LogTester().setLevel(LoggerLevel.WARN);
61 private GithubApplicationHttpClientImpl underTest;
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;
70 this.appUrl = format("http://%s:%s", server.getHostName(), server.getPort());
71 this.underTest = new GithubApplicationHttpClientImpl(new ConstantTimeoutConfiguration(500));
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);
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'");
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");
97 public void getSilent_no_log_if_code_is_not_200() throws IOException {
98 server.enqueue(new MockResponse().setResponseCode(403));
100 GetResponse response = underTest.getSilent(appUrl, accessToken, randomEndPoint);
102 assertThat(logTester.logs()).isEmpty();
103 assertThat(response.getContent()).isEmpty();
108 public void get_log_if_code_is_not_200() throws IOException {
109 server.enqueue(new MockResponse().setResponseCode(403));
111 GetResponse response = underTest.get(appUrl, accessToken, randomEndPoint);
113 assertThat(logTester.logs(LoggerLevel.WARN)).isNotEmpty();
114 assertThat(response.getContent()).isEmpty();
119 public void get_adds_authentication_header_with_Bearer_type_and_Accept_header() throws IOException, InterruptedException {
120 server.enqueue(new MockResponse());
122 GetResponse response = underTest.get(appUrl, accessToken, randomEndPoint);
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);
133 public void get_returns_body_as_response_if_code_is_200() throws IOException {
134 server.enqueue(new MockResponse().setResponseCode(200).setBody(randomBody));
136 GetResponse response = underTest.get(appUrl, accessToken, randomEndPoint);
138 assertThat(response.getContent()).contains(randomBody);
142 public void get_timeout() {
143 server.enqueue(new MockResponse().setSocketPolicy(SocketPolicy.NO_RESPONSE));
146 underTest.get(appUrl, accessToken, randomEndPoint);
147 fail("Expected timeout");
148 } catch (Exception e) {
149 assertThat(e).isInstanceOf(SocketTimeoutException.class);
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));
158 GetResponse response = underTest.get(appUrl, accessToken, randomEndPoint);
160 assertThat(response.getContent()).isEmpty();
164 public void get_returns_empty_endPoint_when_no_link_header() throws IOException {
165 server.enqueue(new MockResponse().setBody(randomBody));
167 GetResponse response = underTest.get(appUrl, accessToken, randomEndPoint);
169 assertThat(response.getNextEndPoint()).isEmpty();
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\""));
178 GetResponse response = underTest.get(appUrl, accessToken, randomEndPoint);
180 assertThat(response.getNextEndPoint()).isEmpty();
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));
189 GetResponse response = underTest.get(appUrl, accessToken, randomEndPoint);
191 assertThat(response.getNextEndPoint()).contains("https://api.github.com/installation/repositories?per_page=5&page=2");
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));
200 GetResponse response = underTest.get(appUrl, accessToken, randomEndPoint);
202 assertThat(response.getNextEndPoint()).contains("https://api.github.com/installation/repositories?per_page=5&page=2");
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\""},
221 public static Object[][] someHttpCodesWithContentBut200() {
222 return new Object[][] {
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'");
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'");
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");
253 public void post_adds_authentication_header_with_Bearer_type_and_Accept_header() throws IOException, InterruptedException {
254 server.enqueue(new MockResponse());
256 Response response = underTest.post(appUrl, accessToken, randomEndPoint);
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);
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));
271 Response response = underTest.post(appUrl, accessToken, randomEndPoint);
273 assertThat(response.getContent()).contains(randomBody);
277 public void post_returns_empty_response_if_code_is_204() throws IOException {
278 server.enqueue(new MockResponse().setResponseCode(204));
280 Response response = underTest.post(appUrl, accessToken, randomEndPoint);
282 assertThat(response.getContent()).isEmpty();
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));
290 Response response = underTest.post(appUrl, accessToken, randomEndPoint);
292 assertThat(response.getContent()).contains(randomBody);
296 public static Object[][] httpCodesBut200_201And204() {
297 return new Object[][] {
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);
314 assertThat(response).isNotNull();
315 RecordedRequest recordedRequest = server.takeRequest();
316 assertThat(recordedRequest.getBody().readUtf8()).isEqualTo(jsonBody);
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\"}";
324 Response response = underTest.patch(appUrl, accessToken, randomEndPoint, jsonBody);
326 assertThat(response).isNotNull();
327 RecordedRequest recordedRequest = server.takeRequest();
328 assertThat(recordedRequest.getBody().readUtf8()).isEqualTo(jsonBody);
332 public void patch_returns_body_as_response_if_code_is_200() throws IOException {
333 server.enqueue(new MockResponse().setResponseCode(200).setBody(randomBody));
335 Response response = underTest.patch(appUrl, accessToken, randomEndPoint, "{}");
337 assertThat(response.getContent()).contains(randomBody);
341 public void patch_returns_empty_response_if_code_is_204() throws IOException {
342 server.enqueue(new MockResponse().setResponseCode(204));
344 Response response = underTest.patch(appUrl, accessToken, randomEndPoint, "{}");
346 assertThat(response.getContent()).isEmpty();
350 public void delete_returns_empty_response_if_code_is_204() throws IOException {
351 server.enqueue(new MockResponse().setResponseCode(204));
353 Response response = underTest.delete(appUrl, accessToken, randomEndPoint);
355 assertThat(response.getContent()).isEmpty();
359 public static Object[][] httpCodesBut204() {
360 return new Object[][] {
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));
378 Response response = underTest.delete(appUrl, accessToken, randomEndPoint);
380 assertThat(response.getContent()).hasValue(randomBody);
384 public static Object[][] httpCodesBut200And204() {
385 return new Object[][] {
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));
402 Response response = underTest.patch(appUrl, accessToken, randomEndPoint, "{}");
404 assertThat(response.getContent()).contains(randomBody);