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.Rule;
33 import org.junit.Test;
34 import org.junit.runner.RunWith;
35 import org.sonar.alm.client.ConstantTimeoutConfiguration;
36 import org.sonar.alm.client.github.GithubApplicationHttpClient.GetResponse;
37 import org.sonar.alm.client.github.GithubApplicationHttpClient.Response;
38 import org.sonar.alm.client.github.security.AccessToken;
39 import org.sonar.alm.client.github.security.UserAccessToken;
41 import static java.lang.String.format;
42 import static org.apache.commons.lang.RandomStringUtils.randomAlphabetic;
43 import static org.assertj.core.api.Assertions.assertThat;
44 import static org.assertj.core.api.Assertions.assertThatThrownBy;
45 import static org.junit.Assert.fail;
47 @RunWith(DataProviderRunner.class)
48 public class GithubApplicationHttpClientImplTest {
49 private static final String BETA_API_HEADER = "application/vnd.github.antiope-preview+json, " +
50 "application/vnd.github.machine-man-preview+json, " +
51 "application/vnd.github.v3+json";
53 public MockWebServer server = new MockWebServer();
55 private GithubApplicationHttpClientImpl underTest;
57 private final AccessToken accessToken = new UserAccessToken(randomAlphabetic(10));
58 private final String randomEndPoint = "/" + randomAlphabetic(10);
59 private final String randomBody = randomAlphabetic(40);
60 private String appUrl;
64 this.appUrl = format("http://%s:%s", server.getHostName(), server.getPort());
65 this.underTest = new GithubApplicationHttpClientImpl(new ConstantTimeoutConfiguration(500));
69 public void get_fails_if_endpoint_does_not_start_with_slash() throws IOException {
70 assertThatThrownBy(() -> underTest.get(appUrl, accessToken, "api/foo/bar"))
71 .hasMessage("endpoint must start with '/' or 'http'")
72 .isInstanceOf(IllegalArgumentException.class);
76 public void get_fails_if_endpoint_does_not_start_with_http() throws IOException {
77 assertThatThrownBy(() -> underTest.get(appUrl, accessToken, "ttp://api/foo/bar"))
78 .isInstanceOf(IllegalArgumentException.class)
79 .hasMessage("endpoint must start with '/' or 'http'");
83 public void get_fails_if_github_endpoint_is_invalid() throws IOException {
84 assertThatThrownBy(() -> underTest.get("invalidUrl", accessToken, "/endpoint"))
85 .isInstanceOf(IllegalArgumentException.class)
86 .hasMessage("invalidUrl/endpoint is not a valid url");
90 public void get_adds_authentication_header_with_Bearer_type_and_Accept_header() throws IOException, InterruptedException {
91 server.enqueue(new MockResponse());
93 GetResponse response = underTest.get(appUrl, accessToken, randomEndPoint);
95 assertThat(response).isNotNull();
96 RecordedRequest recordedRequest = server.takeRequest();
97 assertThat(recordedRequest.getMethod()).isEqualTo("GET");
98 assertThat(recordedRequest.getPath()).isEqualTo(randomEndPoint);
99 assertThat(recordedRequest.getHeader("Authorization")).isEqualTo("token " + accessToken.getValue());
100 assertThat(recordedRequest.getHeader("Accept")).isEqualTo(BETA_API_HEADER);
104 public void get_returns_body_as_response_if_code_is_200() throws IOException {
105 server.enqueue(new MockResponse().setResponseCode(200).setBody(randomBody));
107 GetResponse response = underTest.get(appUrl, accessToken, randomEndPoint);
109 assertThat(response.getContent()).contains(randomBody);
113 public void get_timeout() {
114 server.enqueue(new MockResponse().setSocketPolicy(SocketPolicy.NO_RESPONSE));
117 underTest.get(appUrl, accessToken, randomEndPoint);
118 fail("Expected timeout");
119 } catch (Exception e) {
120 assertThat(e).isInstanceOf(SocketTimeoutException.class);
125 @UseDataProvider("someHttpCodesWithContentBut200")
126 public void get_empty_response_if_code_is_not_200(int code) throws IOException {
127 server.enqueue(new MockResponse().setResponseCode(code).setBody(randomBody));
129 GetResponse response = underTest.get(appUrl, accessToken, randomEndPoint);
131 assertThat(response.getContent()).isEmpty();
135 public void get_returns_empty_endPoint_when_no_link_header() throws IOException {
136 server.enqueue(new MockResponse().setBody(randomBody));
138 GetResponse response = underTest.get(appUrl, accessToken, randomEndPoint);
140 assertThat(response.getNextEndPoint()).isEmpty();
144 public void get_returns_empty_endPoint_when_link_header_does_not_have_next_rel() throws IOException {
145 server.enqueue(new MockResponse().setBody(randomBody)
146 .setHeader("link", "<https://api.github.com/installation/repositories?per_page=5&page=4>; rel=\"prev\", " +
147 "<https://api.github.com/installation/repositories?per_page=5&page=1>; rel=\"first\""));
149 GetResponse response = underTest.get(appUrl, accessToken, randomEndPoint);
151 assertThat(response.getNextEndPoint()).isEmpty();
155 @UseDataProvider("linkHeadersWithNextRel")
156 public void get_returns_endPoint_when_link_header_has_next_rel(String linkHeader) throws IOException {
157 server.enqueue(new MockResponse().setBody(randomBody)
158 .setHeader("link", linkHeader));
160 GetResponse response = underTest.get(appUrl, accessToken, randomEndPoint);
162 assertThat(response.getNextEndPoint()).contains("https://api.github.com/installation/repositories?per_page=5&page=2");
166 public void get_returns_endPoint_when_link_header_has_next_rel_different_case() throws IOException {
167 String linkHeader = "<https://api.github.com/installation/repositories?per_page=5&page=2>; rel=\"next\"";
168 server.enqueue(new MockResponse().setBody(randomBody)
169 .setHeader("Link", linkHeader));
171 GetResponse response = underTest.get(appUrl, accessToken, randomEndPoint);
173 assertThat(response.getNextEndPoint()).contains("https://api.github.com/installation/repositories?per_page=5&page=2");
177 public static Object[][] linkHeadersWithNextRel() {
178 String expected = "https://api.github.com/installation/repositories?per_page=5&page=2";
179 return new Object[][] {
180 {"<" + expected + ">; rel=\"next\""},
181 {"<" + expected + ">; rel=\"next\", " +
182 "<https://api.github.com/installation/repositories?per_page=5&page=1>; rel=\"first\""},
183 {"<https://api.github.com/installation/repositories?per_page=5&page=1>; rel=\"first\", " +
184 "<" + expected + ">; rel=\"next\""},
185 {"<https://api.github.com/installation/repositories?per_page=5&page=1>; rel=\"first\", " +
186 "<" + expected + ">; rel=\"next\", " +
187 "<https://api.github.com/installation/repositories?per_page=5&page=5>; rel=\"last\""},
192 public static Object[][] someHttpCodesWithContentBut200() {
193 return new Object[][] {
203 public void post_fails_if_endpoint_does_not_start_with_slash() throws IOException {
204 assertThatThrownBy(() -> underTest.post(appUrl, accessToken, "api/foo/bar"))
205 .isInstanceOf(IllegalArgumentException.class)
206 .hasMessage("endpoint must start with '/' or 'http'");
210 public void post_fails_if_endpoint_does_not_start_with_http() throws IOException {
211 assertThatThrownBy(() -> underTest.post(appUrl, accessToken, "ttp://api/foo/bar"))
212 .isInstanceOf(IllegalArgumentException.class)
213 .hasMessage("endpoint must start with '/' or 'http'");
217 public void post_fails_if_github_endpoint_is_invalid() throws IOException {
218 assertThatThrownBy(() -> underTest.post("invalidUrl", accessToken, "/endpoint"))
219 .isInstanceOf(IllegalArgumentException.class)
220 .hasMessage("invalidUrl/endpoint is not a valid url");
224 public void post_adds_authentication_header_with_Bearer_type_and_Accept_header() throws IOException, InterruptedException {
225 server.enqueue(new MockResponse());
227 Response response = underTest.post(appUrl, accessToken, randomEndPoint);
229 assertThat(response).isNotNull();
230 RecordedRequest recordedRequest = server.takeRequest();
231 assertThat(recordedRequest.getMethod()).isEqualTo("POST");
232 assertThat(recordedRequest.getPath()).isEqualTo(randomEndPoint);
233 assertThat(recordedRequest.getHeader("Authorization")).isEqualTo("token " + accessToken.getValue());
234 assertThat(recordedRequest.getHeader("Accept")).isEqualTo(BETA_API_HEADER);
238 @DataProvider({"200", "201", "202"})
239 public void post_returns_body_as_response_if_success(int code) throws IOException {
240 server.enqueue(new MockResponse().setResponseCode(code).setBody(randomBody));
242 Response response = underTest.post(appUrl, accessToken, randomEndPoint);
244 assertThat(response.getContent()).contains(randomBody);
248 public void post_returns_empty_response_if_code_is_204() throws IOException {
249 server.enqueue(new MockResponse().setResponseCode(204));
251 Response response = underTest.post(appUrl, accessToken, randomEndPoint);
253 assertThat(response.getContent()).isEmpty();
257 @UseDataProvider("httpCodesBut200_201And204")
258 public void post_has_json_error_in_body_if_code_is_neither_200_201_nor_204(int code) throws IOException {
259 server.enqueue(new MockResponse().setResponseCode(code).setBody(randomBody));
261 Response response = underTest.post(appUrl, accessToken, randomEndPoint);
263 assertThat(response.getContent()).contains(randomBody);
267 public static Object[][] httpCodesBut200_201And204() {
268 return new Object[][] {
280 public void post_with_json_body_adds_json_to_body_request() throws IOException, InterruptedException {
281 server.enqueue(new MockResponse());
282 String jsonBody = "{\"foo\": \"bar\"}";
283 Response response = underTest.post(appUrl, accessToken, randomEndPoint, jsonBody);
285 assertThat(response).isNotNull();
286 RecordedRequest recordedRequest = server.takeRequest();
287 assertThat(recordedRequest.getBody().readUtf8()).isEqualTo(jsonBody);
291 public void patch_with_json_body_adds_json_to_body_request() throws IOException, InterruptedException {
292 server.enqueue(new MockResponse());
293 String jsonBody = "{\"foo\": \"bar\"}";
295 Response response = underTest.patch(appUrl, accessToken, randomEndPoint, jsonBody);
297 assertThat(response).isNotNull();
298 RecordedRequest recordedRequest = server.takeRequest();
299 assertThat(recordedRequest.getBody().readUtf8()).isEqualTo(jsonBody);
303 public void patch_returns_body_as_response_if_code_is_200() throws IOException {
304 server.enqueue(new MockResponse().setResponseCode(200).setBody(randomBody));
306 Response response = underTest.patch(appUrl, accessToken, randomEndPoint, "{}");
308 assertThat(response.getContent()).contains(randomBody);
312 public void patch_returns_empty_response_if_code_is_204() throws IOException {
313 server.enqueue(new MockResponse().setResponseCode(204));
315 Response response = underTest.patch(appUrl, accessToken, randomEndPoint, "{}");
317 assertThat(response.getContent()).isEmpty();
321 public void delete_returns_empty_response_if_code_is_204() throws IOException {
322 server.enqueue(new MockResponse().setResponseCode(204));
324 Response response = underTest.delete(appUrl, accessToken, randomEndPoint);
326 assertThat(response.getContent()).isEmpty();
330 public static Object[][] httpCodesBut204() {
331 return new Object[][] {
345 @UseDataProvider("httpCodesBut204")
346 public void delete_returns_response_if_code_is_not_204(int code) throws IOException {
347 server.enqueue(new MockResponse().setResponseCode(code).setBody(randomBody));
349 Response response = underTest.delete(appUrl, accessToken, randomEndPoint);
351 assertThat(response.getContent()).hasValue(randomBody);
355 public static Object[][] httpCodesBut200And204() {
356 return new Object[][] {
369 @UseDataProvider("httpCodesBut200And204")
370 public void patch_has_json_error_in_body_if_code_is_neither_200_nor_204(int code) throws IOException {
371 server.enqueue(new MockResponse().setResponseCode(code).setBody(randomBody));
373 Response response = underTest.patch(appUrl, accessToken, randomEndPoint, "{}");
375 assertThat(response.getContent()).contains(randomBody);