Skip to content

Commit

Permalink
feature: add unit tests to core project (#81)
Browse files Browse the repository at this point in the history
* Update gradle.yml

* test: add test branch

* feature: add build info

* feature: add nativeClient tests

* feature: add coverage

* feature: initialize client using the builder

* feature: add additional tests to nativeclient

* feature: add requestUtil tests

* feature: add permissions for pr

* feature: remove code coverage

* feature: remove unneeded deps

* feature: remove explicit permissions

* feature: report to workflow summary

* feature: Update .github/workflows/gradle.yml

* feature: test new version

* feature: add summary instead of pr comment
  • Loading branch information
brumarqu-te authored Sep 27, 2024
1 parent 894a5e1 commit 1b35c40
Show file tree
Hide file tree
Showing 11 changed files with 306 additions and 91 deletions.
5 changes: 3 additions & 2 deletions .github/workflows/gradle.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ name: Java CI with Gradle
on:
push:
branches: [ "main" ]
pull_request_target:
pull_request:
branches: [ "main" ]

jobs:
Expand All @@ -25,10 +25,11 @@ jobs:

- name: Add coverage to PR
id: jacoco
uses: madrapps/jacoco-report@v1.7.1
uses: madrapps/jacoco-report@v1.7.2-beta
with:
paths: |
${{ github.workspace }}/**/build/reports/jacoco/**/*.xml
token: ${{ secrets.GITHUB_TOKEN }}
min-coverage-overall: 40
min-coverage-changed-files: 60
comment-type: 'summary'
13 changes: 2 additions & 11 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ plugins {
ext {
apacheCommonsLang3 = "3.14.0"
googleJsr305Version = "3.0.2"
mockitoVersion = "3.2.4"
mockitoVersion = "5.3.0"
jacksonDatabindNullableVersion = "0.2.6"
jacksonVersion = "2.16.1"
jacocoVersion = "0.8.8"
Expand Down Expand Up @@ -46,26 +46,17 @@ subprojects {
classpath = sourceSets.main.runtimeClasspath
}

def jacocoXmlReport = "${project.rootDir}/${project.name}/reports/jacoco/${project.name}.xml"

jacoco {
toolVersion = "$jacocoVersion"
}

jacocoTestReport {
sourceSets sourceSets.main
executionData.from = fileTree(project.buildDir).include("/jacoco/*.exec")
dependsOn test
reports {
csv.required = false
html.required = false
xml.required = true
xml.outputLocation.set(file(jacocoXmlReport))
}
}

test {
useJUnitPlatform()
ignoreFailures = true
finalizedBy jacocoTestReport
maxParallelForks = Runtime.runtime.availableProcessors().intdiv(2) ?: 1
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,18 +35,10 @@ public HttpBearerAuth(String scheme) {
this.scheme = scheme;
}

public String getBearerToken() {
return tokenSupplier.get();
}

public void setBearerToken(String bearerToken) {
this.tokenSupplier = () -> bearerToken;
}

public void setBearerToken(Supplier<String> tokenSupplier) {
this.tokenSupplier = tokenSupplier;
}

@Override
public void applyToParams(List<Pair<String, String>> queryParams,
Map<String, String> headerParams,
Expand Down
2 changes: 2 additions & 0 deletions core/client-native/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -22,4 +22,6 @@ dependencies {
testAnnotationProcessor "org.projectlombok:lombok:1.18.30"

testImplementation "org.junit.jupiter:junit-jupiter:$junitVersion"
testImplementation "org.mockito:mockito-core:$mockitoVersion"
testImplementation "org.mockito:mockito-junit-jupiter:$mockitoVersion"
}
Original file line number Diff line number Diff line change
Expand Up @@ -36,17 +36,13 @@



@Getter
@AllArgsConstructor
public class NativeApiClient implements ApiClient {
@Getter
private String baseUri;
@Getter
private HttpClient httpClient;
@Getter
private ObjectMapper mapper;
@Getter
private Consumer<HttpRequest.Builder> interceptor;
@Getter
private Consumer<HttpResponse<InputStream>> responseInterceptor;

public static NativeApiClientBuilder builder() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,19 +41,34 @@



@Setter
@Accessors(fluent = true)
public class NativeApiClientBuilder {
private static Map<String, Authentication> authentications = Map.of(
private Map<String, Authentication> authentications = Map.of(
HttpBearerAuth.class.getSimpleName(), new HttpBearerAuth("Bearer")
);

@Setter
private HttpClient.Builder httpClientBuilder = HttpClient.newBuilder();

@Setter
private ObjectMapper mapper = JSON.getDefault().getMapper();
private String baseUri = "https://api.thousandeyes.com";

@Setter
private String baseUri = "https://api.thousandeyes.com/v7";

@Setter
private Consumer<HttpRequest.Builder> interceptor;

@Setter
private Consumer<HttpResponse<InputStream>> responseInterceptor;

@Setter
private Duration connectTimeout;

@Setter
private String bearerToken;

@Setter
private boolean defaultRateLimitingEnabled = true;

public ApiClient build() {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,219 @@
package com.thousandeyes.sdk.client;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.argThat;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.verify;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.http.HttpClient;
import java.net.http.HttpHeaders;
import java.net.http.HttpResponse;
import java.time.Duration;
import java.time.OffsetDateTime;
import java.time.ZoneId;
import java.util.List;
import java.util.Map;
import java.util.stream.Stream;

import org.apache.commons.lang3.tuple.Pair;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import org.junit.jupiter.params.provider.ValueSource;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;

import com.fasterxml.jackson.databind.ObjectMapper;



@ExtendWith(MockitoExtension.class)
class NativeApiClientTest {
@Mock
private HttpResponse<InputStream> httpResponse;
@Mock
private HttpClient httpClient;
@Mock
private HttpClient.Builder httpClientBuilder;
private final ObjectMapper objectMapper = new ObjectMapper()
.findAndRegisterModules();
private ApiClient apiClient;

public static Stream<Arguments> invalidRequestProvider() {
return Stream.of(
Arguments.of(IOException.class),
Arguments.of(InterruptedException.class)
);
}

@BeforeEach
public void setUp() {
doReturn(httpClient)
.when(httpClientBuilder)
.build();

apiClient = NativeApiClient.builder()
.baseUri("http://localhost")
.httpClientBuilder(httpClientBuilder)
.mapper(objectMapper)
.build();
}

public static Stream<Arguments> validRequestProvider() {
return Stream.of(
Arguments.of("GET with query params and headers",
baseRequestBuilder()
.queryParams(List.of(Pair.of("aid", "33")))
.build()),
Arguments.of("POST with body and headers",
baseRequestBuilder()
.method("POST")
.header("content-type", List.of("application/json"))
.queryParams(List.of(Pair.of("aid", "33")))
.requestBody(new Request("name", 1))
.build()),
Arguments.of("POST with string body",
baseRequestBuilder()
.method("POST")
.requestBody("a String")
.build()),
Arguments.of("DELETE without query params, body and headers",
baseRequestBuilder()
.method("DELETE")
.build())
);
}

@ParameterizedTest(name = "{0}")
@MethodSource("validRequestProvider")
void shouldSendRequestCorrectly(String testName, ApiRequest request)
throws ApiException, IOException, InterruptedException
{
var expectedResponse = new Response("name", OffsetDateTime.now(ZoneId.of("UTC")));
stubHttpClient(expectedResponse);

var response = apiClient.send(request, Response.class);

assertEquals(expectedResponse, response.getData());
}

@ParameterizedTest(name = "{0}")
@MethodSource("validRequestProvider")
void shouldSendRequestForListCorrectly(String testName, ApiRequest request)
throws ApiException, IOException, InterruptedException
{
var expectedResponse = List.of(new Response("name", OffsetDateTime.now(ZoneId.of("UTC"))));
stubHttpClient(expectedResponse);

var response = apiClient.sendForList(request, Response.class);

assertEquals(expectedResponse, response.getData());
}

@ParameterizedTest(name = "{0}")
@MethodSource("invalidRequestProvider")
void shouldThrowWhenHttpClientThrows(Class<Throwable> expectedException)
throws IOException, InterruptedException
{
var request = baseRequestBuilder().build();
stubHttpClient(expectedException);

var exception = assertThrows(ApiException.class,
() -> apiClient.send(request, Response.class));

assertEquals(expectedException, exception.getCause().getClass());
}

@ParameterizedTest(name = "{0}")
@ValueSource(ints = { 400, 401, 403, 404, 500, 502, 503 })
void shouldThrowWhenHttpClientResponseNot2xx(int statusCode)
throws IOException, InterruptedException
{
var request = baseRequestBuilder().build();
var expectedResponse = List.of(new Response("name", OffsetDateTime.now(ZoneId.of("UTC"))));
stubHttpClient(expectedResponse, statusCode);

var exception = assertThrows(ApiException.class,
() -> apiClient.send(request, Response.class));

assertEquals(statusCode, exception.getCode());
}

@Test
public void shouldAllowDifferentApiClientInstancesWithDifferentBearerTokens()
throws ApiException, IOException, InterruptedException
{
sendRequestAndVerifyBearerToken("BearerTokenA");
sendRequestAndVerifyBearerToken("BearerTokenB");
}

private void sendRequestAndVerifyBearerToken(String bearerToken)
throws IOException, InterruptedException, ApiException
{
stubHttpClient(new Response("name", OffsetDateTime.now(ZoneId.of("UTC"))));
var apiClient = NativeApiClient.builder()
.baseUri("http://localhost")
.bearerToken(bearerToken)
.httpClientBuilder(httpClientBuilder)
.mapper(objectMapper)
.build();

apiClient.send(baseRequestBuilder().build(), Response.class);

verify(httpClient).send(argThat(request -> request.headers()
.firstValue("Authorization")
.orElse("")
.equals("Bearer " + bearerToken)),
any());
}

private void stubHttpClient(Class<Throwable> expectedException) throws IOException,
InterruptedException
{
doThrow(expectedException)
.when(httpClient)
.send(any(), any());
}

private <T> void stubHttpClient(T expectedResponse) throws IOException,
InterruptedException
{
stubHttpClient(expectedResponse, 200);
}

private <T> void stubHttpClient(T expectedResponse, int statusCode) throws IOException,
InterruptedException
{
var expectedResponseBytes = objectMapper.writeValueAsBytes(expectedResponse);
var body = new ByteArrayInputStream(expectedResponseBytes);
doReturn(body).when(httpResponse).body();
var headers = HttpHeaders.of(Map.of(), (s, s2) -> true);
doReturn(headers).when(httpResponse).headers();
doReturn(statusCode).when(httpResponse).statusCode();
doReturn(httpResponse).when(httpClient).send(any(), any());
}

private static ApiRequest.ApiRequestBuilder baseRequestBuilder() {
return ApiRequest.builder()
.method("GET")
.path("/account-groups")
.readTimeout(Duration.ofSeconds(2));
}

private record Request(String name, Integer interval) {
}



private record Response(String name, OffsetDateTime dateTime) {
}
}
Loading

0 comments on commit 1b35c40

Please sign in to comment.