Skip to content
Snippets Groups Projects
Commit 8d546108 authored by OZGCloud's avatar OZGCloud
Browse files

Merge pull request 'simplify tests; remove map' (#31) from...

Merge pull request 'simplify tests; remove map' (#31) from RefactorExceptionControllerTests into master

Reviewed-on: https://git.ozg-sh.de/ozgcloud-app/administration/pulls/31
parents ceb51b32 6466db18
No related branches found
No related tags found
No related merge requests found
......@@ -29,62 +29,70 @@ import java.util.Map;
import java.util.Optional;
import java.util.Set;
import jakarta.validation.ConstraintViolation;
import jakarta.validation.ConstraintViolationException;
import org.springframework.data.rest.webmvc.ResourceNotFoundException;
import org.springframework.http.HttpStatus;
import org.springframework.http.ProblemDetail;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.web.ErrorResponse;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler;
import de.ozgcloud.common.errorhandling.TechnicalException;
import jakarta.validation.ConstraintViolation;
import jakarta.validation.ConstraintViolationException;
@RestControllerAdvice
public class ExceptionController extends ResponseEntityExceptionHandler {
@ExceptionHandler(RuntimeException.class)
public ErrorResponse handleRuntimeException(RuntimeException ex) {
return ErrorResponse.builder(ex, HttpStatus.INTERNAL_SERVER_ERROR, ex.getLocalizedMessage()).build();
@ResponseBody
public ProblemDetail handleRuntimeException(RuntimeException ex) {
return buildProblemDetail(HttpStatus.INTERNAL_SERVER_ERROR, ex);
}
@ExceptionHandler(AccessDeniedException.class)
public ErrorResponse handleAccessDeniedException(AccessDeniedException ex) {
return ErrorResponse.builder(ex, HttpStatus.FORBIDDEN, ex.getLocalizedMessage()).build();
@ResponseBody
public ProblemDetail handleAccessDeniedException(AccessDeniedException ex) {
return buildProblemDetail(HttpStatus.FORBIDDEN, ex);
}
@ExceptionHandler(ResourceNotFoundException.class)
public ErrorResponse handleResourceNotFoundException(ResourceNotFoundException ex) {
return ErrorResponse.builder(ex, HttpStatus.NOT_FOUND, ex.getLocalizedMessage()).build();
@ResponseBody
public ProblemDetail handleResourceNotFoundException(ResourceNotFoundException ex) {
return buildProblemDetail(HttpStatus.NOT_FOUND, ex);
}
@ExceptionHandler(FunctionalException.class)
public ErrorResponse handleFunctionalException(FunctionalException ex) {
return ErrorResponse.builder(ex, HttpStatus.BAD_REQUEST, ex.getLocalizedMessage()).build();
@ResponseBody
public ProblemDetail handleFunctionalException(FunctionalException ex) {
return buildProblemDetail(HttpStatus.BAD_REQUEST, ex);
}
@ExceptionHandler(TechnicalException.class)
public ErrorResponse handleTechnicalException(TechnicalException ex) {
return ErrorResponse.builder(ex, HttpStatus.INTERNAL_SERVER_ERROR, ex.getLocalizedMessage()).build();
@ResponseBody
public ProblemDetail handleTechnicalException(TechnicalException ex) {
return buildProblemDetail(HttpStatus.INTERNAL_SERVER_ERROR, ex);
}
private ProblemDetail buildProblemDetail(HttpStatus status, Exception ex) {
return ProblemDetail.forStatusAndDetail(status, ex.getLocalizedMessage());
}
@ExceptionHandler(ConstraintViolationException.class)
public ErrorResponse handleConstraintViolationException(ConstraintViolationException ex) {
var problemDetail = buildProblemDetail(HttpStatus.UNPROCESSABLE_ENTITY, ex);
return ErrorResponse.builder(ex, problemDetail).build();
@ResponseBody
public ProblemDetail handleConstraintViolationException(ConstraintViolationException ex) {
return buildConstraintViolationProblemDetail(HttpStatus.UNPROCESSABLE_ENTITY, ex);
}
private ProblemDetail buildProblemDetail(HttpStatus status, ConstraintViolationException ex) {
private ProblemDetail buildConstraintViolationProblemDetail(HttpStatus status, ConstraintViolationException ex) {
var problemDetail = ProblemDetail.forStatusAndDetail(status, ex.getLocalizedMessage());
problemDetail.setProperty("invalid-params", getDetailedviolationList(ex.getConstraintViolations()));
return problemDetail;
}
private List<Map<String, String>> getDetailedviolationList(Set<ConstraintViolation<?>> violations) {
List<Map<String, String>> detailedViolations = new ArrayList<>();
var detailedViolations = new ArrayList<Map<String, String>>();
Optional.ofNullable(violations).orElse(Collections.emptySet()).forEach(v -> detailedViolations.add(buildDetailedViolation(v)));
return detailedViolations;
......
......@@ -6,7 +6,6 @@ import static org.springframework.test.web.servlet.request.MockMvcRequestBuilder
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
import java.util.Set;
import java.util.stream.Stream;
import jakarta.validation.ConstraintViolation;
import jakarta.validation.ConstraintViolationException;
......@@ -18,13 +17,9 @@ import org.apache.commons.lang3.StringUtils;
import org.assertj.core.util.Arrays;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.http.HttpStatus;
import org.springframework.security.test.context.support.WithMockUser;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.ResultActions;
......@@ -47,38 +42,9 @@ class ExceptionControllerITCase {
@MockBean
private RootModelAssembler modelAssembler;
@Nested
class TestExceptions {
@ParameterizedTest
@MethodSource("exceptionAndExpectedStatus")
@SneakyThrows
void shouldHandleExceptionWithStatus(Class<? extends Exception> exceptionClass, HttpStatus expectedStatus) {
when(modelAssembler.toModel(any())).thenThrow(TestErrorController.EXCEPTION_PRODUCER.get(exceptionClass).produceException());
var result = performGet();
result.andExpect(status().is(expectedStatus.value()));
}
@ParameterizedTest
@MethodSource("exceptionAndExpectedStatus")
@SneakyThrows
void shouldRespondWithStatusInBody(Class<? extends Exception> exceptionClass, HttpStatus expectedStatus) {
when(modelAssembler.toModel(any())).thenThrow(exceptionClass);
var result = performGet();
result.andExpect(jsonPath("$.status").value(expectedStatus.value()));
}
private static Stream<Arguments> exceptionAndExpectedStatus() {
return TestErrorController.STATUS_BY_EXCEPTION.entrySet().stream().map(kv -> Arguments.of(kv.getKey(), kv.getValue()));
}
}
@Nested
class TestConstraintViolationException {
@Test
@SneakyThrows
void shouldHaveInvalidFieldNameInResponse() {
......@@ -129,11 +95,9 @@ class ExceptionControllerITCase {
private String string2;
}
}
@SneakyThrows
private ResultActions performGet() {
return mockMvc.perform(get(RootController.PATH));
}
}
}
\ No newline at end of file
......@@ -21,23 +21,27 @@
*/
package de.ozgcloud.admin.common.errorhandling;
import static org.junit.Assert.*;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
import java.util.stream.Stream;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import org.springframework.data.rest.webmvc.ResourceNotFoundException;
import org.springframework.http.HttpStatus;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.ResultActions;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import com.jayway.jsonpath.JsonPath;
import de.ozgcloud.common.errorhandling.TechnicalException;
import lombok.SneakyThrows;
class ExceptionControllerTest {
......@@ -51,97 +55,392 @@ class ExceptionControllerTest {
.setControllerAdvice(new ExceptionController()).build();
}
@DisplayName("Error handler")
@DisplayName("Runtime Exception")
@Nested
class TestErrorHandler {
class TestRuntimeException {
private final static String TEST_ERROR_ENDPOINT = TestErrorController.BASE_PATH + "/runtime";
private final static int STATUS_VALUE = HttpStatus.INTERNAL_SERVER_ERROR.value();
@ParameterizedTest
@MethodSource("exceptionAndExpectedStatus")
@SneakyThrows
void shouldHandleExceptionWithStatus(Class<? extends Exception> exceptionClass, HttpStatus expectedStatus) {
var result = doPerformWithError(exceptionClass);
@Test
void shouldReturnStatus() {
var result = doPerformGet();
result.andExpect(status().is(expectedStatus.value()));
result.andExpect(status().is(STATUS_VALUE));
}
@ParameterizedTest
@MethodSource("exceptionAndExpectedStatus")
@DisplayName("response body")
@Nested
class TestRuntimeExceptionBody {
@SneakyThrows
void shouldRespondWithStatusInBody(Class<? extends Exception> exceptionClass, HttpStatus expectedStatus) {
var result = doPerformWithError(exceptionClass);
@Test
void shouldHaveStatus() {
var result = doPerformGet();
result.andExpect(jsonPath("$.status").value(expectedStatus.value()));
result.andExpect(jsonPath("$.status").value(STATUS_VALUE));
}
@ParameterizedTest
@MethodSource("exceptionAndExpectedStatus")
@SneakyThrows
void shouldRespondWithTitler(Class<? extends Exception> exceptionClass, HttpStatus expectedStatus) {
var result = doPerformWithError(exceptionClass);
@Test
void shouldHaveTitle() {
var result = doPerformGet();
result.andExpect(jsonPath("$.title").exists());
result.andExpect(jsonPath("$.title").value("Internal Server Error"));
}
@ParameterizedTest
@MethodSource("exceptionAndExpectedStatus")
@SneakyThrows
void shouldRespondWithDetail(Class<? extends Exception> exceptionClass, HttpStatus expectedStatus) {
var result = doPerformWithError(exceptionClass);
@Test
void shouldHaveDetail() {
var result = doPerformGet();
result.andExpect(jsonPath("$.detail").exists());
result.andExpect(jsonPath("$.detail").value("error message"));
}
@ParameterizedTest
@MethodSource("exceptionAndExpectedStatus")
@SneakyThrows
void shouldRespondWithInstance(Class<? extends Exception> exceptionClass, HttpStatus expectedStatus) {
var result = doPerformWithError(exceptionClass);
@Test
void shouldHaveInstance() {
var result = doPerformGet();
result.andExpect(jsonPath("$.instance").exists());
result.andExpect(jsonPath("$.instance").value(TEST_ERROR_ENDPOINT));
}
@ParameterizedTest
@MethodSource("exceptionAndExpectedStatus")
@SneakyThrows
void shouldRespondWithType(Class<? extends Exception> exceptionClass, HttpStatus expectedStatus) {
var result = doPerformWithError(exceptionClass);
void shouldHaveType() {
var result = doPerformGet();
result.andExpect(jsonPath("$.type").exists());
result.andExpect(jsonPath("$.type").value("about:blank"));
}
}
private static Stream<Arguments> exceptionAndExpectedStatus() {
return TestErrorController.STATUS_BY_EXCEPTION.entrySet().stream().map(kv -> Arguments.of(kv.getKey(), kv.getValue()));
@SneakyThrows
private ResultActions doPerformGet() {
return mockMvc.perform(get(TEST_ERROR_ENDPOINT));
}
}
@DisplayName("Access Denied Exception")
@Nested
class TestAccessDeniedException {
private final static String TEST_ERROR_ENDPOINT = TestErrorController.BASE_PATH + "/access-denied";
private final static int STATUS_VALUE = HttpStatus.FORBIDDEN.value();
@SneakyThrows
private ResultActions doPerformWithError(Class<? extends Exception> exceptionClass) {
return mockMvc.perform(get("/api/test-error").param("errorClassName", exceptionClass.getName()));
@Test
void shouldReturnStatus() {
var result = doPerformGet();
result.andExpect(status().is(STATUS_VALUE));
}
@DisplayName("response body")
@Nested
class TestAccessDeniedExceptionBody {
@SneakyThrows
@Test
void shouldHaveStatus() {
var result = doPerformGet();
result.andExpect(jsonPath("$.status").value(STATUS_VALUE));
}
@SneakyThrows
@Test
void shouldHaveTitle() {
var result = doPerformGet();
result.andExpect(jsonPath("$.title").value("Forbidden"));
}
@SneakyThrows
@Test
void shouldHaveDetail() {
var result = doPerformGet();
result.andExpect(jsonPath("$.detail").value("error message"));
}
@DisplayName("ResourceNotFound error")
@SneakyThrows
@Test
void shouldHaveInstance() {
var result = doPerformGet();
result.andExpect(jsonPath("$.instance").value(TEST_ERROR_ENDPOINT));
}
@SneakyThrows
void shouldHaveType() {
var result = doPerformGet();
result.andExpect(jsonPath("$.type").value("about:blank"));
}
}
@SneakyThrows
private ResultActions doPerformGet() {
return mockMvc.perform(get(TEST_ERROR_ENDPOINT));
}
}
@DisplayName("ResourceNotFound Exception")
@Nested
class TestResourceNotFoundError {
class TestResourceNotFoundException {
private final static String TEST_ERROR_ENDPOINT = TestErrorController.BASE_PATH + "/resource-not-found";
private final static int STATUS_VALUE = HttpStatus.NOT_FOUND.value();
@SneakyThrows
@Test
void shouldReturnStatus() {
var result = doPerformGet();
result.andExpect(status().is(STATUS_VALUE));
}
@DisplayName("response body")
@Nested
class TestResourceNotFoundExceptionBody {
@SneakyThrows
@Test
void shouldHaveStatus() {
var result = doRequestUnknown();
var result = doPerformGet();
result.andExpect(status().is(HttpStatus.NOT_FOUND.value()));
result.andExpect(jsonPath("$.status").value(STATUS_VALUE));
}
@SneakyThrows
@Test
void shouldHaveTitle() {
var result = doPerformGet();
result.andExpect(jsonPath("$.title").value("Not Found"));
}
@SneakyThrows
@Test
void shouldHaveDetail() {
var result = doPerformGet();
result.andExpect(jsonPath("$.detail").value("error message"));
}
@SneakyThrows
@Test
void shouldHaveInstance() {
var result = doPerformGet();
result.andExpect(jsonPath("$.instance").value(TEST_ERROR_ENDPOINT));
}
@SneakyThrows
void shouldRespondWithStatusInBody() {
var result = doRequestUnknown();
void shouldHaveType() {
var result = doPerformGet();
result.andExpect(jsonPath("$.type").value("about:blank"));
}
}
result.andExpect(jsonPath("$.status").value(HttpStatus.NOT_FOUND.value()));
@SneakyThrows
private ResultActions doPerformGet() {
return mockMvc.perform(get(TEST_ERROR_ENDPOINT));
}
}
@DisplayName("Functional Exception")
@Nested
class TestFunctionalException {
private final static String TEST_ERROR_ENDPOINT = TestErrorController.BASE_PATH + "/functional";
private final static int STATUS_VALUE = HttpStatus.BAD_REQUEST.value();
@SneakyThrows
private ResultActions doRequestUnknown() {
return mockMvc.perform(get("/api/unknown"));
@Test
void shouldReturnStatus() {
var result = doPerformGet();
result.andExpect(status().is(STATUS_VALUE));
}
@DisplayName("response body")
@Nested
class TestFunctionalExceptionBody {
@SneakyThrows
@Test
void shouldHaveStatus() {
var result = doPerformGet();
result.andExpect(jsonPath("$.status").value(STATUS_VALUE));
}
@SneakyThrows
@Test
void shouldHaveTitle() {
var result = doPerformGet();
result.andExpect(jsonPath("$.title").value("Bad Request"));
}
@DisplayName("detail")
@Nested
class TestBodyDetail {
@SneakyThrows
@Test
void shouldContainErrorMessage() {
var result = doPerformGet();
assertTrue(getDetailFromResponseContent(result).contains("Functional error: error message"));
}
@SneakyThrows
@Test
void shouldContainExceptionId() {
var result = doPerformGet();
assertTrue(getDetailFromResponseContent(result).contains("(ExceptionId: "));
}
}
@SneakyThrows
@Test
void shouldHaveInstance() {
var result = doPerformGet();
result.andExpect(jsonPath("$.instance").value(TEST_ERROR_ENDPOINT));
}
@SneakyThrows
void shouldHaveType() {
var result = doPerformGet();
result.andExpect(jsonPath("$.type").value("about:blank"));
}
}
@SneakyThrows
private ResultActions doPerformGet() {
return mockMvc.perform(get(TEST_ERROR_ENDPOINT));
}
}
@DisplayName("TechnicalException")
@Nested
class TestTechnicalException {
private final static String TEST_ERROR_ENDPOINT = TestErrorController.BASE_PATH + "/technical";
private final static int STATUS_VALUE = HttpStatus.INTERNAL_SERVER_ERROR.value();
@SneakyThrows
@Test
void shouldReturnStatus() {
var result = doPerformGet();
result.andExpect(status().is(STATUS_VALUE));
}
@DisplayName("response body")
@Nested
class TestTechnicalExceptionBody {
@SneakyThrows
@Test
void shouldHaveStatus() {
var result = doPerformGet();
result.andExpect(jsonPath("$.status").value(STATUS_VALUE));
}
@SneakyThrows
@Test
void shouldHaveTitle() {
var result = doPerformGet();
result.andExpect(jsonPath("$.title").value("Internal Server Error"));
}
@DisplayName("detail")
@Nested
class TestBodyDetail {
@SneakyThrows
@Test
void shouldContainErrorMessage() {
var result = doPerformGet();
assertTrue(getDetailFromResponseContent(result).contains("error message"));
}
@SneakyThrows
@Test
void shouldContainExceptionId() {
var result = doPerformGet();
assertTrue(getDetailFromResponseContent(result).contains("(ExceptionId: "));
}
}
@SneakyThrows
@Test
void shouldHaveInstance() {
var result = doPerformGet();
result.andExpect(jsonPath("$.instance").value(TEST_ERROR_ENDPOINT));
}
@SneakyThrows
void shouldHaveType() {
var result = doPerformGet();
result.andExpect(jsonPath("$.type").value("about:blank"));
}
}
@SneakyThrows
private ResultActions doPerformGet() {
return mockMvc.perform(get(TEST_ERROR_ENDPOINT));
}
}
@SneakyThrows
private String getDetailFromResponseContent(ResultActions resultActions) {
return JsonPath.read(resultActions.andReturn().getResponse().getContentAsString(), "$.detail");
}
}
@RestController
@RequestMapping(TestErrorController.BASE_PATH)
class TestErrorController {
public final static String BASE_PATH = "/api/test-error";
static final String ERROR_MESSAGE = "error message";
@GetMapping("/runtime")
String throwRuntimeException() throws Exception {
throw new RuntimeException(ERROR_MESSAGE);
}
@GetMapping("/access-denied")
String throwAccessException() throws Exception {
throw new AccessDeniedException(ERROR_MESSAGE);
}
@GetMapping("/resource-not-found")
String throwResourceNotFoundException() throws Exception {
throw new ResourceNotFoundException(ERROR_MESSAGE);
}
@GetMapping("/functional")
String throwFunctionalExceptionException() throws Exception {
throw new FunctionalException(() -> ERROR_MESSAGE);
}
@GetMapping("/technical")
String throwTechnicalExceptionException() throws Exception {
throw new TechnicalException(ERROR_MESSAGE);
}
}
\ No newline at end of file
/*
* Copyright (c) 2024. Das Land Schleswig-Holstein vertreten durch das Ministerium für Energiewende, Klimaschutz, Umwelt und Natur
* Zentrales IT-Management
*
* Lizenziert unter der EUPL, Version 1.2 oder - sobald
* diese von der Europäischen Kommission genehmigt wurden -
* Folgeversionen der EUPL ("Lizenz");
* Sie dürfen dieses Werk ausschließlich gemäß
* dieser Lizenz nutzen.
* Eine Kopie der Lizenz finden Sie hier:
*
* https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12
*
* Sofern nicht durch anwendbare Rechtsvorschriften
* gefordert oder in schriftlicher Form vereinbart, wird
* die unter der Lizenz verbreitete Software "so wie sie
* ist", OHNE JEGLICHE GEWÄHRLEISTUNG ODER BEDINGUNGEN -
* ausdrücklich oder stillschweigend - verbreitet.
* Die sprachspezifischen Genehmigungen und Beschränkungen
* unter der Lizenz sind dem Lizenztext zu entnehmen.
*/
package de.ozgcloud.admin.common.errorhandling;
import java.util.Collections;
import java.util.Map;
import jakarta.validation.ConstraintViolationException;
import org.springframework.data.rest.webmvc.ResourceNotFoundException;
import org.springframework.http.HttpStatus;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import de.ozgcloud.common.errorhandling.TechnicalException;
@RestController
@RequestMapping("/api/test-error")
class TestErrorController {
@FunctionalInterface
interface ExceptionProducer {
Exception produceException();
}
static final Map<Class<? extends Exception>, HttpStatus> STATUS_BY_EXCEPTION = Map.of(
RuntimeException.class, HttpStatus.INTERNAL_SERVER_ERROR,
AccessDeniedException.class, HttpStatus.FORBIDDEN,
ConstraintViolationException.class, HttpStatus.UNPROCESSABLE_ENTITY,
ResourceNotFoundException.class, HttpStatus.NOT_FOUND,
FunctionalException.class, HttpStatus.BAD_REQUEST,
TechnicalException.class, HttpStatus.INTERNAL_SERVER_ERROR);
static final String ERROR_MESSAGE = "error message";
static Map<Class<? extends Exception>, ExceptionProducer> EXCEPTION_PRODUCER = Map.of(
RuntimeException.class, () -> new RuntimeException(ERROR_MESSAGE),
AccessDeniedException.class, () -> new AccessDeniedException(ERROR_MESSAGE),
ConstraintViolationException.class, () -> new ConstraintViolationException(ERROR_MESSAGE, Collections.emptySet()),
ResourceNotFoundException.class, () -> new ResourceNotFoundException(ERROR_MESSAGE),
FunctionalException.class, () -> new FunctionalException(() -> ERROR_MESSAGE),
TechnicalException.class, () -> new TechnicalException(ERROR_MESSAGE));
@GetMapping
String throwException(@RequestParam String errorClassName) throws Exception {
throw EXCEPTION_PRODUCER.get(
Class.forName(errorClassName)).produceException();
}
}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment