src/main/java/net/hostsharing/hsadminng/config/JsonObjectMapperConfiguration.java
@@ -1,5 +1,6 @@ package net.hostsharing.hsadminng.config; import com.fasterxml.jackson.core.JsonParser; import com.fasterxml.jackson.databind.SerializationFeature; import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; import org.openapitools.jackson.nullable.JsonNullableModule; @@ -16,6 +17,7 @@ public Jackson2ObjectMapperBuilder customObjectMapper() { return new Jackson2ObjectMapperBuilder() .modules(new JsonNullableModule(), new JavaTimeModule()) .featuresToEnable(JsonParser.Feature.ALLOW_COMMENTS) .featuresToDisable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS); } } src/main/java/net/hostsharing/hsadminng/errors/CustomErrorResponse.java
New file @@ -0,0 +1,51 @@ package net.hostsharing.hsadminng.errors; import com.fasterxml.jackson.annotation.JsonFormat; import lombok.Getter; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.context.request.WebRequest; import java.time.LocalDateTime; @Getter class CustomErrorResponse { static ResponseEntity<CustomErrorResponse> errorResponse( final WebRequest request, final HttpStatus httpStatus, final String message) { return new ResponseEntity<>( new CustomErrorResponse(request.getContextPath(), httpStatus, message), httpStatus); } static String firstMessageLine(final Throwable exception) { if (exception.getMessage() != null) { return firstLine(exception.getMessage()); } return "ERROR: [500] " + exception.getClass().getName(); } static String firstLine(final String message) { return message.split("\\r|\\n|\\r\\n", 0)[0]; } @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd hh:mm:ss") private final LocalDateTime timestamp; private final String path; private final int status; private final String error; private final String message; CustomErrorResponse(final String path, final HttpStatus status, final String message) { this.timestamp = LocalDateTime.now(); this.path = path; this.status = status.value(); this.error = status.getReasonPhrase(); this.message = message; } } src/main/java/net/hostsharing/hsadminng/errors/RestResponseEntityExceptionHandler.java
@@ -1,13 +1,12 @@ package net.hostsharing.hsadminng.errors; import com.fasterxml.jackson.annotation.JsonFormat; import lombok.Getter; import org.iban4j.Iban4jException; import org.springframework.core.NestedExceptionUtils; import org.springframework.dao.DataIntegrityViolationException; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.lang.Nullable; import org.springframework.orm.jpa.JpaObjectRetrievalFailureException; import org.springframework.orm.jpa.JpaSystemException; import org.springframework.web.bind.MethodArgumentNotValidException; @@ -17,10 +16,11 @@ import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler; import javax.persistence.EntityNotFoundException; import java.time.LocalDateTime; import java.util.NoSuchElementException; import java.util.Optional; import java.util.regex.Pattern; import static net.hostsharing.hsadminng.errors.CustomErrorResponse.*; @ControllerAdvice public class RestResponseEntityExceptionHandler @@ -72,6 +72,16 @@ } @Override @SuppressWarnings("unchecked,rawtypes") protected ResponseEntity handleExceptionInternal( Exception exc, @Nullable Object body, HttpHeaders headers, HttpStatus status, WebRequest request) { final var response = super.handleExceptionInternal(exc, body, headers, status, request); return errorResponse(request, response.getStatusCode(), Optional.ofNullable(response.getBody()).map(Object::toString).orElse(firstMessageLine(exc))); } //@ExceptionHandler({ MethodArgumentNotValidException.class }) @SuppressWarnings("unchecked,rawtypes") protected ResponseEntity handleMethodArgumentNotValid( MethodArgumentNotValidException exc, @@ -126,45 +136,4 @@ return Optional.empty(); } private static ResponseEntity<CustomErrorResponse> errorResponse( final WebRequest request, final HttpStatus httpStatus, final String message) { return new ResponseEntity<>( new CustomErrorResponse(request.getContextPath(), httpStatus, message), httpStatus); } private String firstMessageLine(final Throwable exception) { if (exception.getMessage() != null) { return firstLine(exception.getMessage()); } return "ERROR: [500] " + exception.getClass().getName(); } private String firstLine(final String message) { return message.split("\\r|\\n|\\r\\n", 0)[0]; } } @Getter class CustomErrorResponse { @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd hh:mm:ss") private final LocalDateTime timestamp; private final String path; private final int status; private final String error; private final String message; public CustomErrorResponse(final String path, final HttpStatus status, final String message) { this.timestamp = LocalDateTime.now(); this.path = path; this.status = status.value(); this.error = status.getReasonPhrase(); this.message = message; } } src/test/java/net/hostsharing/hsadminng/hs/office/bankaccount/HsOfficeBankAccountControllerAcceptanceTest.java
@@ -136,7 +136,7 @@ .port(port) .when() .post("http://localhost/api/hs/office/bankaccounts") .then().assertThat() .then().log().all().assertThat() .statusCode(201) .contentType(ContentType.JSON) .body("uuid", isUuidValid()) @@ -346,7 +346,10 @@ jpaAttempt.transacted(() -> { context.define("superuser-alex@hostsharing.net", null); tempBankAccountUuids.addAll( bankAccountRepo.findByOptionalHolderLike("some temp acc").stream().map(HsOfficeBankAccountEntity::getUuid).toList() bankAccountRepo.findByOptionalHolderLike("some temp acc") .stream() .map(HsOfficeBankAccountEntity::getUuid) .toList() ); }); tempBankAccountUuids.forEach(uuid -> { src/test/java/net/hostsharing/hsadminng/test/cust/TestCustomerControllerAcceptanceTest.java
@@ -247,6 +247,26 @@ context.define("superuser-fran@hostsharing.net"); assertThat(testCustomerRepository.findCustomerByOptionalPrefixLike("uuu")).hasSize(0); } @Test void invalidRequestBodyJson_raisesClientError() { RestAssured // @formatter:off .given() .header("current-user", "superuser-alex@hostsharing.net") .contentType(ContentType.JSON) .body("{]") // deliberately invalid JSON .port(port) .when() .post("http://localhost/api/test/customers") .then().assertThat() .statusCode(400) .contentType(ContentType.JSON) .body("message", containsString("JSON parse error: Unexpected close marker ']': expected '}'")) .body("message", containsString("line: 1, column: 1")); // @formatter:on } } private UUID toCleanup(final UUID tempPartnerUuid) { @@ -262,7 +282,9 @@ System.out.println("DELETING temporary partner: " + uuid); final var entity = testCustomerRepository.findByUuid(uuid); final var count = testCustomerRepository.deleteByUuid(uuid); System.out.println("DELETED temporary partner: " + uuid + (count > 0 ? " successful" : " failed") + " (" + entity.map(TestCustomerEntity::getPrefix).orElse("???") + ")"); System.out.println( "DELETED temporary partner: " + uuid + (count > 0 ? " successful" : " failed") + " (" + entity.map( TestCustomerEntity::getPrefix).orElse("???") + ")"); }).assertSuccessful(); }); }