Skip to content

Commit

Permalink
FAIRSPC-81: tuned global exception handling
Browse files Browse the repository at this point in the history
  • Loading branch information
tgreenwood committed Oct 11, 2024
1 parent 17e7514 commit 921ade7
Show file tree
Hide file tree
Showing 7 changed files with 184 additions and 63 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package io.fairspace.saturn.controller.dto;

import ioinformarics.oss.jackson.module.jsonld.annotation.JsonldProperty;
import ioinformarics.oss.jackson.module.jsonld.annotation.JsonldType;

import io.fairspace.saturn.vocabulary.FS;

@JsonldType(FS.ERROR_URI)
public record ErrorDto(
@JsonldProperty(FS.ERROR_STATUS_URI) int status,
@JsonldProperty(FS.ERROR_MESSAGE_URI) String message,
@JsonldProperty(FS.ERROR_DETAILS_URI) Object details) {}
Original file line number Diff line number Diff line change
@@ -1,52 +1,48 @@
package io.fairspace.saturn.controller.exception;

import java.util.stream.Collectors;

import jakarta.servlet.http.HttpServletRequest;
import jakarta.validation.ConstraintViolation;
import jakarta.validation.ConstraintViolationException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;

import io.fairspace.saturn.services.PayloadParsingException;
import io.fairspace.saturn.services.errors.ErrorDto;
import io.fairspace.saturn.controller.dto.ErrorDto;
import io.fairspace.saturn.services.metadata.validation.ValidationException;

@Slf4j
@ControllerAdvice
public class GlobalExceptionHandler {

// // todo: add tests
// @ExceptionHandler(AccessDeniedException.class)
// public ResponseEntity<String> handleAccessDenied(AccessDeniedException ex) {
// return new ResponseEntity<>("Access Denied: " + ex.getMessage(), HttpStatus.FORBIDDEN);
// }

@ExceptionHandler(PayloadParsingException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public ResponseEntity<ErrorDto> handlePayloadParsingException(PayloadParsingException ex, HttpServletRequest req) {
log.error("Malformed request body for request {} {}", req.getMethod(), req.getRequestURI(), ex);
return buildErrorResponse(HttpStatus.BAD_REQUEST, "Malformed request body");
@ExceptionHandler(ConstraintViolationException.class)
public ResponseEntity<ErrorDto> handleConstraintViolationException(
ConstraintViolationException ex, HttpServletRequest req) {
var violations = ex.getConstraintViolations().stream()
.map(ConstraintViolation::getMessage)
.sorted()
.collect(Collectors.joining("; "));
return buildErrorResponse(HttpStatus.BAD_REQUEST, "Validation Error", "Violations: " + violations);
}

@ExceptionHandler(ValidationException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public ResponseEntity<ErrorDto> handleValidationException(ValidationException ex, HttpServletRequest req) {
log.error("Validation error for request {} {}", req.getMethod(), req.getRequestURI(), ex);
return buildErrorResponse(HttpStatus.BAD_REQUEST, "Validation Error", ex.getViolations());
}

@ExceptionHandler(IllegalArgumentException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public ResponseEntity<ErrorDto> handleIllegalArgumentException(
IllegalArgumentException ex, HttpServletRequest req) {
log.error("Validation error for request {} {}", req.getMethod(), req.getRequestURI(), ex);
return buildErrorResponse(HttpStatus.BAD_REQUEST, "Validation Error", ex.getMessage());
}

@ExceptionHandler(AccessDeniedException.class)
@ResponseStatus(HttpStatus.FORBIDDEN)
public ResponseEntity<ErrorDto> handleAccessDeniedException(AccessDeniedException ex, HttpServletRequest req) {
log.error("Access denied for request {} {}", req.getMethod(), req.getRequestURI(), ex);
return buildErrorResponse(HttpStatus.FORBIDDEN, "Access Denied");
Expand Down

This file was deleted.

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@

@ImportAutoConfiguration(exclude = {SecurityAutoConfiguration.class, OAuth2ResourceServerAutoConfiguration.class})
@Import(BaseControllerTest.CustomObjectMapperConfig.class)
class BaseControllerTest {
public class BaseControllerTest {

@MockBean
private JwtAuthConverterProperties jwtAuthConverterProperties;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
package io.fairspace.saturn.controller.exception;

import java.util.Set;

import jakarta.validation.ConstraintViolation;
import jakarta.validation.ConstraintViolationException;
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.http.MediaType;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.test.web.servlet.MockMvc;

import io.fairspace.saturn.controller.BaseControllerTest;
import io.fairspace.saturn.services.metadata.validation.ValidationException;
import io.fairspace.saturn.services.metadata.validation.Violation;

import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.when;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

@WebMvcTest({GlobalExceptionHandler.class, TestController.class})
public class GlobalExceptionHandlerTest extends BaseControllerTest {

@Autowired
private MockMvc mockMvc;

@MockBean
private TestController.TestInnerClass testInnerClass;

@Test
public void testHandleConstraintViolationException() throws Exception {
// Mocking a ConstraintViolationException with a couple of violations
ConstraintViolation<?> violation1 = Mockito.mock(ConstraintViolation.class);
ConstraintViolation<?> violation2 = Mockito.mock(ConstraintViolation.class);
when(violation1.getMessage()).thenReturn("Violation 1");
when(violation2.getMessage()).thenReturn("Violation 2");
Set<ConstraintViolation<?>> violations = Set.of(violation1, violation2);
ConstraintViolationException exception = new ConstraintViolationException(violations);

doThrow(exception).when(testInnerClass).method(); // Simulating the exception

mockMvc.perform(get("/test"))
.andExpect(status().isBadRequest())
.andExpect(content().contentType(MediaType.APPLICATION_JSON))
.andExpect(
content()
.json(
"""
{
"status": 400,
"message": "Validation Error",
"details": "Violations: Violation 1; Violation 2"
}
"""));
}

@Test
public void testHandleValidationException() throws Exception {
// Mocking a ValidationException with a violation
Set<Violation> violations = Set.of(new Violation("Invalid value", "subject", "predicate", "value"));
ValidationException exception = new ValidationException(violations);

doThrow(exception).when(testInnerClass).method(); // Simulating the exception

mockMvc.perform(get("/test"))
.andExpect(status().isBadRequest())
.andExpect(content().contentType(MediaType.APPLICATION_JSON))
.andExpect(
content()
.json(
"""
{
"status": 400,
"message": "Validation Error",
"details": [
{
"message": "Invalid value",
"subject": "subject",
"predicate": "predicate",
"value": "value"
}
]
}
"""));
}

@Test
public void testHandleIllegalArgumentException() throws Exception {
// Mocking an IllegalArgumentException
IllegalArgumentException exception = new IllegalArgumentException("Invalid argument");

doThrow(exception).when(testInnerClass).method(); // Simulating the exception

mockMvc.perform(get("/test"))
.andExpect(status().isBadRequest())
.andExpect(content().contentType(MediaType.APPLICATION_JSON))
.andExpect(
content()
.json(
"""
{
"status": 400,
"message": "Validation Error",
"details": "Invalid argument"
}
"""));
}

@Test
public void testHandleAccessDeniedException() throws Exception {
// Mocking an AccessDeniedException
AccessDeniedException exception = new AccessDeniedException("Access denied");

doThrow(exception).when(testInnerClass).method(); // Simulating the exception

mockMvc.perform(get("/test"))
.andExpect(status().isForbidden())
.andExpect(content().contentType(MediaType.APPLICATION_JSON))
.andExpect(
content()
.json(
"""
{
"status": 403,
"message": "Access Denied",
"details": null
}
"""));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package io.fairspace.saturn.controller.exception;

import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequiredArgsConstructor
public class TestController {

private final TestInnerClass testInnerClass;

@GetMapping("/test")
public void testMethod() {
testInnerClass.method();
}

@Component
public static class TestInnerClass {
public void method() {}
}
}

0 comments on commit 921ade7

Please sign in to comment.