diff --git a/Jenkinsfile b/Jenkinsfile index 29d72c6ab83a5b3d97f1bd01d6ada52ad2dc0f2a..a4c66b8630e7fc3fe05d4887c3a78b333740cbaa 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -48,7 +48,7 @@ pipeline { } configFileProvider([configFile(fileId: 'maven-settings', variable: 'MAVEN_SETTINGS')]) { - sh "mvn -s $MAVEN_SETTINGS clean install -Dmaven.wagon.http.retryHandler.count=3 -DelasticTests.disabled=true -Dbuild.number=$BUILD_NUMBER" + sh 'mvn -s $MAVEN_SETTINGS clean install -Dmaven.wagon.http.retryHandler.count=3 -DelasticTests.disabled=true -Dbuild.number=$BUILD_NUMBER' } } } @@ -116,7 +116,7 @@ pipeline { configFileProvider([configFile(fileId: 'maven-settings', variable: 'MAVEN_SETTINGS')]) { withCredentials([usernamePassword(credentialsId: 'jenkins-nexus-login', usernameVariable: 'USER', passwordVariable: 'PASSWORD')]) { - sh 'mvn -s $MAVEN_SETTINGS spring-boot:build-image -DskipTests -Dmaven.wagon.http.retryHandler.count=3 $BUILD_PROFILE -Ddocker.publishRegistry.username=${USER} -Ddocker.publishRegistry.password=${PASSWORD}' + sh 'mvn -s $MAVEN_SETTINGS spring-boot:build-image -DskipTests -Dmaven.wagon.http.retryHandler.count=3 $BUILD_PROFILE -Ddocker.publishRegistry.username=${USER} -Ddocker.publishRegistry.password=${PASSWORD} -Dbuild.number=$BUILD_NUMBER' } } } diff --git a/src/main/java/de/ozgcloud/admin/RootModelAssembler.java b/src/main/java/de/ozgcloud/admin/RootModelAssembler.java index cd66647d10b09a38b229b1931cf2e65bbef5733a..2cf01448f6fa1ab4f52d3a72fc06b15d57c5cda7 100644 --- a/src/main/java/de/ozgcloud/admin/RootModelAssembler.java +++ b/src/main/java/de/ozgcloud/admin/RootModelAssembler.java @@ -28,13 +28,11 @@ import org.springframework.hateoas.server.RepresentationModelAssembler; import org.springframework.hateoas.server.mvc.WebMvcLinkBuilder; import org.springframework.stereotype.Component; -import io.micrometer.common.lang.NonNullApi; import lombok.RequiredArgsConstructor; @Component @RequiredArgsConstructor -@NonNullApi -class RootModelAssembler implements RepresentationModelAssembler<Root, EntityModel<Root>> { +public class RootModelAssembler implements RepresentationModelAssembler<Root, EntityModel<Root>> { static final String REL_CONFIGURATION = "configuration"; private final RepositoryRestProperties restProperties; @@ -46,7 +44,6 @@ class RootModelAssembler implements RepresentationModelAssembler<Root, EntityMod return EntityModel.of( root, Link.of(configLink.toUriString(), REL_CONFIGURATION), - rootLink.withSelfRel() - ); + rootLink.withSelfRel()); } } diff --git a/src/main/java/de/ozgcloud/admin/errorhandling/AdminExceptionHandler.java b/src/main/java/de/ozgcloud/admin/common/errorhandling/ExceptionController.java similarity index 95% rename from src/main/java/de/ozgcloud/admin/errorhandling/AdminExceptionHandler.java rename to src/main/java/de/ozgcloud/admin/common/errorhandling/ExceptionController.java index 27f9c626b4586717558355e5a2671676ec0e570d..75a3dda5ce10f4d50ebaaabc31ec840ac9c54d3b 100644 --- a/src/main/java/de/ozgcloud/admin/errorhandling/AdminExceptionHandler.java +++ b/src/main/java/de/ozgcloud/admin/common/errorhandling/ExceptionController.java @@ -19,7 +19,7 @@ * Die sprachspezifischen Genehmigungen und Beschränkungen * unter der Lizenz sind dem Lizenztext zu entnehmen. */ -package de.ozgcloud.admin.errorhandling; +package de.ozgcloud.admin.common.errorhandling; import java.util.Map; @@ -36,7 +36,7 @@ import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExcep import de.ozgcloud.common.errorhandling.TechnicalException; @RestControllerAdvice -public class AdminExceptionHandler extends ResponseEntityExceptionHandler { +public class ExceptionController extends ResponseEntityExceptionHandler { static final Map<Class<? extends Exception>, HttpStatus> STATUS_BY_EXCEPTION = Map.of( RuntimeException.class, HttpStatus.INTERNAL_SERVER_ERROR, diff --git a/src/main/java/de/ozgcloud/admin/errorhandling/FunctionalException.java b/src/main/java/de/ozgcloud/admin/common/errorhandling/FunctionalException.java similarity index 97% rename from src/main/java/de/ozgcloud/admin/errorhandling/FunctionalException.java rename to src/main/java/de/ozgcloud/admin/common/errorhandling/FunctionalException.java index e0e57b3feb65073674c0645aac9fc997af8dbfed..5b2369b2a97772a1607b84ebaa9a46de2fec9643 100644 --- a/src/main/java/de/ozgcloud/admin/errorhandling/FunctionalException.java +++ b/src/main/java/de/ozgcloud/admin/common/errorhandling/FunctionalException.java @@ -19,7 +19,7 @@ * Die sprachspezifischen Genehmigungen und Beschränkungen * unter der Lizenz sind dem Lizenztext zu entnehmen. */ -package de.ozgcloud.admin.errorhandling; +package de.ozgcloud.admin.common.errorhandling; import java.io.Serial; import java.util.UUID; diff --git a/src/main/java/de/ozgcloud/admin/environment/FrontendEnvironment.java b/src/main/java/de/ozgcloud/admin/environment/Environment.java similarity index 87% rename from src/main/java/de/ozgcloud/admin/environment/FrontendEnvironment.java rename to src/main/java/de/ozgcloud/admin/environment/Environment.java index 0f7ca897f9e77c5956cdcab164977ea35d694476..e2bfb54b4aeb2d33da4461d3eb6ae95d89eab93e 100644 --- a/src/main/java/de/ozgcloud/admin/environment/FrontendEnvironment.java +++ b/src/main/java/de/ozgcloud/admin/environment/Environment.java @@ -5,7 +5,7 @@ import lombok.Getter; @Getter @Builder -public class FrontendEnvironment { +public class Environment { private boolean production; private String remoteHost; private String authServer; diff --git a/src/main/java/de/ozgcloud/admin/environment/FrontendEnvironmentController.java b/src/main/java/de/ozgcloud/admin/environment/EnvironmentController.java similarity index 81% rename from src/main/java/de/ozgcloud/admin/environment/FrontendEnvironmentController.java rename to src/main/java/de/ozgcloud/admin/environment/EnvironmentController.java index b61f1d9c358751a1e974be75b9cc33b3f2551536..71cf34ce3180c793235693f7ec8582a06aa27c63 100644 --- a/src/main/java/de/ozgcloud/admin/environment/FrontendEnvironmentController.java +++ b/src/main/java/de/ozgcloud/admin/environment/EnvironmentController.java @@ -9,10 +9,10 @@ import org.springframework.web.bind.annotation.RestController; import de.ozgcloud.admin.RootController; import lombok.RequiredArgsConstructor; -@RestController +@RestController("ozgCloudEnvironmentController") @RequiredArgsConstructor -@RequestMapping(FrontendEnvironmentController.PATH) -public class FrontendEnvironmentController { +@RequestMapping(EnvironmentController.PATH) +public class EnvironmentController { static final String PATH = "/api/environment"; // NOSONAR @@ -21,8 +21,8 @@ public class FrontendEnvironmentController { private final OAuth2Properties oAuthProperties; @GetMapping - public FrontendEnvironment getEnvironment() { - return FrontendEnvironment.builder() + public Environment getEnvironment() { + return Environment.builder() .production(environmentProperties.isProduction()) .remoteHost(linkTo(RootController.class).toUri().toString()) .authServer(oAuthProperties.getAuthServerUrl()) diff --git a/src/test/java/de/ozgcloud/admin/common/errorhandling/ExceptionControllerITCase.java b/src/test/java/de/ozgcloud/admin/common/errorhandling/ExceptionControllerITCase.java new file mode 100644 index 0000000000000000000000000000000000000000..b4884dd0e4b23e908645251677c76064c9086e8a --- /dev/null +++ b/src/test/java/de/ozgcloud/admin/common/errorhandling/ExceptionControllerITCase.java @@ -0,0 +1,111 @@ +package de.ozgcloud.admin.common.errorhandling; + +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; +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; +import jakarta.validation.Validation; +import jakarta.validation.Validator; +import jakarta.validation.constraints.NotEmpty; + +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; + +import de.ozgcloud.admin.RootController; +import de.ozgcloud.admin.RootModelAssembler; +import de.ozgcloud.common.test.ITCase; +import lombok.Builder; +import lombok.Getter; +import lombok.SneakyThrows; + +@ITCase +@AutoConfigureMockMvc +@WithMockUser +class ExceptionControllerITCase { + + @Autowired + private MockMvc mockMvc; + + @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 ExceptionController.STATUS_BY_EXCEPTION.entrySet().stream().map(kv -> Arguments.of(kv.getKey(), kv.getValue())); + } + + } + + @Nested + class TestConstraintViolationException { + @Test + @SneakyThrows + void shouldHaveValidationMessage() { + when(modelAssembler.toModel(any())).thenAnswer((a) -> { + throw new ConstraintViolationException(getConstraintViolations()); + }); + + var result = performGet(); + + result.andExpect(jsonPath("$.detail").value("string: Empty field")); + } + + private Set<ConstraintViolation<ValidatedClass>> getConstraintViolations() { + Validator validator = Validation.buildDefaultValidatorFactory().getValidator(); + return validator.validate(ValidatedClass.builder().build()); + } + + @Getter + @Builder + private static class ValidatedClass { + @NotEmpty(message = "Empty field") + private String string; + } + + } + + @SneakyThrows + private ResultActions performGet() { + return mockMvc.perform(get(RootController.PATH)); + } + +} diff --git a/src/test/java/de/ozgcloud/admin/errorhandling/AdminExceptionHandlerITCase.java b/src/test/java/de/ozgcloud/admin/common/errorhandling/ExceptionControllerTest.java similarity index 69% rename from src/test/java/de/ozgcloud/admin/errorhandling/AdminExceptionHandlerITCase.java rename to src/test/java/de/ozgcloud/admin/common/errorhandling/ExceptionControllerTest.java index 646d2c0b24579a6d7cfd502fbbdf58cfc530806a..f33a3715b32778a78b725e096ea0bf99e1fd4047 100644 --- a/src/test/java/de/ozgcloud/admin/errorhandling/AdminExceptionHandlerITCase.java +++ b/src/test/java/de/ozgcloud/admin/common/errorhandling/ExceptionControllerTest.java @@ -19,9 +19,9 @@ * Die sprachspezifischen Genehmigungen und Beschränkungen * unter der Lizenz sind dem Lizenztext zu entnehmen. */ -package de.ozgcloud.admin.errorhandling; +package de.ozgcloud.admin.common.errorhandling; -import static de.ozgcloud.admin.errorhandling.AdminExceptionHandler.*; +import static de.ozgcloud.admin.common.errorhandling.ExceptionController.*; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; @@ -41,7 +41,7 @@ import org.springframework.test.web.servlet.setup.MockMvcBuilders; import lombok.SneakyThrows; -class AdminExceptionHandlerITCase { +class ExceptionControllerTest { private MockMvc mockMvc; @@ -49,7 +49,7 @@ class AdminExceptionHandlerITCase { void setup() { mockMvc = MockMvcBuilders .standaloneSetup(new TestErrorController()) - .setControllerAdvice(new AdminExceptionHandler()).build(); + .setControllerAdvice(new ExceptionController()).build(); } @DisplayName("Error handler") @@ -74,13 +74,49 @@ class AdminExceptionHandlerITCase { result.andExpect(jsonPath("$.status").value(expectedStatus.value())); } + @ParameterizedTest + @MethodSource("exceptionAndExpectedStatus") + @SneakyThrows + void shouldRespondWithTitler(Class<? extends Exception> exceptionClass, HttpStatus expectedStatus) { + var result = doPerformWithError(exceptionClass); + + result.andExpect(jsonPath("$.title").exists()); + } + + @ParameterizedTest + @MethodSource("exceptionAndExpectedStatus") + @SneakyThrows + void shouldRespondWithDetail(Class<? extends Exception> exceptionClass, HttpStatus expectedStatus) { + var result = doPerformWithError(exceptionClass); + + result.andExpect(jsonPath("$.detail").exists()); + } + + @ParameterizedTest + @MethodSource("exceptionAndExpectedStatus") + @SneakyThrows + void shouldRespondWithInstance(Class<? extends Exception> exceptionClass, HttpStatus expectedStatus) { + var result = doPerformWithError(exceptionClass); + + result.andExpect(jsonPath("$.instance").exists()); + } + + @ParameterizedTest + @MethodSource("exceptionAndExpectedStatus") + @SneakyThrows + void shouldRespondWithType(Class<? extends Exception> exceptionClass, HttpStatus expectedStatus) { + var result = doPerformWithError(exceptionClass); + + result.andExpect(jsonPath("$.type").exists()); + } + private static Stream<Arguments> exceptionAndExpectedStatus() { return STATUS_BY_EXCEPTION.entrySet().stream().map(kv -> Arguments.of(kv.getKey(), kv.getValue())); } @SneakyThrows private ResultActions doPerformWithError(Class<? extends Exception> exceptionClass) { - return mockMvc.perform(get("/test-error").param("errorClassName", exceptionClass.getName())); + return mockMvc.perform(get("/api/test-error").param("errorClassName", exceptionClass.getName())); } } diff --git a/src/test/java/de/ozgcloud/admin/errorhandling/TestErrorController.java b/src/test/java/de/ozgcloud/admin/common/errorhandling/TestErrorController.java similarity index 92% rename from src/test/java/de/ozgcloud/admin/errorhandling/TestErrorController.java rename to src/test/java/de/ozgcloud/admin/common/errorhandling/TestErrorController.java index 4ad218f842cdc6e96a0c3b563e51aad7c9cdbeaf..c10446041aaba51ac761ac4e91ec01d6f9b7bb4b 100644 --- a/src/test/java/de/ozgcloud/admin/errorhandling/TestErrorController.java +++ b/src/test/java/de/ozgcloud/admin/common/errorhandling/TestErrorController.java @@ -19,7 +19,7 @@ * Die sprachspezifischen Genehmigungen und Beschränkungen * unter der Lizenz sind dem Lizenztext zu entnehmen. */ -package de.ozgcloud.admin.errorhandling; +package de.ozgcloud.admin.common.errorhandling; import java.util.Collections; import java.util.Map; @@ -34,11 +34,9 @@ import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import de.ozgcloud.common.errorhandling.TechnicalException; -import io.micrometer.common.lang.NonNullApi; @RestController -@RequestMapping("/test-error") -@NonNullApi +@RequestMapping("/api/test-error") class TestErrorController { @FunctionalInterface @@ -54,13 +52,12 @@ class TestErrorController { 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) - ); + TechnicalException.class, () -> new TechnicalException(ERROR_MESSAGE)); @GetMapping String throwException(@RequestParam String errorClassName) throws Exception { throw EXCEPTION_PRODUCER.get( - Class.forName(errorClassName) - ).produceException(); + Class.forName(errorClassName)).produceException(); } + } diff --git a/src/test/java/de/ozgcloud/admin/environment/FrontendEnvironmentControllerTest.java b/src/test/java/de/ozgcloud/admin/environment/EnvironmentControllerTest.java similarity index 94% rename from src/test/java/de/ozgcloud/admin/environment/FrontendEnvironmentControllerTest.java rename to src/test/java/de/ozgcloud/admin/environment/EnvironmentControllerTest.java index 67d81f084a8a4bea8e747713cbb205c9d9433ca3..bdeaf83b00de2410112833f1447e940fc088e9b5 100644 --- a/src/test/java/de/ozgcloud/admin/environment/FrontendEnvironmentControllerTest.java +++ b/src/test/java/de/ozgcloud/admin/environment/EnvironmentControllerTest.java @@ -22,11 +22,11 @@ import de.ozgcloud.admin.RootController; import lombok.SneakyThrows; @ExtendWith(MockitoExtension.class) -class FrontendEnvironmentControllerTest { +class EnvironmentControllerTest { @Spy @InjectMocks - private FrontendEnvironmentController controller; + private EnvironmentController controller; @Mock private ProductionProperties environmentProperties; @@ -120,7 +120,7 @@ class FrontendEnvironmentControllerTest { @SneakyThrows private ResultActions doRequest() { - return mockMvc.perform(get(FrontendEnvironmentController.PATH)); + return mockMvc.perform(get(EnvironmentController.PATH)); } }