Skip to content

Commit f736c36

Browse files
committed
test: Add more tests and doc comments
1 parent 1924023 commit f736c36

File tree

4 files changed

+240
-3
lines changed

4 files changed

+240
-3
lines changed

src/main/java/com/spotify/github/http/BaseHttpResponse.java

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
import java.util.List;
2424
import java.util.Map;
2525

26+
/** BaseHttpResponse is the base implementation of HttpResponse. */
2627
public abstract class BaseHttpResponse implements HttpResponse {
2728
private static final int HTTP_OK = 200;
2829
private static final int HTTP_BAD_REQUEST = 400;
@@ -43,30 +44,66 @@ public BaseHttpResponse(
4344
this.headers = headers;
4445
}
4546

47+
/**
48+
* Returns the request that generated this response.
49+
*
50+
* @return HttpRequest the request that generated this response
51+
*/
4652
@Override
4753
public HttpRequest request() {
4854
return request;
4955
}
5056

57+
/**
58+
* Returns the HTTP status code of the response.
59+
*
60+
* @return the status code of the response
61+
*/
62+
@Override
5163
public int statusCode() {
5264
return statusCode;
5365
}
5466

67+
/**
68+
* Returns the HTTP status message of the response.
69+
*
70+
* @return the status message of the response
71+
*/
5572
@Override
5673
public String statusMessage() {
5774
return statusMessage;
5875
}
5976

77+
/**
78+
* Returns the headers of the response.
79+
*
80+
* @return the headers of the response as a Map of strings
81+
*/
6082
@Override
6183
public Map<String, List<String>> headers() {
6284
return this.headers;
6385
}
6486

87+
/**
88+
* Returns the values of the header with the given name. If the header is not present, this method
89+
* returns null.
90+
*
91+
* @param headerName the name of the header
92+
* @return the values of the header with the given name as a List of strings, or null if the
93+
* header is not present
94+
*/
6595
@Override
6696
public List<String> headers(final String headerName) {
6797
return this.headers.get(headerName);
6898
}
6999

100+
/**
101+
* Returns the first value of the header with the given name. If the header is not present, this
102+
* method returns null.
103+
*
104+
* @param headerName the name of the header
105+
* @return the first value of the header with the given name, or null if the header is not present
106+
*/
70107
@Override
71108
public String header(final String headerName) {
72109
List<String> headerValues = this.headers(headerName);
@@ -80,6 +117,11 @@ public String header(final String headerName) {
80117
}
81118
}
82119

120+
/**
121+
* Was the request successful?
122+
*
123+
* @return true if the status code is in the range [200, 400)
124+
*/
83125
@Override
84126
public boolean isSuccessful() {
85127
return this.statusCode() >= HTTP_OK && this.statusCode() < HTTP_BAD_REQUEST;

src/main/java/com/spotify/github/http/okhttp/OkHttpHttpClient.java

Lines changed: 47 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,11 @@
3939
import okhttp3.*;
4040
import org.jetbrains.annotations.NotNull;
4141

42+
/**
43+
* OkHttpHttpClient is the implementation of HttpClient using OkHttp. This also serves as an example
44+
* of how to create a custom HttpClient. This HttpClient is also capable of tracing the requests
45+
* using OpenCensus or OpenTelemetry.
46+
*/
4247
public class OkHttpHttpClient implements HttpClient {
4348
private final OkHttpClient client;
4449
private Tracer tracer;
@@ -56,9 +61,15 @@ public OkHttpHttpClient(final OkHttpClient client, final Tracer tracer) {
5661
this.callFactory = createTracedClient();
5762
}
5863

64+
/**
65+
* Send a request and return a future with the response.
66+
*
67+
* @param httpRequest the request to send
68+
* @return a future with the response
69+
*/
5970
@Override
6071
public CompletableFuture<HttpResponse> send(final HttpRequest httpRequest) {
61-
Request request = buildRequest(httpRequest);
72+
Request request = buildOkHttpRequest(httpRequest);
6273
CompletableFuture<HttpResponse> future = new CompletableFuture<>();
6374
try (Span span = tracer.span(httpRequest)) {
6475
if (this.callFactory == null) {
@@ -91,7 +102,13 @@ public void setTracer(final Tracer tracer) {
91102
this.callFactory = createTracedClient();
92103
}
93104

94-
private Request buildRequest(final HttpRequest request) {
105+
/**
106+
* Build an OkHttp Request from an HttpRequest.
107+
*
108+
* @param request the HttpRequest
109+
* @return the OkHttp Request
110+
*/
111+
private Request buildOkHttpRequest(final HttpRequest request) {
95112
Request.Builder requestBuilder = new Request.Builder().url(request.url());
96113
request
97114
.headers()
@@ -109,6 +126,12 @@ private Request buildRequest(final HttpRequest request) {
109126
return requestBuilder.build();
110127
}
111128

129+
/**
130+
* Build an HttpRequest from an OkHttp Request.
131+
*
132+
* @param request the OkHttp Request
133+
* @return the HttpRequest
134+
*/
112135
private HttpRequest buildHttpRequest(final Request request) {
113136
return ImmutableHttpRequest.builder()
114137
.url(request.url().toString())
@@ -118,6 +141,11 @@ private HttpRequest buildHttpRequest(final Request request) {
118141
.build();
119142
}
120143

144+
/**
145+
* Create a traced client based on the tracer.
146+
*
147+
* @return the traced client
148+
*/
121149
private Call.Factory createTracedClient() {
122150
if (this.tracer == null || this.tracer instanceof NoopTracer) {
123151
return createTracedClientNoopTracer();
@@ -131,6 +159,11 @@ private Call.Factory createTracedClient() {
131159
return createTracedClientNoopTracer();
132160
}
133161

162+
/**
163+
* Create a traced client with a NoopTracer.
164+
*
165+
* @return the traced client
166+
*/
134167
private Call.Factory createTracedClientNoopTracer() {
135168
return new Call.Factory() {
136169
@NotNull
@@ -141,12 +174,23 @@ public Call newCall(@NotNull final Request request) {
141174
};
142175
}
143176

177+
/**
178+
* Create a traced client with OpenTelemetry.
179+
*
180+
* @return the traced client
181+
*/
144182
private Call.Factory createTracedClientOpenTelemetry() {
183+
// OkHttpTelemetry is a helper class that provides a Call.Factory that can be used to trace
145184
return OkHttpTelemetry.builder(((OpenTelemetryTracer) this.tracer).getOpenTelemetry())
146185
.build()
147186
.newCallFactory(client);
148187
}
149188

189+
/**
190+
* Create a traced client with OpenCensus.
191+
*
192+
* @return the traced client
193+
*/
150194
private Call.Factory createTracedClientOpenCensus() {
151195
return new Call.Factory() {
152196
@NotNull
@@ -159,6 +203,7 @@ public Call newCall(@NotNull final Request request) {
159203
.span(buildHttpRequest(request))
160204
.addTag(TraceHelper.TraceTags.HTTP_URL, request.url().toString());
161205
OkHttpClient.Builder okBuilder = client.newBuilder();
206+
// Add a network interceptor to trace the request
162207
okBuilder
163208
.networkInterceptors()
164209
.add(

src/main/java/com/spotify/github/http/okhttp/OkHttpHttpResponse.java

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,9 +32,10 @@
3232
import org.slf4j.Logger;
3333
import org.slf4j.LoggerFactory;
3434

35+
/** OkHttpHttpResponse is the implementation of HttpResponse using OkHttp. */
3536
public class OkHttpHttpResponse extends BaseHttpResponse {
3637
private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
37-
38+
3839
private final Response response;
3940
private InputStream body;
4041
private String bodyString;
@@ -52,6 +53,7 @@ public InputStream body() {
5253
return body;
5354
}
5455

56+
@Override
5557
public String bodyString() {
5658
if (bodyString == null) {
5759
if (response != null) {
@@ -75,6 +77,12 @@ public void close() {
7577
}
7678
}
7779

80+
/**
81+
* Get the response body as a string.
82+
*
83+
* @param response the response
84+
* @return the response body as a string
85+
*/
7886
private static String responseBodyUnchecked(final Response response) {
7987
if (response.body() == null) {
8088
return null;
Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
/*-
2+
* -\-\-
3+
* github-api
4+
* --
5+
* Copyright (C) 2021 Spotify AB
6+
* --
7+
* Licensed under the Apache License, Version 2.0 (the "License");
8+
* you may not use this file except in compliance with the License.
9+
* You may obtain a copy of the License at
10+
*
11+
* http://www.apache.org/licenses/LICENSE-2.0
12+
*
13+
* Unless required by applicable law or agreed to in writing, software
14+
* distributed under the License is distributed on an "AS IS" BASIS,
15+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16+
* See the License for the specific language governing permissions and
17+
* limitations under the License.
18+
* -/-/-
19+
*/
20+
21+
package com.spotify.github.http.okhttp;
22+
23+
import com.spotify.github.http.HttpRequest;
24+
import com.spotify.github.http.HttpResponse;
25+
import com.spotify.github.http.ImmutableHttpRequest;
26+
import com.spotify.github.tracing.Tracer;
27+
import okhttp3.*;
28+
import org.junit.jupiter.api.AfterEach;
29+
import org.junit.jupiter.api.BeforeAll;
30+
import org.junit.jupiter.api.Test;
31+
import org.mockito.ArgumentCaptor;
32+
33+
import java.io.IOException;
34+
import java.util.concurrent.CompletableFuture;
35+
import java.util.concurrent.CompletionException;
36+
37+
import static org.junit.jupiter.api.Assertions.*;
38+
import static org.mockito.ArgumentMatchers.any;
39+
import static org.mockito.Mockito.*;
40+
41+
class OkHttpHttpClientTest {
42+
private static OkHttpClient okHttpClient;
43+
private static OkHttpHttpClient httpClient;
44+
private static Tracer tracer;
45+
46+
@BeforeAll
47+
static void setUp() {
48+
okHttpClient = mock(OkHttpClient.class);
49+
tracer = mock(Tracer.class);
50+
httpClient = new OkHttpHttpClient(okHttpClient, tracer);
51+
}
52+
53+
@AfterEach
54+
void tearDown() {
55+
reset(okHttpClient, tracer);
56+
}
57+
58+
@Test
59+
void sendSuccessfully() throws IOException {
60+
// Given
61+
final Call call = mock(Call.class);
62+
final ArgumentCaptor<Callback> capture = ArgumentCaptor.forClass(Callback.class);
63+
doNothing().when(call).enqueue(capture.capture());
64+
final Response response =
65+
new okhttp3.Response.Builder()
66+
.code(200)
67+
.body(ResponseBody.create(MediaType.get("application/json"), "{\"foo\":\"bar\"}"))
68+
.message("foo")
69+
.protocol(Protocol.HTTP_1_1)
70+
.request(new Request.Builder().url("https://example.com").build())
71+
.build();
72+
73+
HttpRequest httpRequest = ImmutableHttpRequest.builder().url("https://example.com").build();
74+
when(okHttpClient.newCall(any())).thenReturn(call);
75+
76+
// When
77+
CompletableFuture<HttpResponse> futureResponse = httpClient.send(httpRequest);
78+
capture.getValue().onResponse(call, response);
79+
HttpResponse httpResponse = futureResponse.join();
80+
81+
// Then
82+
assertNotNull(httpResponse);
83+
assertEquals("{\"foo\":\"bar\"}", httpResponse.bodyString());
84+
assertEquals(200, httpResponse.statusCode());
85+
assertEquals("foo", httpResponse.statusMessage());
86+
assertTrue(httpResponse.isSuccessful());
87+
verify(tracer, times(1)).span(any(HttpRequest.class));
88+
}
89+
90+
@Test
91+
void sendWithException() {
92+
// Given
93+
final Call call = mock(Call.class);
94+
final ArgumentCaptor<Callback> capture = ArgumentCaptor.forClass(Callback.class);
95+
doNothing().when(call).enqueue(capture.capture());
96+
final IOException exception = new IOException("Network error");
97+
98+
HttpRequest httpRequest = ImmutableHttpRequest.builder().url("https://example.com").build();
99+
when(okHttpClient.newCall(any())).thenReturn(call);
100+
101+
// When
102+
CompletableFuture<HttpResponse> futureResponse = httpClient.send(httpRequest);
103+
capture.getValue().onFailure(call, exception);
104+
105+
// Then
106+
assertThrows(CompletionException.class, futureResponse::join);
107+
verify(tracer, times(1)).span(any(HttpRequest.class));
108+
}
109+
110+
@Test
111+
void sendWithClientError() throws IOException {
112+
// Given
113+
final Call call = mock(Call.class);
114+
final ArgumentCaptor<Callback> capture = ArgumentCaptor.forClass(Callback.class);
115+
doNothing().when(call).enqueue(capture.capture());
116+
final Response response =
117+
new okhttp3.Response.Builder()
118+
.code(404)
119+
.body(
120+
ResponseBody.create(MediaType.get("application/json"), "{\"error\":\"Not Found\"}"))
121+
.message("Not Found")
122+
.protocol(Protocol.HTTP_1_1)
123+
.request(new Request.Builder().url("https://example.com").build())
124+
.build();
125+
126+
HttpRequest httpRequest = ImmutableHttpRequest.builder().url("https://example.com").build();
127+
when(okHttpClient.newCall(any())).thenReturn(call);
128+
129+
// When
130+
CompletableFuture<HttpResponse> futureResponse = httpClient.send(httpRequest);
131+
capture.getValue().onResponse(call, response);
132+
HttpResponse httpResponse = futureResponse.join();
133+
134+
// Then
135+
assertNotNull(httpResponse);
136+
assertEquals("{\"error\":\"Not Found\"}", httpResponse.bodyString());
137+
assertEquals(404, httpResponse.statusCode());
138+
assertEquals("Not Found", httpResponse.statusMessage());
139+
assertFalse(httpResponse.isSuccessful());
140+
verify(tracer, times(1)).span(any(HttpRequest.class));
141+
}
142+
}

0 commit comments

Comments
 (0)