Skip to content
Snippets Groups Projects
Commit 64ed3b6a authored by Jan Zickermann's avatar Jan Zickermann
Browse files

OZG-4717 OZG-4799 Add other exception types

parent e2b1fb36
No related branches found
No related tags found
No related merge requests found
...@@ -34,6 +34,14 @@ ...@@ -34,6 +34,14 @@
<groupId>org.springframework.boot</groupId> <groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId> <artifactId>spring-boot-starter-web</artifactId>
</dependency> </dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<!-- Dev --> <!-- Dev -->
<dependency> <dependency>
......
...@@ -19,11 +19,14 @@ ...@@ -19,11 +19,14 @@
* Die sprachspezifischen Genehmigungen und Beschränkungen * Die sprachspezifischen Genehmigungen und Beschränkungen
* unter der Lizenz sind dem Lizenztext zu entnehmen. * unter der Lizenz sind dem Lizenztext zu entnehmen.
*/ */
package de.ozgcloud.admin.web.error; package de.ozgcloud.admin.errorhandling;
import java.nio.file.AccessDeniedException;
import java.util.Map; import java.util.Map;
import jakarta.validation.ConstraintViolationException;
import org.springframework.http.HttpStatus;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.web.ErrorResponse; import org.springframework.web.ErrorResponse;
import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice; import org.springframework.web.bind.annotation.RestControllerAdvice;
...@@ -34,13 +37,26 @@ import de.ozgcloud.common.errorhandling.TechnicalException; ...@@ -34,13 +37,26 @@ import de.ozgcloud.common.errorhandling.TechnicalException;
@RestControllerAdvice @RestControllerAdvice
public class ExceptionTranslator extends ResponseEntityExceptionHandler { public class ExceptionTranslator extends ResponseEntityExceptionHandler {
@ExceptionHandler({ RuntimeException.class, AccessDeniedException.class, TechnicalException.class }) static final Map<Class<? extends Exception>, HttpStatus> EXCEPTION_STATUS = Map.of(
public ErrorResponse handleAccessDeniedException(Exception ex) { RuntimeException.class, HttpStatus.INTERNAL_SERVER_ERROR,
var errorType = Map.of( AccessDeniedException.class, HttpStatus.FORBIDDEN,
AccessDeniedException.class, ErrorType.AUTHORIZATION, ConstraintViolationException.class, HttpStatus.UNPROCESSABLE_ENTITY,
TechnicalException.class, ErrorType.TECHNICAL ResourceNotFoundException.class, HttpStatus.NOT_FOUND,
).getOrDefault(ex.getClass(), ErrorType.UNKNOWN); FunctionalException.class, HttpStatus.BAD_REQUEST,
TechnicalException.class, HttpStatus.INTERNAL_SERVER_ERROR
);
@ExceptionHandler({
RuntimeException.class,
AccessDeniedException.class,
ConstraintViolationException.class,
ResourceNotFoundException.class,
FunctionalException.class,
TechnicalException.class
})
public ErrorResponse handleRuntimeExceptionGeneric(RuntimeException ex) {
var errorStatus = EXCEPTION_STATUS.getOrDefault(ex.getClass(), HttpStatus.INTERNAL_SERVER_ERROR);
return ErrorResponse.builder(ex, errorType.getStatus(), ex.getLocalizedMessage()).build(); return ErrorResponse.builder(ex, errorStatus, ex.getLocalizedMessage()).build();
} }
} }
...@@ -19,38 +19,41 @@ ...@@ -19,38 +19,41 @@
* Die sprachspezifischen Genehmigungen und Beschränkungen * Die sprachspezifischen Genehmigungen und Beschränkungen
* unter der Lizenz sind dem Lizenztext zu entnehmen. * unter der Lizenz sind dem Lizenztext zu entnehmen.
*/ */
package de.ozgcloud.admin.service; package de.ozgcloud.admin.errorhandling;
import java.time.Instant; import java.io.Serial;
import java.util.Optional; import java.util.UUID;
import org.springframework.boot.info.BuildProperties; import de.ozgcloud.common.errorhandling.ExceptionUtil;
import org.springframework.stereotype.Service; import de.ozgcloud.common.errorhandling.FunctionalErrorCode;
import de.ozgcloud.common.errorhandling.IdentifiableException;
import lombok.AllArgsConstructor; public class FunctionalException extends RuntimeException implements IdentifiableException {
@Service @Serial
@AllArgsConstructor private static final long serialVersionUID = 1L;
public class BasicAppInfoService {
private Optional<BuildProperties> buildProperties;
public String getVersion() { private final FunctionalErrorCode errorCode;
return buildProperties.map(BuildProperties::getVersion).orElse("--"); private final String exceptionId;
}
public FunctionalException(FunctionalErrorCode errorCode) {
super("Functional error: " + errorCode.getErrorCode());
public Instant getBuildTime() { this.errorCode = errorCode;
return buildProperties.map(BuildProperties::getTime).orElse(Instant.now()); this.exceptionId = UUID.randomUUID().toString();
} }
public String getJavaVersion() { public String getErrorCode() {
return System.getProperty("java.version", "?"); return errorCode.getErrorCode();
} }
public String getBuildNumber() { @Override
return buildProperties.map(p -> p.get("number")).orElse("?"); public String getMessage() {
return ExceptionUtil.formatMessageWithExceptionId(super.getMessage(), exceptionId);
} }
public String getBuildUrl() { @Override
return buildProperties.map(p -> p.get("url")).orElse("?"); public String getExceptionId() {
return exceptionId;
} }
} }
...@@ -19,21 +19,26 @@ ...@@ -19,21 +19,26 @@
* Die sprachspezifischen Genehmigungen und Beschränkungen * Die sprachspezifischen Genehmigungen und Beschränkungen
* unter der Lizenz sind dem Lizenztext zu entnehmen. * unter der Lizenz sind dem Lizenztext zu entnehmen.
*/ */
package de.ozgcloud.admin.web.error; package de.ozgcloud.admin.errorhandling;
import org.springframework.http.HttpStatus; import java.io.Serial;
import lombok.AllArgsConstructor; import de.ozgcloud.common.errorhandling.FunctionalErrorCode;
import lombok.Getter; import lombok.Getter;
@AllArgsConstructor
@Getter @Getter
public enum ErrorType { public class ResourceNotFoundException extends FunctionalException {
UNKNOWN(HttpStatus.INTERNAL_SERVER_ERROR),
AUTHORIZATION(HttpStatus.UNAUTHORIZED),
FUNCTIONAL(HttpStatus.BAD_REQUEST),
TECHNICAL(HttpStatus.INTERNAL_SERVER_ERROR),
;
private final HttpStatus status; @Serial
private static final long serialVersionUID = 1L;
private static final String MESSAGE_TEMPLATE = "Resource '%s' with id '%s' not found.";
public ResourceNotFoundException(Class<?> resource, Object id) {
super(buildErrorCode(resource, id));
}
private static FunctionalErrorCode buildErrorCode(Class<?> resource, Object id) {
return () -> String.format(MESSAGE_TEMPLATE, resource.getSimpleName(), id.toString());
}
} }
/*
* 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.web.dto;
import java.time.Instant;
import lombok.Builder;
import lombok.Getter;
@Builder
@Getter
public class BasicAppInfo {
private String javaVersion;
private String buildVersion;
private Instant buildTime;
private String buildNumber;
private String buildUrl;
}
...@@ -19,82 +19,61 @@ ...@@ -19,82 +19,61 @@
* Die sprachspezifischen Genehmigungen und Beschränkungen * Die sprachspezifischen Genehmigungen und Beschränkungen
* unter der Lizenz sind dem Lizenztext zu entnehmen. * unter der Lizenz sind dem Lizenztext zu entnehmen.
*/ */
package de.ozgcloud.admin.web.error; package de.ozgcloud.admin.errorhandling;
import static org.mockito.Mockito.*; import static de.ozgcloud.admin.errorhandling.ExceptionTranslator.*;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
import java.nio.file.AccessDeniedException; import java.util.stream.Stream;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks; import org.junit.jupiter.params.ParameterizedTest;
import org.mockito.Mock; import org.junit.jupiter.params.provider.Arguments;
import org.mockito.Spy; import org.junit.jupiter.params.provider.MethodSource;
import org.springframework.boot.test.context.SpringBootTest; import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.http.HttpStatus;
import org.springframework.test.web.servlet.MockMvc; 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.test.web.servlet.setup.MockMvcBuilders;
import de.ozgcloud.admin.service.BasicAppInfoService; import lombok.SneakyThrows;
import de.ozgcloud.admin.web.controller.BasicAppInfoController;
import de.ozgcloud.common.errorhandling.TechnicalException;
@SpringBootTest @ExtendWith(MockitoExtension.class)
class ExceptionTranslatorITCase { class ExceptionTranslatorITCase {
@Spy
@InjectMocks
private BasicAppInfoController basicAppInfoController;
@Mock
private BasicAppInfoService basicAppInfoService;
private MockMvc mockMvc; private MockMvc mockMvc;
@BeforeEach @BeforeEach
void mock() { void setup() {
mockMvc = MockMvcBuilders mockMvc = MockMvcBuilders
.standaloneSetup(basicAppInfoController) .standaloneSetup(new TestErrorController())
.setControllerAdvice(new ExceptionTranslator()) .setControllerAdvice(new ExceptionTranslator()).build();
.build();
} }
@DisplayName("Error handler") @DisplayName("Error handler")
@Nested @Nested
class TestErrorHandler { class TestErrorHandler {
private void throwWhenApiCalled(Exception ex) { private static Stream<Arguments> exceptionAndExpectedStatus() {
when(basicAppInfoService.getJavaVersion()) return EXCEPTION_STATUS.entrySet().stream().map(kv -> Arguments.of(kv.getKey(), kv.getValue()));
.thenAnswer(invocation -> {
throw ex;
});
}
@Test
void shouldHandleAccessDenied() throws Exception {
throwWhenApiCalled(new AccessDeniedException("test no access"));
mockMvc.perform(get("/api"))
.andExpect(status().is(ErrorType.AUTHORIZATION.getStatus().value()));
} }
@Test @ParameterizedTest
void shouldHandleTechnicalError() throws Exception { @MethodSource("exceptionAndExpectedStatus")
throwWhenApiCalled(new TechnicalException("test technical error")); @SneakyThrows
void shouldHandleExceptionWithStatus(Class<? extends Exception> exceptionClass, HttpStatus expectedStatus) {
var result = doPerformWithError(exceptionClass);
mockMvc.perform(get("/api")) result.andExpect(status().is(expectedStatus.value()));
.andExpect(status().is(ErrorType.TECHNICAL.getStatus().value()));
} }
@Test @SneakyThrows
void shouldRuntimeError() throws Exception { private ResultActions doPerformWithError(Class<? extends Exception> exceptionClass) {
throwWhenApiCalled(new RuntimeException("test unknown error")); return mockMvc.perform(get("/test-error").param("errorClassName", exceptionClass.getName()));
mockMvc.perform(get("/api"))
.andExpect(status().is(ErrorType.UNKNOWN.getStatus().value()));
} }
} }
} }
...@@ -19,37 +19,51 @@ ...@@ -19,37 +19,51 @@
* Die sprachspezifischen Genehmigungen und Beschränkungen * Die sprachspezifischen Genehmigungen und Beschränkungen
* unter der Lizenz sind dem Lizenztext zu entnehmen. * unter der Lizenz sind dem Lizenztext zu entnehmen.
*/ */
package de.ozgcloud.admin.web.controller; package de.ozgcloud.admin.errorhandling;
import static org.junit.Assert.*;
import java.util.Collections;
import java.util.Map;
import java.util.Objects;
import jakarta.validation.ConstraintViolationException;
import org.springframework.data.repository.query.Param;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
import de.ozgcloud.admin.service.BasicAppInfoService; import de.ozgcloud.common.errorhandling.TechnicalException;
import de.ozgcloud.admin.web.dto.BasicAppInfo;
import lombok.AllArgsConstructor;
@RestController @RestController
@AllArgsConstructor @RequestMapping("/test-error")
@RequestMapping(BasicAppInfoController.PATH) public class TestErrorController {
public class BasicAppInfoController {
static final String PATH = "/api"; // NOSONAR @FunctionalInterface
interface ExceptionProducer {
Exception produceException();
}
private BasicAppInfoService basicAppInfoService; static Map<Class<? extends Exception>, ExceptionProducer> EXCEPTION_PRODUCER = Map.of(
RuntimeException.class, () -> new RuntimeException("x"),
AccessDeniedException.class, () -> new AccessDeniedException("x"),
ConstraintViolationException.class, () -> new ConstraintViolationException("x", Collections.emptySet()),
ResourceNotFoundException.class, () -> new ResourceNotFoundException(Object.class, "x"),
FunctionalException.class, () -> new FunctionalException(() -> "x"),
TechnicalException.class, () -> new TechnicalException("x")
);
@GetMapping @GetMapping
public BasicAppInfo getInfo() { String throwException(@Param("error") String errorClassName) throws Exception {
return buildInfo(); Objects.requireNonNull(errorClassName);
}
private BasicAppInfo buildInfo() { var clazz = Class.forName(errorClassName);
return BasicAppInfo.builder()
.javaVersion(basicAppInfoService.getJavaVersion())
.buildTime(basicAppInfoService.getBuildTime())
.buildVersion(basicAppInfoService.getVersion())
.buildNumber(basicAppInfoService.getBuildNumber())
.buildUrl(basicAppInfoService.getBuildUrl())
.build();
}
var exception = EXCEPTION_PRODUCER.get(clazz).produceException();
assertEquals(clazz, exception.getClass());
throw exception;
} }
}
/*
* 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.web;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
@SpringBootTest
class AdministrationApplicationTest {
@Test
void shouldSpringContextLoad() { // NOSONAR
}
}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment