Skip to content

Commit

Permalink
add integration tests to webflux
Browse files Browse the repository at this point in the history
  • Loading branch information
pboos committed Nov 22, 2023
1 parent 312f3ac commit 4a1e00c
Show file tree
Hide file tree
Showing 9 changed files with 475 additions and 5 deletions.
Original file line number Diff line number Diff line change
@@ -1,13 +1,10 @@
package com.getyourguide.openapi.validation.integration;

import com.getyourguide.openapi.validation.api.log.ViolationLogger;
import com.getyourguide.openapi.validation.test.DefaultSpringBootTestConfiguration;
import com.getyourguide.openapi.validation.test.TestViolationLogger;
import org.springframework.boot.SpringBootConfiguration;
import org.springframework.boot.autoconfigure.AutoConfigurationExcludeFilter;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.context.TypeExcludeFilter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.FilterType;

Expand Down
5 changes: 3 additions & 2 deletions spring-boot-starter/spring-boot-starter-webflux/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,9 @@ dependencies {
// TODO use spotbugs instead and also apply to all modules?
implementation(libs.find.bugs)

testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation 'org.springframework:spring-webflux'
testImplementation project(':test:test-utils')
testImplementation project(':test:openapi-webflux')
testImplementation 'org.springframework.boot:spring-boot-starter-webflux'
testImplementation 'io.projectreactor:reactor-test'
testImplementation 'org.apache.tomcat.embed:tomcat-embed-core' // For jakarta.servlet.ServletContext
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
package com.getyourguide.openapi.validation.integration;

import static org.junit.jupiter.api.Assertions.assertEquals;

import com.getyourguide.openapi.validation.test.TestViolationLogger;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.http.MediaType;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import org.springframework.test.web.reactive.server.WebTestClient;

@SpringBootTest
@AutoConfigureMockMvc
@ExtendWith(SpringExtension.class)
public class ExceptionsNoExceptionHandlerTest {

// These test cases test that requests to an endpoint that throws an exception (Mono.error)
// that is not handled by any code (no global error handler either) is correctly intercepted by the library.

@Autowired
private WebTestClient webTestClient;

@Autowired
private TestViolationLogger openApiViolationLogger;

@BeforeEach
public void setup() {
openApiViolationLogger.clearViolations();
}

@Test
public void whenTestInvalidQueryParamThenReturns400WithoutViolationLogged() throws Exception {
webTestClient
.get().uri("/test?date=not-a-date")
.accept(MediaType.APPLICATION_JSON)
.exchange()
.expectStatus().isBadRequest()
.expectBody()
.jsonPath("$.status").isEqualTo(400)
.jsonPath("$.path").isEqualTo("/test")
.jsonPath("$.error").isEqualTo("Bad Request");
Thread.sleep(100);

assertEquals(0, openApiViolationLogger.getViolations().size());
}

@Test
public void whenTestThrowExceptionWithResponseStatusThenReturns400WithoutViolationLogged()
throws Exception {
webTestClient
.get().uri("/test?testCase=throwExceptionWithResponseStatus")
.accept(MediaType.APPLICATION_JSON)
.exchange()
.expectStatus().isBadRequest()
.expectBody()
.jsonPath("$.status").isEqualTo(400)
.jsonPath("$.path").isEqualTo("/test")
.jsonPath("$.error").isEqualTo("Bad Request");
Thread.sleep(100);

assertEquals(0, openApiViolationLogger.getViolations().size());
}

@Test
public void whenTestThrowExceptionWithoutResponseStatusThenReturns500WithoutViolationLogged()
throws Exception {
webTestClient
.get().uri("/test?testCase=throwExceptionWithoutResponseStatus")
.accept(MediaType.APPLICATION_JSON)
.exchange()
.expectStatus().is5xxServerError()
.expectBody()
.jsonPath("$.status").isEqualTo(500)
.jsonPath("$.path").isEqualTo("/test")
.jsonPath("$.error").isEqualTo("Internal Server Error");
Thread.sleep(100);

assertEquals(0, openApiViolationLogger.getViolations().size());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
package com.getyourguide.openapi.validation.integration;

import static org.junit.jupiter.api.Assertions.assertEquals;

import com.getyourguide.openapi.validation.test.TestViolationLogger;
import com.getyourguide.openapi.validation.test.exception.WithResponseStatusException;
import com.getyourguide.openapi.validation.test.exception.WithoutResponseStatusException;
import com.getyourguide.openapi.validation.test.openapi.webflux.model.BadRequestResponse;
import java.util.Optional;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import org.springframework.test.web.reactive.server.WebTestClient;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException;
import org.springframework.web.server.ServerWebInputException;

@SpringBootTest(classes = {
SpringBootTestConfiguration.class,
ExceptionsWithExceptionHandlerTest.ExceptionHandlerConfiguration.class,
})
@AutoConfigureMockMvc
@ExtendWith(SpringExtension.class)
public class ExceptionsWithExceptionHandlerTest {

@Autowired
private WebTestClient webTestClient;

@Autowired
private TestViolationLogger openApiViolationLogger;

@BeforeEach
public void setup() {
openApiViolationLogger.clearViolations();
}

@Test
public void whenTestInvalidQueryParamThenReturns400WithoutViolationLogged() throws Exception {
webTestClient
.get().uri("/test?date=not-a-date")
.accept(MediaType.APPLICATION_JSON)
.exchange()
.expectStatus().isBadRequest()
.expectBody().jsonPath("$.error").isEqualTo("ServerWebInputException");
Thread.sleep(100);

assertEquals(0, openApiViolationLogger.getViolations().size());
}

@Test
public void whenTestThrowExceptionWithResponseStatusThenReturns400WithoutViolationLogged()
throws Exception {
webTestClient
.get().uri("/test?testCase=throwExceptionWithResponseStatus")
.accept(MediaType.APPLICATION_JSON)
.exchange()
.expectStatus().isBadRequest()
.expectBody().jsonPath("$.error").isEqualTo("Unhandled exception");
Thread.sleep(100);

assertEquals(0, openApiViolationLogger.getViolations().size());
}

@Test
public void whenTestThrowExceptionWithoutResponseStatusThenReturns500WithoutViolationLogged()
throws Exception {
webTestClient
.get().uri("/test?testCase=throwExceptionWithoutResponseStatus")
.accept(MediaType.APPLICATION_JSON)
.exchange()
.expectStatus().is5xxServerError()
.expectBody().isEmpty();
Thread.sleep(100);

// Note: We return no body on purpose in the exception handler below to test this violation appears.
assertEquals(1, openApiViolationLogger.getViolations().size());
var violation = openApiViolationLogger.getViolations().get(0);
assertEquals("validation.response.body.missing", violation.getRule());
assertEquals(Optional.of(500), violation.getResponseStatus());
}

@ControllerAdvice
public static class ExceptionHandlerConfiguration {
@ExceptionHandler(ServerWebInputException.class)
public ResponseEntity<?> handle(ServerWebInputException exception) {
return ResponseEntity.badRequest().body(new BadRequestResponse().error("ServerWebInputException"));
}

@ExceptionHandler(WithResponseStatusException.class)
public ResponseEntity<?> handle(WithResponseStatusException exception) {
return ResponseEntity.badRequest().body(new BadRequestResponse().error("Unhandled exception"));
}

@ExceptionHandler(WithoutResponseStatusException.class)
public ResponseEntity<?> handle(WithoutResponseStatusException exception) {
return ResponseEntity.internalServerError().build();
}

@ExceptionHandler(MethodArgumentTypeMismatchException.class)
public ResponseEntity<?> handle(MethodArgumentTypeMismatchException exception) {
return ResponseEntity.badRequest().body(new BadRequestResponse().error("Invalid parameter"));
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
package com.getyourguide.openapi.validation.integration;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;

import com.getyourguide.openapi.validation.api.model.OpenApiViolation;
import com.getyourguide.openapi.validation.test.TestViolationLogger;
import com.getyourguide.openapi.validation.test.openapi.webflux.model.TestResponse;
import java.util.List;
import java.util.Optional;
import javax.annotation.Nullable;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.http.MediaType;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import org.springframework.test.web.reactive.server.WebTestClient;

@SpringBootTest
@AutoConfigureMockMvc
@ExtendWith(SpringExtension.class)
public class OpenApiValidationIntegrationTest {
@Autowired
private WebTestClient webTestClient;

@Autowired
private TestViolationLogger openApiViolationLogger;

@BeforeEach
public void setup() {
openApiViolationLogger.clearViolations();
}

@Test
public void whenTestSuccessfulResponseThenShouldNotLogViolation() throws Exception {
webTestClient
.get().uri("/test")
.accept(MediaType.APPLICATION_JSON)
.exchange()
.expectStatus().isOk()
.expectBody(TestResponse.class)
.consumeWith(serverResponse -> {
assertNotNull(serverResponse.getResponseBody());
assertEquals("test", serverResponse.getResponseBody().getValue());
});
Thread.sleep(100);

assertEquals(0, openApiViolationLogger.getViolations().size());
}

@Test
public void whenTestValidRequestWithInvalidResponseThenShouldReturnSuccessAndLogViolation() throws Exception {
webTestClient
.get().uri("/test?value=invalid-response-value!")
.accept(MediaType.APPLICATION_JSON)
.exchange()
.expectStatus().isOk()
.expectBody(TestResponse.class)
.consumeWith(serverResponse -> {
assertNotNull(serverResponse.getResponseBody());
assertEquals("invalid-response-value!", serverResponse.getResponseBody().getValue());
});
Thread.sleep(100);

assertEquals(1, openApiViolationLogger.getViolations().size());
var violation = openApiViolationLogger.getViolations().get(0);
assertEquals("validation.response.body.schema.pattern", violation.getRule());
assertEquals(Optional.of(200), violation.getResponseStatus());
assertEquals(Optional.of("/value"), violation.getInstance());
}

@Test
public void whenTestInvalidRequestNotHandledBySpringBootThenShouldReturnSuccessAndLogViolation() throws Exception {
webTestClient
.post().uri("/test")
.accept(MediaType.APPLICATION_JSON)
.contentType(MediaType.APPLICATION_JSON)
.bodyValue("{ \"value\": 1 }")
.exchange()
.expectStatus().isNoContent()
.expectBody().isEmpty();
Thread.sleep(100);

assertEquals(1, openApiViolationLogger.getViolations().size());
var violation = openApiViolationLogger.getViolations().get(0);
assertEquals("validation.request.body.schema.type", violation.getRule());
assertEquals(Optional.of(204), violation.getResponseStatus());
assertEquals(Optional.of("/value"), violation.getInstance());
}


@Test
public void whenTestInvalidRequestAndInvalidResponseThenShouldReturnSuccessAndLogViolation() throws Exception {
webTestClient
.post().uri("/test")
.accept(MediaType.APPLICATION_JSON)
.contentType(MediaType.APPLICATION_JSON)
.bodyValue("{ \"value\": 1, \"responseStatusCode\": 200 }")
.exchange()
.expectStatus().isOk()
.expectBody(TestResponse.class)
.consumeWith(serverResponse -> {
assertNotNull(serverResponse.getResponseBody());
assertEquals("1", serverResponse.getResponseBody().getValue());
});
Thread.sleep(100);

var violations = openApiViolationLogger.getViolations();
assertEquals(2, violations.size());
var violation = getViolationByRule(violations, "validation.response.body.schema.pattern");
assertNotNull(violation);
assertEquals(Optional.of(200), violation.getResponseStatus());
assertEquals(Optional.of("/value"), violation.getInstance());
var violation2 = getViolationByRule(violations, "validation.request.body.schema.type");
assertNotNull(violation2);
assertEquals(Optional.of(200), violation2.getResponseStatus());
assertEquals(Optional.of("/value"), violation2.getInstance());
}

@Test
public void whenTestOptionsCallThenShouldNotValidate() throws Exception {
// Note: Options is not in the spec and would report a violation if it was validated.
webTestClient
.options().uri("/test")
.accept(MediaType.APPLICATION_JSON)
.exchange()
.expectStatus().isOk()
.expectBody().isEmpty();
Thread.sleep(100);

assertEquals(0, openApiViolationLogger.getViolations().size());
}

// TODO Add test that fails on request violation immediately (maybe needs separate test class & setup) should not log violation

@Nullable
private OpenApiViolation getViolationByRule(List<OpenApiViolation> violations, String rule) {
return violations.stream()
.filter(violation -> violation.getRule().equals(rule))
.findFirst()
.orElse(null);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package com.getyourguide.openapi.validation.integration;

import com.getyourguide.openapi.validation.test.DefaultSpringBootTestConfiguration;
import org.springframework.boot.SpringBootConfiguration;
import org.springframework.boot.autoconfigure.AutoConfigurationExcludeFilter;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.context.TypeExcludeFilter;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.FilterType;

@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan(excludeFilters = {
@ComponentScan.Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class),
@ComponentScan.Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class)
})
public class SpringBootTestConfiguration extends DefaultSpringBootTestConfiguration {
}
Loading

0 comments on commit 4a1e00c

Please sign in to comment.