From 413ca0917e403be8177abc857240e57ee92c1589 Mon Sep 17 00:00:00 2001 From: Michael Hoennig Date: Mon, 31 Mar 2025 13:46:41 +0200 Subject: [PATCH] feature/add-i18n-support (#167) Co-authored-by: Michael Hoennig Reviewed-on: https://dev.hostsharing.net/hostsharing/hs.hsadmin.ng/pulls/167 Reviewed-by: Marc Sandlus --- .../hsadminng/config/MessageTranslator.java | 40 +++++++ .../config/MessagesResourceConfig.java | 17 +++ .../config/RealCasAuthenticator.java | 14 ++- .../config/RetroactiveTranslator.java | 11 ++ .../hsadminng/config/WebSecurityConfig.java | 5 +- .../hsadminng/errors/CustomErrorResponse.java | 14 +-- .../errors/ReferenceNotFoundException.java | 21 +++- .../RestResponseEntityExceptionHandler.java | 113 +++++++++++------- .../asset/HsHostingAssetTranslations.java | 40 +++++++ .../HostingAssetEntityValidator.java | 2 +- .../HsIPv6NumberHostingAssetValidator.java | 2 +- ...OfficeCoopAssetsTransactionController.java | 82 ++++++++----- .../partner/HsOfficePartnerController.java | 6 +- .../hs/validation/ValidatableProperty.java | 4 +- .../hsadminng/ping/PingController.java | 15 ++- .../hsadminng/rbac/generator/RbacSpec.java | 2 +- .../resources/i18n/messages_de.properties | 26 ++++ .../resources/i18n/messages_en.properties | 8 ++ ...asAuthenticationFilterIntegrationTest.java | 8 +- .../hsadminng/config/HttpHeadersBuilder.java | 13 +- ...esponseEntityExceptionHandlerUnitTest.java | 35 ++---- ...HsBookingItemControllerAcceptanceTest.java | 2 +- .../item/HsBookingItemControllerRestTest.java | 3 +- .../HsHostingAssetControllerRestTest.java | 3 +- ...udServerHostingAssetValidatorUnitTest.java | 2 +- ...DnsSetupHostingAssetValidatorUnitTest.java | 4 +- ...ttpSetupHostingAssetValidatorUnitTest.java | 4 +- ...mainMboxHostingAssetValidatorUnitTest.java | 2 +- ...ainSetupHostingAssetValidatorUnitTest.java | 10 +- ...mtpSetupHostingAssetValidatorUnitTest.java | 2 +- ...lAddressHostingAssetValidatorUnitTest.java | 2 +- ...ailAliasHostingAssetValidatorUnitTest.java | 2 +- ...v4NumberHostingAssetValidatorUnitTest.java | 2 +- ...v6NumberHostingAssetValidatorUnitTest.java | 2 +- ...edServerHostingAssetValidatorUnitTest.java | 4 +- ...WebspaceHostingAssetValidatorUnitTest.java | 2 +- ...DatabaseHostingAssetValidatorUnitTest.java | 4 +- ...InstanceHostingAssetValidatorUnitTest.java | 2 +- ...iaDbUserHostingAssetValidatorUnitTest.java | 2 +- ...DatabaseHostingAssetValidatorUnitTest.java | 4 +- ...InstanceHostingAssetValidatorUnitTest.java | 2 +- ...eSqlUserHostingAssetValidatorUnitTest.java | 2 +- ...UnixUserHostingAssetValidatorUnitTest.java | 2 +- .../hs/migration/ImportHostingAssets.java | 6 +- .../hs/migration/PostgresTestcontainer.java | 2 +- ...HsOfficeBankAccountControllerRestTest.java | 3 +- ...tsTransactionControllerAcceptanceTest.java | 7 +- ...opAssetsTransactionControllerRestTest.java | 52 ++++---- ...opSharesTransactionControllerRestTest.java | 24 ++-- .../HsOfficeMembershipControllerRestTest.java | 11 +- .../HsOfficePartnerControllerRestTest.java | 54 ++++++--- .../scenarios/HsOfficeScenarioTests.java | 4 +- .../ping/PingControllerAcceptanceTest.java | 75 ++++++++++++ .../ping/PingControllerRestTest.java | 67 +++++++++++ .../rbac/role/RbacRoleControllerRestTest.java | 3 +- .../RbacSubjectControllerRestTest.java | 3 +- 56 files changed, 621 insertions(+), 232 deletions(-) create mode 100644 src/main/java/net/hostsharing/hsadminng/config/MessageTranslator.java create mode 100644 src/main/java/net/hostsharing/hsadminng/config/MessagesResourceConfig.java create mode 100644 src/main/java/net/hostsharing/hsadminng/config/RetroactiveTranslator.java create mode 100644 src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetTranslations.java create mode 100644 src/main/resources/i18n/messages_de.properties create mode 100644 src/main/resources/i18n/messages_en.properties create mode 100644 src/test/java/net/hostsharing/hsadminng/ping/PingControllerAcceptanceTest.java create mode 100644 src/test/java/net/hostsharing/hsadminng/ping/PingControllerRestTest.java diff --git a/src/main/java/net/hostsharing/hsadminng/config/MessageTranslator.java b/src/main/java/net/hostsharing/hsadminng/config/MessageTranslator.java new file mode 100644 index 00000000..b179dcf3 --- /dev/null +++ b/src/main/java/net/hostsharing/hsadminng/config/MessageTranslator.java @@ -0,0 +1,40 @@ +package net.hostsharing.hsadminng.config; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.MessageSource; +import org.springframework.stereotype.Service; +import org.springframework.web.context.annotation.RequestScope; + +import jakarta.servlet.http.HttpServletRequest; +import java.util.Locale; + +@Service +@RequestScope +public class MessageTranslator { + + @Autowired + private HttpServletRequest httpRequest; + + @Autowired + private MessageSource messageSource; + + public String translateTo(final Locale locale, final String messageKey, final Object... args) { + try { + // we don't use the method which also takes a default message right away ... + final var translatedMessage = messageSource.getMessage(messageKey, args, locale); + return translatedMessage; + } catch (final Exception e) { + final var defaultMessage = messageKey.replace("'", "''"); + final var translatedMessage = messageSource.getMessage(messageKey, args, defaultMessage, locale); + if (locale != Locale.ENGLISH) { + // ... because we want to add a hint that the translation is missing, even if placeholders got replaced + return translatedMessage + " [" + locale + " translation missing]"; + } + return translatedMessage; + } + } + + public String translate(final String messageKey, final Object... args) { + return translateTo(httpRequest.getLocale(), messageKey, args); + } +} diff --git a/src/main/java/net/hostsharing/hsadminng/config/MessagesResourceConfig.java b/src/main/java/net/hostsharing/hsadminng/config/MessagesResourceConfig.java new file mode 100644 index 00000000..18111ea1 --- /dev/null +++ b/src/main/java/net/hostsharing/hsadminng/config/MessagesResourceConfig.java @@ -0,0 +1,17 @@ +package net.hostsharing.hsadminng.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.support.ResourceBundleMessageSource; + +@Configuration +public class MessagesResourceConfig { + @Bean + public ResourceBundleMessageSource messageSource() { + final var source = new ResourceBundleMessageSource(); + source.setBasenames("i18n/messages"); + source.setDefaultEncoding("UTF-8"); + return source; + } + +} diff --git a/src/main/java/net/hostsharing/hsadminng/config/RealCasAuthenticator.java b/src/main/java/net/hostsharing/hsadminng/config/RealCasAuthenticator.java index 35a3db83..18c5d0f1 100644 --- a/src/main/java/net/hostsharing/hsadminng/config/RealCasAuthenticator.java +++ b/src/main/java/net/hostsharing/hsadminng/config/RealCasAuthenticator.java @@ -1,6 +1,7 @@ package net.hostsharing.hsadminng.config; import io.micrometer.core.annotation.Timed; +import lombok.RequiredArgsConstructor; import lombok.SneakyThrows; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; @@ -17,6 +18,7 @@ import java.io.IOException; // HOWTO add logger @Slf4j +@RequiredArgsConstructor public class RealCasAuthenticator implements CasAuthenticator { @Value("${hsadminng.cas.server}") @@ -25,8 +27,11 @@ public class RealCasAuthenticator implements CasAuthenticator { @Value("${hsadminng.cas.service}") private String serviceUrl; + private final MessageTranslator messageTranslator; + private final RestTemplate restTemplate = new RestTemplate(); + @SneakyThrows @Timed("app.cas.authenticate") public String authenticate(final HttpServletRequest httpRequest) { @@ -52,7 +57,7 @@ public class RealCasAuthenticator implements CasAuthenticator { private Document verifyServiceTicket(final String serviceTicket) throws SAXException, IOException, ParserConfigurationException { if ( !serviceTicket.startsWith("ST-") ) { - throwBadCredentialsException("Invalid authorization ticket"); + throwBadCredentialsException("unknown authorization ticket"); } final var url = casServerUrl + "/cas/p3/serviceValidate" + @@ -69,12 +74,13 @@ public class RealCasAuthenticator implements CasAuthenticator { private String extractUserName(final Document verification) { if (verification.getElementsByTagName("cas:authenticationSuccess").getLength() == 0) { - throwBadCredentialsException("CAS service ticket could not be verified"); + throwBadCredentialsException("CAS service-ticket could not be verified"); } return verification.getElementsByTagName("cas:user").item(0).getTextContent(); } - private void throwBadCredentialsException(final String message) { - throw new BadCredentialsException(message); + private void throwBadCredentialsException(final String messageKey) { + final var translatedMessage = messageTranslator.translate(messageKey); + throw new BadCredentialsException(translatedMessage); } } diff --git a/src/main/java/net/hostsharing/hsadminng/config/RetroactiveTranslator.java b/src/main/java/net/hostsharing/hsadminng/config/RetroactiveTranslator.java new file mode 100644 index 00000000..14d17cc1 --- /dev/null +++ b/src/main/java/net/hostsharing/hsadminng/config/RetroactiveTranslator.java @@ -0,0 +1,11 @@ +package net.hostsharing.hsadminng.config; + +/** + * Makes it possible to translate messages which got created by external sources (libraries, database, etc.) + * without i18n support. + */ +public interface RetroactiveTranslator { + + boolean canTranslate(final String message); + String translate(final String message); +} diff --git a/src/main/java/net/hostsharing/hsadminng/config/WebSecurityConfig.java b/src/main/java/net/hostsharing/hsadminng/config/WebSecurityConfig.java index 7eaae1d2..92ab5f2c 100644 --- a/src/main/java/net/hostsharing/hsadminng/config/WebSecurityConfig.java +++ b/src/main/java/net/hostsharing/hsadminng/config/WebSecurityConfig.java @@ -26,6 +26,9 @@ public class WebSecurityConfig { @Autowired private CasAuthenticationFilter authenticationFilter; + @Autowired + private MessageTranslator messageTranslator; + @Bean @Profile("!test") public SecurityFilterChain securityFilterChain(final HttpSecurity http) throws Exception { @@ -55,7 +58,7 @@ public class WebSecurityConfig { @Bean @Profile("realCasAuthenticator") public CasAuthenticator realCasServiceTicketValidator() { - return new RealCasAuthenticator(); + return new RealCasAuthenticator(messageTranslator); } @Bean diff --git a/src/main/java/net/hostsharing/hsadminng/errors/CustomErrorResponse.java b/src/main/java/net/hostsharing/hsadminng/errors/CustomErrorResponse.java index 2455ee76..9d759206 100644 --- a/src/main/java/net/hostsharing/hsadminng/errors/CustomErrorResponse.java +++ b/src/main/java/net/hostsharing/hsadminng/errors/CustomErrorResponse.java @@ -11,7 +11,7 @@ import java.time.LocalDateTime; @Getter public class CustomErrorResponse { - static ResponseEntity errorResponse( + static ResponseEntity customErrorResponse( final WebRequest request, final HttpStatus httpStatus, final String message) { @@ -21,13 +21,13 @@ public class CustomErrorResponse { static String firstMessageLine(final Throwable exception) { if (exception.getMessage() != null) { - return line(exception.getMessage(), 0); + return stripTechnicalDetails(exception.getMessage()); } return "ERROR: [500] " + exception.getClass().getName(); } - static String line(final String message, final int lineNo) { - return message.split("\\r|\\n|\\r\\n", 0)[lineNo]; + static String stripTechnicalDetails(final String message) { + return message.split("\\r|\\n|\\r\\n", 0)[0]; } @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd hh:mm:ss") @@ -41,12 +41,12 @@ public class CustomErrorResponse { private final String message; - CustomErrorResponse(final String path, final HttpStatus status, final String message) { + CustomErrorResponse(final String path, final HttpStatus status, final String rawMessage) { + // HOWTO: debug serverside error response - set a breakpoint here this.timestamp = LocalDateTime.now(); this.path = path; this.statusCode = status.value(); this.statusPhrase = status.getReasonPhrase(); - // HOWTO: debug serverside error response - set a breakpoint here - this.message = message.startsWith("ERROR: [") ? message : "ERROR: [" + statusCode + "] " + message; + this.message = rawMessage.startsWith("ERROR: [") ? rawMessage : "ERROR: [" + statusCode + "] " + rawMessage; } } diff --git a/src/main/java/net/hostsharing/hsadminng/errors/ReferenceNotFoundException.java b/src/main/java/net/hostsharing/hsadminng/errors/ReferenceNotFoundException.java index 7d032d50..ef4d6d22 100644 --- a/src/main/java/net/hostsharing/hsadminng/errors/ReferenceNotFoundException.java +++ b/src/main/java/net/hostsharing/hsadminng/errors/ReferenceNotFoundException.java @@ -1,19 +1,36 @@ package net.hostsharing.hsadminng.errors; +import net.hostsharing.hsadminng.config.MessageTranslator; + import java.util.UUID; +import static java.util.Locale.ENGLISH; + public class ReferenceNotFoundException extends RuntimeException { + private final String TRANSLATABLE_MESSAGE = "{0} \"{1}\" not found"; + + private final MessageTranslator translator; + private final Class entityClass; + private final String entityClassDisplayName; private final UUID uuid; - public ReferenceNotFoundException(final Class entityClass, final UUID uuid, final Throwable exc) { + + public ReferenceNotFoundException(final MessageTranslator translator, final Class entityClass, final UUID uuid, final Throwable exc) { super(exc); + this.translator = translator; this.entityClass = entityClass; + this.entityClassDisplayName = DisplayAs.DisplayName.of(entityClass); this.uuid = uuid; } @Override public String getMessage() { - return "Cannot resolve " + entityClass.getSimpleName() +" with uuid " + uuid; + return translator.translateTo(ENGLISH, TRANSLATABLE_MESSAGE, entityClassDisplayName, uuid); + } + + @Override + public String getLocalizedMessage() { + return translator.translate(TRANSLATABLE_MESSAGE, entityClassDisplayName, uuid); } } diff --git a/src/main/java/net/hostsharing/hsadminng/errors/RestResponseEntityExceptionHandler.java b/src/main/java/net/hostsharing/hsadminng/errors/RestResponseEntityExceptionHandler.java index ce4b5b93..0a6c3eed 100644 --- a/src/main/java/net/hostsharing/hsadminng/errors/RestResponseEntityExceptionHandler.java +++ b/src/main/java/net/hostsharing/hsadminng/errors/RestResponseEntityExceptionHandler.java @@ -1,8 +1,13 @@ package net.hostsharing.hsadminng.errors; +import lombok.RequiredArgsConstructor; +import net.hostsharing.hsadminng.config.MessageTranslator; +import net.hostsharing.hsadminng.config.RetroactiveTranslator; import org.iban4j.Iban4jException; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.core.NestedExceptionUtils; import org.springframework.dao.DataIntegrityViolationException; +import org.springframework.http.HttpEntity; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatusCode; @@ -23,62 +28,67 @@ import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExcep import jakarta.persistence.EntityNotFoundException; import jakarta.validation.ValidationException; import java.util.*; -import java.util.regex.Pattern; +import java.util.function.Function; import static net.hostsharing.hsadminng.errors.CustomErrorResponse.*; @ControllerAdvice +@RequiredArgsConstructor // HOWTO handle exceptions to produce specific http error codes and sensible error messages public class RestResponseEntityExceptionHandler extends ResponseEntityExceptionHandler { + @Autowired + private final MessageTranslator messageTranslator; + + @Autowired(required = false) + private final List retroactiveTranslators; + @ExceptionHandler(DataIntegrityViolationException.class) protected ResponseEntity handleConflict( final RuntimeException exc, final WebRequest request) { - final var rawMessage = NestedExceptionUtils.getMostSpecificCause(exc).getMessage(); - var message = line(rawMessage, 0); - if (message.contains("violates foreign key constraint")) { - return errorResponse(request, HttpStatus.BAD_REQUEST, line(rawMessage, 1).replaceAll(" *Detail: *", "")); - } - return errorResponse(request, HttpStatus.CONFLICT, message); + final var fullMaybeLocalizedMessage = localizedMessage(NestedExceptionUtils.getMostSpecificCause(exc)); + final var sprippedMaybeLocalizedMessage = stripTechnicalDetails(fullMaybeLocalizedMessage); + return errorResponse(request, HttpStatus.CONFLICT, sprippedMaybeLocalizedMessage); } @ExceptionHandler(JpaSystemException.class) protected ResponseEntity handleJpaExceptions( final RuntimeException exc, final WebRequest request) { - final var message = line(NestedExceptionUtils.getMostSpecificCause(exc).getMessage(), 0); - return errorResponse(request, httpStatus(exc, message).orElse(HttpStatus.INTERNAL_SERVER_ERROR), message); + final var fullMaybeLocalizedMessage = localizedMessage(NestedExceptionUtils.getMostSpecificCause(exc)); + final var sprippedMaybeLocalizedMessage = stripTechnicalDetails(fullMaybeLocalizedMessage); + return errorResponse(request, httpStatus(exc, sprippedMaybeLocalizedMessage).orElse(HttpStatus.INTERNAL_SERVER_ERROR), sprippedMaybeLocalizedMessage); } @ExceptionHandler(NoSuchElementException.class) protected ResponseEntity handleNoSuchElementException( final RuntimeException exc, final WebRequest request) { - final var message = line(NestedExceptionUtils.getMostSpecificCause(exc).getMessage(), 0); - return errorResponse(request, HttpStatus.NOT_FOUND, message); + final var fullMaybeLocalizedMessage = localizedMessage(NestedExceptionUtils.getMostSpecificCause(exc)); + final var sprippedMaybeLocalizedMessage = stripTechnicalDetails(fullMaybeLocalizedMessage); + return errorResponse(request, HttpStatus.NOT_FOUND, sprippedMaybeLocalizedMessage); } @ExceptionHandler(ReferenceNotFoundException.class) protected ResponseEntity handleReferenceNotFoundException( final ReferenceNotFoundException exc, final WebRequest request) { - return errorResponse(request, HttpStatus.BAD_REQUEST, exc.getMessage()); + return errorResponse(request, HttpStatus.BAD_REQUEST, localizedMessage(exc)); } @ExceptionHandler({ JpaObjectRetrievalFailureException.class, EntityNotFoundException.class }) protected ResponseEntity handleJpaObjectRetrievalFailureException( final RuntimeException exc, final WebRequest request) { - final var message = - userReadableEntityClassName( - line(NestedExceptionUtils.getMostSpecificCause(exc).getMessage(), 0)); - return errorResponse(request, HttpStatus.BAD_REQUEST, message); + final var localizedMessage = localizedMessage(NestedExceptionUtils.getMostSpecificCause(exc)); + final var sprippedMaybeLocalizedMessage = stripTechnicalDetails(localizedMessage); + return errorResponse(request, HttpStatus.BAD_REQUEST, sprippedMaybeLocalizedMessage); } @ExceptionHandler({ Iban4jException.class, ValidationException.class }) protected ResponseEntity handleValidationExceptions( final Throwable exc, final WebRequest request) { - final String fullMessage = NestedExceptionUtils.getMostSpecificCause(exc).getMessage(); - final var message = exc instanceof MultiValidationException ? fullMessage : line(fullMessage, 0); - return errorResponse(request, HttpStatus.BAD_REQUEST, message); + final var localizedMessage = localizedMessage(NestedExceptionUtils.getMostSpecificCause(exc)); + final var sprippedMaybeLocalizedMessage = exc instanceof MultiValidationException ? localizedMessage : stripTechnicalDetails(localizedMessage); + return errorResponse(request, HttpStatus.BAD_REQUEST, sprippedMaybeLocalizedMessage); } @ExceptionHandler(Throwable.class) @@ -96,15 +106,16 @@ public class RestResponseEntityExceptionHandler final var response = super.handleExceptionInternal(exc, body, headers, statusCode, request); return errorResponse(request, HttpStatus.valueOf(statusCode.value()), - Optional.ofNullable(response.getBody()).map(Object::toString).orElse(firstMessageLine(exc))); + Optional.ofNullable(response).map(HttpEntity::getBody).map(Object::toString).orElse(firstMessageLine(exc))); } @Override @SuppressWarnings("unchecked,rawtypes") protected ResponseEntity handleHttpMessageNotReadable( HttpMessageNotReadableException exc, HttpHeaders headers, HttpStatusCode status, WebRequest request) { - final var message = line(exc.getMessage(), 0); - return errorResponse(request, HttpStatus.BAD_REQUEST, message); + final var localizedMessage = localizedMessage(exc); + final var sprippedMaybeLocalizedMessage = stripTechnicalDetails(localizedMessage); + return errorResponse(request, HttpStatus.BAD_REQUEST, sprippedMaybeLocalizedMessage); } @Override @@ -139,37 +150,26 @@ public class RestResponseEntityExceptionHandler .flatMap(Collection::stream) .filter(FieldError.class::isInstance) .map(FieldError.class::cast) - .map(fieldError -> fieldError.getField() + " " + fieldError.getDefaultMessage() + " but is \"" - + fieldError.getRejectedValue() + "\"") + .map(toEnrichedFieldErrorMessage()) .toList(); return errorResponse(request, HttpStatus.BAD_REQUEST, errorList.toString()); } - - private String userReadableEntityClassName(final String exceptionMessage) { - final var regex = "(net.hostsharing.hsadminng.[a-z0-9_.]*.[A-Za-z0-9_$]*Entity) "; - final var pattern = Pattern.compile(regex); - final var matcher = pattern.matcher(exceptionMessage); - if (matcher.find()) { - final var entityName = matcher.group(1); - final var entityClass = resolveClass(entityName); - if (entityClass.isPresent()) { - return (entityClass.get().isAnnotationPresent(DisplayAs.class) - ? exceptionMessage.replace(entityName, entityClass.get().getAnnotation(DisplayAs.class).value()) - : exceptionMessage.replace(entityName, entityClass.get().getSimpleName())) - .replace(" with id ", " with uuid "); - } - - } - return exceptionMessage; + private Function toEnrichedFieldErrorMessage() { + final var translatedButIsLiteral = messageTranslator.translate("but is"); + // TODO.i18n: the following does not work in all languages, e.g. not in right-to-left languages + return fieldError -> fieldError.getField() + " " + fieldError.getDefaultMessage() + + " " + translatedButIsLiteral + " " + optionallyQuoted(fieldError.getRejectedValue()); } - private static Optional> resolveClass(final String entityName) { - try { - return Optional.of(ClassLoader.getSystemClassLoader().loadClass(entityName)); - } catch (ClassNotFoundException e) { - return Optional.empty(); + private String optionallyQuoted(final Object value) { + if (value == null) { + return "null"; } + if (value instanceof Number) { + return value.toString(); + } + return "\"" + value + "\""; } private Optional httpStatus(final Throwable causingException, final String message) { @@ -187,4 +187,25 @@ public class RestResponseEntityExceptionHandler return Optional.empty(); } + private String localizedMessage(final Throwable throwable) { + // most libraries seem to provide the localized message in both properties, but just for the case: + return throwable.getLocalizedMessage() != null ? throwable.getLocalizedMessage() : throwable.getMessage(); + } + + private String tryTranslation(final String message) { + + for ( RetroactiveTranslator rtx: retroactiveTranslators ) { + if (rtx.canTranslate(message)) { + return rtx.translate(message); + } + } + return message; + } + + private ResponseEntity errorResponse( + final WebRequest request, + final HttpStatus httpStatus, + final String maybeTranslatedMessage) { + return customErrorResponse(request, httpStatus, tryTranslation(maybeTranslatedMessage)); + } } diff --git a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetTranslations.java b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetTranslations.java new file mode 100644 index 00000000..7d7abf29 --- /dev/null +++ b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetTranslations.java @@ -0,0 +1,40 @@ +package net.hostsharing.hsadminng.hs.hosting.asset; + +import net.hostsharing.hsadminng.config.MessageTranslator; +import net.hostsharing.hsadminng.config.RetroactiveTranslator; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + + +// HOWTO translate messages which got created without i18n support, in this case in a PostgreSQL constraint trigger +@Service +public class HsHostingAssetTranslations implements RetroactiveTranslator { + + public static final String ERROR_400_PREFIX = "ERROR: [400] "; + + @Autowired + private MessageTranslator messageTranslator; + + @Override + public boolean canTranslate(final String message) { + return message.equals("ERROR: [400] coop assets transaction would result in a negative balance of assets"); + } + + @Override + public String translate(final String message) { + // it's guaranteed to be the same message, for which canTranslate(...) returned true + // and in this case it's just one + return ERROR_400_PREFIX + messageTranslator.translate(message.substring(ERROR_400_PREFIX.length())); + + // HOWTO extract variable parts from a messages which got created without i18n support: + // final var regex = "(?[^ ]+) (?.+) not found"; + // final var pattern = Pattern.compile(regex); + // final var matcher = pattern.matcher(message); + // + // if (matcher.matches()) { + // final var propertyName = matcher.group("propertyName"); + // final var propertyValue = matcher.group("propertyValue"); + // return messageTranslator.translate("", propertyName, propertyValue); + // } + } +} diff --git a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HostingAssetEntityValidator.java b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HostingAssetEntityValidator.java index bff087f4..c9da2b19 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HostingAssetEntityValidator.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HostingAssetEntityValidator.java @@ -145,7 +145,7 @@ public abstract class HostingAssetEntityValidator extends HsEntityValidator violations) { if (List.of(DEPOSIT, HsOfficeCoopAssetsTransactionTypeResource.ADOPTION).contains(requestBody.getTransactionType()) && requestBody.getAssetValue().signum() < 0) { - violations.add("for %s, assetValue must be positive but is \"%.2f\"".formatted( + violations.add(messageTranslator.translate("for transactionType={0}, assetValue must be positive but is {1,number,#0.00}", requestBody.getTransactionType(), requestBody.getAssetValue())); } } - private static void validateCreditTransaction( + private void validateCreditTransaction( final HsOfficeCoopAssetsTransactionInsertResource requestBody, final ArrayList violations) { if (List.of(DISBURSAL, HsOfficeCoopAssetsTransactionTypeResource.TRANSFER, CLEARING, LOSS) .contains(requestBody.getTransactionType()) && requestBody.getAssetValue().signum() > 0) { - violations.add("for %s, assetValue must be negative but is \"%.2f\"".formatted( + violations.add(messageTranslator.translate("for transactionType={0}, assetValue must be negative but is {1,number,#0.00}", requestBody.getTransactionType(), requestBody.getAssetValue())); } } - private static void validateAssetValue( + private void validateAssetValue( final HsOfficeCoopAssetsTransactionInsertResource requestBody, final ArrayList violations) { if (requestBody.getAssetValue().signum() == 0) { - violations.add("assetValue must not be 0 but is \"%.2f\"".formatted( - requestBody.getAssetValue())); + violations.add(messageTranslator.translate("assetValue must not be 0")); } } @@ -221,26 +223,32 @@ public class HsOfficeCoopAssetsTransactionController implements HsOfficeCoopAsse final HsOfficeMembershipEntity membership = ofNullable(emw.find( HsOfficeMembershipEntity.class, resource.getMembershipUuid())) - .orElseThrow(() -> new EntityNotFoundException("membership.uuid %s not found".formatted( - resource.getMembershipUuid()))); + .orElseThrow(() -> new EntityNotFoundException( + messageTranslator.translate( + "{0} \"{1}\" not found", "membership.uuid", resource.getMembershipUuid()))); entity.setMembership(membership); } if (entity.getTransactionType() == REVERSAL) { if (resource.getRevertedAssetTxUuid() == null) { - throw new ValidationException("REVERSAL asset transaction requires revertedAssetTx.uuid"); + throw new ValidationException(messageTranslator.translate( + "a REVERSAL asset transaction requires specifying a revertedAssetTx.uuid")); } final var revertedAssetTx = coopAssetsTransactionRepo.findByUuid(resource.getRevertedAssetTxUuid()) - .orElseThrow(() -> new EntityNotFoundException("revertedAssetTx.uuid %s not found".formatted( + .orElseThrow(() -> new EntityNotFoundException(messageTranslator.translate( + "{0} \"{1}\" not found", + "revertedAssetTx.uuid", resource.getRevertedAssetTxUuid()))); revertedAssetTx.setReversalAssetTx(entity); entity.setRevertedAssetTx(revertedAssetTx); if (resource.getAssetValue().negate().compareTo(revertedAssetTx.getAssetValue()) != 0) { - throw new ValidationException("given assetValue=" + resource.getAssetValue() + - " but must be negative value from reverted asset tx: " + revertedAssetTx.getAssetValue()); + throw new ValidationException( + messageTranslator.translate( + "given assetValue {0,number,#0.00} must be the negative value of the reverted asset transaction: {1,number,#0.00}", + resource.getAssetValue(), revertedAssetTx.getAssetValue())); } - if (revertedAssetTx.getTransactionType() == TRANSFER) { + if (revertedAssetTx.getTransactionType() == HsOfficeCoopAssetsTransactionType.TRANSFER) { final var adoptionAssetTx = revertedAssetTx.getAdoptionAssetTx(); final var adoptionReversalAssetTx = HsOfficeCoopAssetsTransactionEntity.builder() .transactionType(REVERSAL) @@ -259,8 +267,9 @@ public class HsOfficeCoopAssetsTransactionController implements HsOfficeCoopAsse if (resource.getTransactionType() == HsOfficeCoopAssetsTransactionTypeResource.TRANSFER) { final var adoptingMembership = determineAdoptingMembership(resource); if ( entity.getMembership() == adoptingMembership) { - throw new ValidationException("transferring and adopting membership must be different, but both are " + - adoptingMembership.getTaggedMemberNumber()); + throw new ValidationException(messageTranslator.translate( + "transferring and adopting membership must be different, but both are {0}", + adoptingMembership.getTaggedMemberNumber())); } final var adoptingAssetTx = createAdoptingAssetTx(entity, adoptingMembership); entity.setAdoptionAssetTx(adoptingAssetTx); @@ -271,34 +280,45 @@ public class HsOfficeCoopAssetsTransactionController implements HsOfficeCoopAsse final var adoptingMembershipUuid = resource.getAdoptingMembershipUuid(); final var adoptingMembershipMemberNumber = resource.getAdoptingMembershipMemberNumber(); if (adoptingMembershipUuid != null && adoptingMembershipMemberNumber != null) { - throw new ValidationException( - // @formatter:off - resource.getTransactionType() == HsOfficeCoopAssetsTransactionTypeResource.TRANSFER - ? "either adoptingMembership.uuid or adoptingMembership.memberNumber can be given, not both" - : "adoptingMembership.uuid and adoptingMembership.memberNumber must not be given for transactionType=" - + resource.getTransactionType()); - // @formatter:on + // @formatter:off + final var message = messageTranslator.translate( + resource.getTransactionType() == HsOfficeCoopAssetsTransactionTypeResource.TRANSFER + ? "either {0} or {1} must be given, not both" + : "neither {0} nor {1} must be given for transactionType={2}", + "adoptingMembership.uuid", + "adoptingMembership.memberNumber", + resource.getTransactionType()); + // @formatter:on + throw new ValidationException(message); } if (adoptingMembershipUuid != null) { final var adoptingMembership = membershipRepo.findByUuid(adoptingMembershipUuid); return adoptingMembership.orElseThrow(() -> - new ValidationException( - "adoptingMembership.uuid='" + adoptingMembershipUuid + "' not found or not accessible")); + new ValidationException(messageTranslator.translate( + "{0} \"{1}\" not found or not accessible", + "adoptingMembership.uuid", + adoptingMembershipUuid))); } if (adoptingMembershipMemberNumber != null) { final var adoptingMemberNumber = Integer.valueOf(adoptingMembershipMemberNumber.substring("M-".length())); final var adoptingMembership = membershipRepo.findMembershipByMemberNumber(adoptingMemberNumber); return adoptingMembership.orElseThrow( () -> - new ValidationException("adoptingMembership.memberNumber='" + adoptingMembershipMemberNumber - + "' not found or not accessible") - ); + new ValidationException( + messageTranslator.translate( + "{0} \"{1}\" not found or not accessible", + "adoptingMembership.memberNumber", + adoptingMembershipMemberNumber))); } throw new ValidationException( - "either adoptingMembership.uuid or adoptingMembership.memberNumber must be given for transactionType=" - + HsOfficeCoopAssetsTransactionTypeResource.TRANSFER); + messageTranslator.translate( + "either {0} or {1} must be given for transactionType={2}", + "adoptingMembership.uuid", + "adoptingMembership.memberNumber", + resource.getTransactionType() + )); } private HsOfficeCoopAssetsTransactionEntity createAdoptingAssetTx( diff --git a/src/main/java/net/hostsharing/hsadminng/hs/office/partner/HsOfficePartnerController.java b/src/main/java/net/hostsharing/hsadminng/hs/office/partner/HsOfficePartnerController.java index 0bf358a3..2f5c0ea4 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/office/partner/HsOfficePartnerController.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/office/partner/HsOfficePartnerController.java @@ -2,6 +2,7 @@ package net.hostsharing.hsadminng.hs.office.partner; import io.micrometer.core.annotation.Timed; import io.swagger.v3.oas.annotations.security.SecurityRequirement; +import net.hostsharing.hsadminng.config.MessageTranslator; import net.hostsharing.hsadminng.context.Context; import net.hostsharing.hsadminng.errors.ReferenceNotFoundException; import net.hostsharing.hsadminng.hs.office.contact.HsOfficeContactFromResourceConverter; @@ -45,6 +46,9 @@ public class HsOfficePartnerController implements HsOfficePartnersApi { @Autowired private StrictMapper mapper; + @Autowired + private MessageTranslator translator; + @Autowired private HsOfficeContactFromResourceConverter contactFromResourceConverter; @@ -236,7 +240,7 @@ public class HsOfficePartnerController implements HsOfficePartnersApi { try { return em.getReference(entityClass, uuid); } catch (final Throwable exc) { - throw new ReferenceNotFoundException(entityClass, uuid, exc); + throw new ReferenceNotFoundException(translator, entityClass, uuid, exc); } } diff --git a/src/main/java/net/hostsharing/hsadminng/hs/validation/ValidatableProperty.java b/src/main/java/net/hostsharing/hsadminng/hs/validation/ValidatableProperty.java index fb51e7fe..ef2783bc 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/validation/ValidatableProperty.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/validation/ValidatableProperty.java @@ -217,8 +217,8 @@ public abstract class ValidatableProperty

, T //noinspection unchecked validate(result, (T) propValue, propsProvider); } else { - result.add(propertyName + "' is expected to be of type " + type.getSimpleName() + ", " + - "but is of type " + propValue.getClass().getSimpleName()); + result.add(propertyName + "' is expected to be of type " + type.getSimpleName() + + " but is of type " + propValue.getClass().getSimpleName()); } } return result; diff --git a/src/main/java/net/hostsharing/hsadminng/ping/PingController.java b/src/main/java/net/hostsharing/hsadminng/ping/PingController.java index 0ee2f564..be639ab6 100644 --- a/src/main/java/net/hostsharing/hsadminng/ping/PingController.java +++ b/src/main/java/net/hostsharing/hsadminng/ping/PingController.java @@ -1,8 +1,9 @@ package net.hostsharing.hsadminng.ping; +import net.hostsharing.hsadminng.config.MessageTranslator; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.stereotype.Controller; -import org.springframework.web.bind.annotation.RequestHeader; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.ResponseBody; @@ -11,11 +12,15 @@ import org.springframework.web.bind.annotation.ResponseBody; @Controller public class PingController { + @Autowired + private MessageTranslator messageTranslator; + @ResponseBody @RequestMapping(value = "/api/ping", method = RequestMethod.GET) - public String ping( - @RequestHeader(name = "assumed-roles", required = false) String assumedRoles - ) { - return "pong " + SecurityContextHolder.getContext().getAuthentication().getName() + "\n"; + public String ping() { + final var userName = SecurityContextHolder.getContext().getAuthentication().getName(); + // HOWTO translate text with placeholders - also see in resource files i18n/messages_*.properties. + final var translatedMessage = messageTranslator.translate("pong {0} - in English", userName); + return translatedMessage + "\n"; } } diff --git a/src/main/java/net/hostsharing/hsadminng/rbac/generator/RbacSpec.java b/src/main/java/net/hostsharing/hsadminng/rbac/generator/RbacSpec.java index e3242791..81f9a9b9 100644 --- a/src/main/java/net/hostsharing/hsadminng/rbac/generator/RbacSpec.java +++ b/src/main/java/net/hostsharing/hsadminng/rbac/generator/RbacSpec.java @@ -1069,7 +1069,7 @@ public class RbacSpec { */ public static SQL fetchedBySql(final String sql) { if ( !sql.startsWith("SELECT ${columns}") ) { - throw new IllegalArgumentException("SQL SELECT expression must start with 'SELECT ${columns}', but is: " + sql); + throw new IllegalArgumentException("SQL SELECT expression must start with 'SELECT ${columns}' but is: " + sql); } validateExpression(sql); return new SQL(sql, Part.SQL_QUERY); diff --git a/src/main/resources/i18n/messages_de.properties b/src/main/resources/i18n/messages_de.properties new file mode 100644 index 00000000..14a49800 --- /dev/null +++ b/src/main/resources/i18n/messages_de.properties @@ -0,0 +1,26 @@ +# This file must be in UTF-8 encoding. Check if you see umlauts or garbage: äöüÄÖÜß. +# HINT IntelliJ IDEA shows unused keys in gray. + +pong\ {0}\ -\ in\ English=pong {0} - auf Deutsch + +# config (including authorization) +CAS\ service-ticket\ could\ not\ be\ verified=CAS Service-Ticket konnte nicht verifiziert werden +unknown\ authorization\ ticket=unbekanntes Autorisierungs-Ticket + +# general validations +{0}\ "{1}"\ not\ found={0} "{1}" nicht gefunden +{0}\ "{1}"\ not\ found\ or\ not\ accessible={0} "{1}" nicht gefunden oder nicht zugänglich +but\ is=ist aber + +# office.coop-assets +either\ {0}\ or\ {1}\ must\ be\ given=entweder {0} oder {1} muss angegeben werden +either\ {0}\ or\ {1}\ must\ be\ given,\ not\ both=entweder {0} oder {1} muss angegeben werden, nicht beide +either\ {0}\ or\ {1}\ must\ be\ given\ for\ transactionType\={2}=für transactionType={2} muss entweder {0} oder {1} angegeben werden +neither\ {0}\ nor\ {1}\ must\ be\ given\ for\ transactionType\={2}=für transactionType={2} darf weder {0} noch {1} angegeben werden +assetValue\ must\ not\ be\ 0=assetValue darf nicht 0 sein +for\ transactionType\={0},\ assetValue\ must\ be\ positive\ but\ is\ {1,number,#0.00}=für transactionType={0}, muss assetValue positiv sein, ist aber {1,number,#0.00} +for\ transactionType\={0},\ assetValue\ must\ be\ negative\ but\ is\ {1,number,#0.00}=für transactionType={0}, muss assetValue negativ sein, ist aber {1,number,#0.00} +given\ assetValue\ {0,number,#0.00}\ must\ be\ the\ negative\ value\ of\ the\ reverted\ asset\ transaction\:\ {1,number,#0.00}=assetValue={0,number,#0.00} muss dem negativen Wert des Wertes der stornierten Geschäftsguthaben-Transaktion entsprechen: {1,number,#0.00} +a\ REVERSAL\ asset\ transaction\ requires\ specifying\ a\ revertedAssetTx.uuid=eine REVERSAL Geschäftsguthaben-Transaktion erfordert die Angabe einer revertedAssetTx.uuid +transferring\ and\ adopting\ membership\ must\ be\ different,\ but\ both\ are\ {0}=übertragende und annehmende Mitgliedschaft müssen unterschiedlich sein, aber beide sind {0} +coop\ assets\ transaction\ would\ result\ in\ a\ negative\ balance\ of\ assets=Geschäftsguthaben-Transaktion würde zu einem negativen Geschäftsguthaben-Saldo führen diff --git a/src/main/resources/i18n/messages_en.properties b/src/main/resources/i18n/messages_en.properties new file mode 100644 index 00000000..c7facd4e --- /dev/null +++ b/src/main/resources/i18n/messages_en.properties @@ -0,0 +1,8 @@ +# This file must be in UTF-8 encoding. Check if you see umlauts or garbage: äöüÄÖÜß +# HINT IntelliJ IDEA shows unused keys in gray. + +# If the English translation is identical to the translation-key, it does not need to be included here. +# But in that case, you can NOT use a prefix - or the prefix would be shown to the user as well. +# I'm not sure, though, if using the english default translations as keys is really a good idea. + + diff --git a/src/test/java/net/hostsharing/hsadminng/config/CasAuthenticationFilterIntegrationTest.java b/src/test/java/net/hostsharing/hsadminng/config/CasAuthenticationFilterIntegrationTest.java index 24a103e6..3e00b7c3 100644 --- a/src/test/java/net/hostsharing/hsadminng/config/CasAuthenticationFilterIntegrationTest.java +++ b/src/test/java/net/hostsharing/hsadminng/config/CasAuthenticationFilterIntegrationTest.java @@ -18,9 +18,11 @@ import org.springframework.http.HttpStatus; import org.springframework.test.context.ActiveProfiles; import org.springframework.test.context.TestPropertySource; + import static com.github.tomakehurst.wiremock.client.WireMock.aResponse; import static com.github.tomakehurst.wiremock.client.WireMock.get; import static com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo; +import static java.util.Map.entry; import static net.hostsharing.hsadminng.config.HttpHeadersBuilder.headers; import static org.apache.commons.lang3.RandomStringUtils.randomAlphanumeric; import static org.assertj.core.api.Assertions.assertThat; @@ -69,13 +71,13 @@ class CasAuthenticationFilterIntegrationTest { final var result = restTemplate.exchange( "http://localhost:" + this.serverPort + "/api/ping", HttpMethod.GET, - new HttpEntity<>(null, headers("Authorization", "ST-valid")), + new HttpEntity<>(null, headers(entry("Authorization", "ST-valid"))), String.class ); // then assertThat(result.getStatusCode()).isEqualTo(HttpStatus.OK); - assertThat(result.getBody()).isEqualTo("pong " + username + "\n"); + assertThat(result.getBody()).startsWith("pong " + username); // HOWTO assert log messages assertThat(capturedOutput.getOut()).containsPattern( LogbackLogPattern.of(LogLevel.DEBUG, RealCasAuthenticator.class, "CAS-user: " + username)); @@ -97,7 +99,7 @@ class CasAuthenticationFilterIntegrationTest { final var result = restTemplate.exchange( "http://localhost:" + this.serverPort + "/api/ping", HttpMethod.GET, - new HttpEntity<>(null, headers("Authorization", "invalid")), + new HttpEntity<>(null, headers(entry("Authorization", "invalid"))), String.class ); diff --git a/src/test/java/net/hostsharing/hsadminng/config/HttpHeadersBuilder.java b/src/test/java/net/hostsharing/hsadminng/config/HttpHeadersBuilder.java index ac61fb35..77a77c91 100644 --- a/src/test/java/net/hostsharing/hsadminng/config/HttpHeadersBuilder.java +++ b/src/test/java/net/hostsharing/hsadminng/config/HttpHeadersBuilder.java @@ -2,11 +2,16 @@ package net.hostsharing.hsadminng.config; import org.springframework.http.HttpHeaders; +import java.util.Map; + public class HttpHeadersBuilder { - public static HttpHeaders headers(final String key, final String value) { - final var headers = new HttpHeaders(); - headers.set(key, value); - return headers; + @SafeVarargs + public static HttpHeaders headers(final Map.Entry... headers) { + final var httpHeaders = new HttpHeaders(); + for (Map.Entry entry : headers) { + httpHeaders.set(entry.getKey(), entry.getValue()); + } + return httpHeaders; } } diff --git a/src/test/java/net/hostsharing/hsadminng/errors/RestResponseEntityExceptionHandlerUnitTest.java b/src/test/java/net/hostsharing/hsadminng/errors/RestResponseEntityExceptionHandlerUnitTest.java index 1da27e14..7017f2e6 100644 --- a/src/test/java/net/hostsharing/hsadminng/errors/RestResponseEntityExceptionHandlerUnitTest.java +++ b/src/test/java/net/hostsharing/hsadminng/errors/RestResponseEntityExceptionHandlerUnitTest.java @@ -1,5 +1,6 @@ package net.hostsharing.hsadminng.errors; +import net.hostsharing.hsadminng.config.MessageTranslator; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.params.ParameterizedTest; @@ -20,6 +21,7 @@ import jakarta.persistence.EntityNotFoundException; import java.util.List; import java.util.NoSuchElementException; +import static java.util.Collections.emptyList; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; @@ -27,7 +29,8 @@ import static org.mockito.Mockito.when; @ExtendWith(MockitoExtension.class) class RestResponseEntityExceptionHandlerUnitTest { - final RestResponseEntityExceptionHandler exceptionHandler = new RestResponseEntityExceptionHandler(); + final RestResponseEntityExceptionHandler exceptionHandler = + new RestResponseEntityExceptionHandler(mock(MessageTranslator.class), emptyList()); @Test void handleConflict() { @@ -46,20 +49,16 @@ class RestResponseEntityExceptionHandlerUnitTest { @Test void handleForeignKeyViolation() { // given - final var givenException = new DataIntegrityViolationException(""" - ... violates foreign key constraint ... - Detail: Second Line - Third Line - """); + final var givenException = new DataIntegrityViolationException("... violates foreign key constraint ..."); final var givenWebRequest = mock(WebRequest.class); // when final var errorResponse = exceptionHandler.handleConflict(givenException, givenWebRequest); // then - assertThat(errorResponse.getBody().getStatusCode()).isEqualTo(400); - assertThat(errorResponse.getBody()).isNotNull() - .extracting(CustomErrorResponse::getMessage).isEqualTo("ERROR: [400] Second Line"); + assertThat(errorResponse.getBody().getStatusCode()).isEqualTo(HttpStatus.CONFLICT.value()); + assertThat(errorResponse.getBody()).isNotNull().extracting(CustomErrorResponse::getMessage).isEqualTo( + "ERROR: [409] ... violates foreign key constraint ..."); } @Test @@ -127,24 +126,6 @@ class RestResponseEntityExceptionHandlerUnitTest { assertThat(errorResponse.getBody().getMessage()).isEqualTo("ERROR: [400] whatever error message"); } - @Test - void handleJpaObjectRetrievalFailureExceptionWithEntityName() { - // given - final var givenException = new JpaObjectRetrievalFailureException( - new EntityNotFoundException("Unable to find " - + NoDisplayNameEntity.class.getTypeName() - + " with id 12345-123454") - ); - final var givenWebRequest = mock(WebRequest.class); - - // when - final var errorResponse = exceptionHandler.handleJpaObjectRetrievalFailureException(givenException, givenWebRequest); - - // then - assertThat(errorResponse.getBody().getStatusCode()).isEqualTo(400); - assertThat(errorResponse.getBody().getMessage()).isEqualTo("ERROR: [400] Unable to find NoDisplayNameEntity with uuid 12345-123454"); - } - @Test void jpaExceptionWithUnknownErrorCode() { // given diff --git a/src/test/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemControllerAcceptanceTest.java b/src/test/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemControllerAcceptanceTest.java index b84e0433..d368f67e 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemControllerAcceptanceTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemControllerAcceptanceTest.java @@ -45,7 +45,7 @@ import static org.hamcrest.Matchers.matchesRegex; @SpringBootTest( webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, - classes = { HsadminNgApplication.class, DisableSecurityConfig.class, JpaAttempt.class } + classes = { HsadminNgApplication.class, DisableSecurityConfig.class, JpaAttempt.class} ) @ActiveProfiles("test") @Transactional diff --git a/src/test/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemControllerRestTest.java b/src/test/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemControllerRestTest.java index 162e9ff1..ee226814 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemControllerRestTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemControllerRestTest.java @@ -1,6 +1,7 @@ package net.hostsharing.hsadminng.hs.booking.item; import net.hostsharing.hsadminng.config.JsonObjectMapperConfiguration; +import net.hostsharing.hsadminng.config.MessageTranslator; import net.hostsharing.hsadminng.context.Context; import net.hostsharing.hsadminng.hs.booking.generated.api.v1.model.HsBookingItemInsertResource; import net.hostsharing.hsadminng.hs.booking.project.HsBookingProjectRealEntity; @@ -42,7 +43,7 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers. import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @WebMvcTest(HsBookingItemController.class) -@Import({StrictMapper.class, JsonObjectMapperConfiguration.class, DisableSecurityConfig.class}) +@Import({StrictMapper.class, JsonObjectMapperConfiguration.class, DisableSecurityConfig.class, MessageTranslator.class}) @RunWith(SpringRunner.class) @ActiveProfiles("test") class HsBookingItemControllerRestTest { diff --git a/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetControllerRestTest.java b/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetControllerRestTest.java index e50d9d1c..972b8807 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetControllerRestTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetControllerRestTest.java @@ -6,6 +6,7 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.SerializationFeature; import lombok.SneakyThrows; import net.hostsharing.hsadminng.config.JsonObjectMapperConfiguration; +import net.hostsharing.hsadminng.config.MessageTranslator; import net.hostsharing.hsadminng.context.Context; import net.hostsharing.hsadminng.hs.booking.item.HsBookingItemRealRepository; import net.hostsharing.hsadminng.mapper.Array; @@ -55,7 +56,7 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers. import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @WebMvcTest(HsHostingAssetController.class) -@Import({ StrictMapper.class, JsonObjectMapperConfiguration.class, DisableSecurityConfig.class }) +@Import({ StrictMapper.class, JsonObjectMapperConfiguration.class, DisableSecurityConfig.class, MessageTranslator.class }) @RunWith(SpringRunner.class) @ActiveProfiles("test") public class HsHostingAssetControllerRestTest { diff --git a/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsCloudServerHostingAssetValidatorUnitTest.java b/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsCloudServerHostingAssetValidatorUnitTest.java index 669a0c46..59ca90f7 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsCloudServerHostingAssetValidatorUnitTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsCloudServerHostingAssetValidatorUnitTest.java @@ -54,7 +54,7 @@ class HsCloudServerHostingAssetValidatorUnitTest { // then assertThat(result).containsExactlyInAnyOrder( - "'identifier' expected to match '^vm[0-9][0-9][0-9][0-9]$', but is 'xyz99'"); + "'identifier' expected to match '^vm[0-9][0-9][0-9][0-9]$' but is 'xyz99'"); } @Test diff --git a/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsDomainDnsSetupHostingAssetValidatorUnitTest.java b/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsDomainDnsSetupHostingAssetValidatorUnitTest.java index 607485bf..15e649ff 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsDomainDnsSetupHostingAssetValidatorUnitTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsDomainDnsSetupHostingAssetValidatorUnitTest.java @@ -121,7 +121,7 @@ class HsDomainDnsSetupHostingAssetValidatorUnitTest { // then assertThat(result).containsExactly( - "'identifier' expected to match '^\\Qexample.org|DNS\\E$', but is 'example.org'" + "'identifier' expected to match '^\\Qexample.org|DNS\\E$' but is 'example.org'" ); } @@ -203,7 +203,7 @@ class HsDomainDnsSetupHostingAssetValidatorUnitTest { // then assertThat(result).containsExactlyInAnyOrder( - "'DOMAIN_DNS_SETUP:example.org|DNS.config.TTL' is expected to be of type Integer, but is of type String", + "'DOMAIN_DNS_SETUP:example.org|DNS.config.TTL' is expected to be of type Integer but is of type String", "'DOMAIN_DNS_SETUP:example.org|DNS.config.user-RR' is expected to match any of [(\\*\\.)?([a-zA-Z0-9\\._-]+|@)[ \t]+(([1-9][0-9]*[mMhHdDwW]?)+[ \t]+)?[iI][nN][ \t]+[a-zA-Z]+[ \t]+(([^;]+)|(\".*\")|(\\(.*\\)))[ \t]*(;.*)?, (\\*\\.)?([a-zA-Z0-9\\._-]+|@)[ \t]+[iI][nN][ \t]+(([1-9][0-9]*[mMhHdDwW]?)+[ \t]+)?[a-zA-Z]+[ \t]+(([^;]+)|(\".*\")|(\\(.*\\)))[ \t]*(;.*)?] but '@ 1814400 IN 1814400 BAD1 TTL only allowed once' does not match any", "'DOMAIN_DNS_SETUP:example.org|DNS.config.user-RR' is expected to match any of [(\\*\\.)?([a-zA-Z0-9\\._-]+|@)[ \t]+(([1-9][0-9]*[mMhHdDwW]?)+[ \t]+)?[iI][nN][ \t]+[a-zA-Z]+[ \t]+(([^;]+)|(\".*\")|(\\(.*\\)))[ \t]*(;.*)?, (\\*\\.)?([a-zA-Z0-9\\._-]+|@)[ \t]+[iI][nN][ \t]+(([1-9][0-9]*[mMhHdDwW]?)+[ \t]+)?[a-zA-Z]+[ \t]+(([^;]+)|(\".*\")|(\\(.*\\)))[ \t]*(;.*)?] but 'www BAD1 Record-Class missing / not enough columns' does not match any"); } diff --git a/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsDomainHttpSetupHostingAssetValidatorUnitTest.java b/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsDomainHttpSetupHostingAssetValidatorUnitTest.java index 91fecdd5..19ad09df 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsDomainHttpSetupHostingAssetValidatorUnitTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsDomainHttpSetupHostingAssetValidatorUnitTest.java @@ -88,7 +88,7 @@ class HsDomainHttpSetupHostingAssetValidatorUnitTest { // then assertThat(result).containsExactly( - "'identifier' expected to match '^\\Qexample.org|HTTP\\E$', but is 'example.org'" + "'identifier' expected to match '^\\Qexample.org|HTTP\\E$' but is 'example.org'" ); } @@ -155,7 +155,7 @@ class HsDomainHttpSetupHostingAssetValidatorUnitTest { // then assertThat(result).containsExactlyInAnyOrder( - "'DOMAIN_HTTP_SETUP:example.org|HTTP.config.htdocsfallback' is expected to be of type Boolean, but is of type String", + "'DOMAIN_HTTP_SETUP:example.org|HTTP.config.htdocsfallback' is expected to be of type Boolean but is of type String", "'DOMAIN_HTTP_SETUP:example.org|HTTP.config.fcgi-php-bin' is expected to match [^/.*] but 'false' does not match", "'DOMAIN_HTTP_SETUP:example.org|HTTP.config.subdomains' is expected to match [(\\*|(?!-)[A-Za-z0-9-]{1,63}(? assertThat(!free || haType == MANAGED_WEBSPACE || defaultPrefix(bookingItem) .equals("hsh")) - .as("packet.free only supported for Hostsharing-Assets and ManagedWebspace in customer-ManagedServer, but is set for " + .as("packet.free only supported for Hostsharing-Assets and ManagedWebspace in customer-ManagedServerbut is set for " + packet_name) .isTrue()); @@ -1739,7 +1739,7 @@ public class ImportHostingAssets extends CsvDataImport { return givenValue != null && !givenValue.isBlank() ? givenValue : defaultStringValue; } throw new RuntimeException( - "property default value expected to be of type string, but is of type " + defaultValue.getClass() + "property default value expected to be of type stringbut is of type " + defaultValue.getClass() .getSimpleName()); } diff --git a/src/test/java/net/hostsharing/hsadminng/hs/migration/PostgresTestcontainer.java b/src/test/java/net/hostsharing/hsadminng/hs/migration/PostgresTestcontainer.java index 8de3e5ba..dafb1afc 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/migration/PostgresTestcontainer.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/migration/PostgresTestcontainer.java @@ -64,7 +64,7 @@ public class PostgresTestcontainer { } private static void makeDir(final File dir) { - assertThat(!dir.exists() || dir.isDirectory()).describedAs(dir + " does exist, but is not a directory").isTrue(); + assertThat(!dir.exists() || dir.isDirectory()).describedAs(dir + " does exist but is not a directory").isTrue(); assertThat(dir.isDirectory() || dir.mkdirs()).describedAs(dir + " cannot be created").isTrue(); } diff --git a/src/test/java/net/hostsharing/hsadminng/hs/office/bankaccount/HsOfficeBankAccountControllerRestTest.java b/src/test/java/net/hostsharing/hsadminng/hs/office/bankaccount/HsOfficeBankAccountControllerRestTest.java index ad2fece0..be448e0d 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/office/bankaccount/HsOfficeBankAccountControllerRestTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/office/bankaccount/HsOfficeBankAccountControllerRestTest.java @@ -1,5 +1,6 @@ package net.hostsharing.hsadminng.hs.office.bankaccount; +import net.hostsharing.hsadminng.config.MessageTranslator; import net.hostsharing.hsadminng.context.Context; import net.hostsharing.hsadminng.mapper.StrictMapper; import net.hostsharing.hsadminng.config.DisableSecurityConfig; @@ -19,7 +20,7 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers. import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @WebMvcTest(HsOfficeBankAccountController.class) -@Import(DisableSecurityConfig.class) +@Import({DisableSecurityConfig.class, MessageTranslator.class}) @ActiveProfiles("test") class HsOfficeBankAccountControllerRestTest { diff --git a/src/test/java/net/hostsharing/hsadminng/hs/office/coopassets/HsOfficeCoopAssetsTransactionControllerAcceptanceTest.java b/src/test/java/net/hostsharing/hsadminng/hs/office/coopassets/HsOfficeCoopAssetsTransactionControllerAcceptanceTest.java index 7336f39d..1c414db8 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/office/coopassets/HsOfficeCoopAssetsTransactionControllerAcceptanceTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/office/coopassets/HsOfficeCoopAssetsTransactionControllerAcceptanceTest.java @@ -3,6 +3,7 @@ package net.hostsharing.hsadminng.hs.office.coopassets; import io.restassured.RestAssured; import io.restassured.http.ContentType; import net.hostsharing.hsadminng.HsadminNgApplication; +import net.hostsharing.hsadminng.config.MessageTranslator; import net.hostsharing.hsadminng.context.Context; import net.hostsharing.hsadminng.hs.office.membership.HsOfficeMembershipRepository; import net.hostsharing.hsadminng.rbac.test.ContextBasedTestWithCleanup; @@ -34,7 +35,8 @@ import static org.hamcrest.Matchers.startsWith; @SpringBootTest( webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, - classes = { HsadminNgApplication.class, DisableSecurityConfig.class, JpaAttempt.class } + classes = { HsadminNgApplication.class, DisableSecurityConfig.class, JpaAttempt.class, + MessageTranslator.class} ) @ActiveProfiles("test") @Transactional @@ -355,6 +357,7 @@ class HsOfficeCoopAssetsTransactionControllerAcceptanceTest extends ContextBased RestAssured // @formatter:off .given() .header("Authorization", "Bearer superuser-alex@hostsharing.net") + .header("Accept-Language", "de") .contentType(ContentType.JSON) .body(""" { @@ -376,7 +379,7 @@ class HsOfficeCoopAssetsTransactionControllerAcceptanceTest extends ContextBased { "statusCode": 400, "statusPhrase": "Bad Request", - "message": "ERROR: [400] coop assets transaction would result in a negative balance of assets" + "message": "ERROR: [400] Geschäftsguthaben-Transaktion würde zu einem negativen Geschäftsguthaben-Saldo führen" } """)); // @formatter:on } diff --git a/src/test/java/net/hostsharing/hsadminng/hs/office/coopassets/HsOfficeCoopAssetsTransactionControllerRestTest.java b/src/test/java/net/hostsharing/hsadminng/hs/office/coopassets/HsOfficeCoopAssetsTransactionControllerRestTest.java index 33135f67..2855f34c 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/office/coopassets/HsOfficeCoopAssetsTransactionControllerRestTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/office/coopassets/HsOfficeCoopAssetsTransactionControllerRestTest.java @@ -1,10 +1,12 @@ package net.hostsharing.hsadminng.hs.office.coopassets; import net.hostsharing.hsadminng.config.JsonObjectMapperConfiguration; +import net.hostsharing.hsadminng.config.MessageTranslator; import net.hostsharing.hsadminng.context.Context; import net.hostsharing.hsadminng.hs.office.membership.HsOfficeMembershipEntity; import net.hostsharing.hsadminng.hs.office.membership.HsOfficeMembershipRepository; import net.hostsharing.hsadminng.hs.office.partner.HsOfficePartnerRealEntity; +import net.hostsharing.hsadminng.config.MessagesResourceConfig; import net.hostsharing.hsadminng.mapper.StrictMapper; import net.hostsharing.hsadminng.persistence.EntityManagerWrapper; import net.hostsharing.hsadminng.rbac.test.JsonBuilder; @@ -42,6 +44,7 @@ import static net.hostsharing.hsadminng.test.JsonMatcher.lenientlyEquals; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assumptions.assumeThat; import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.startsWith; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.when; @@ -49,7 +52,11 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers. import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @WebMvcTest(HsOfficeCoopAssetsTransactionController.class) -@Import({ StrictMapper.class, JsonObjectMapperConfiguration.class, DisableSecurityConfig.class }) +@Import({ StrictMapper.class, + MessagesResourceConfig.class, + MessageTranslator.class, + JsonObjectMapperConfiguration.class, + DisableSecurityConfig.class }) @ActiveProfiles("test") @RunWith(SpringRunner.class) class HsOfficeCoopAssetsTransactionControllerRestTest { @@ -531,11 +538,12 @@ class HsOfficeCoopAssetsTransactionControllerRestTest { enum BadRequestTestCases { MEMBERSHIP_UUID_MISSING( requestBody -> requestBody.without("membership.uuid"), - "[membershipUuid must not be null but is \"null\"]"), // TODO.impl: should be membership.uuid, Spring validation-problem? + // TODO.impl: should be membership.uuid, but the Hibernate validator does not use the name from @JsonProperty + "[membershipUuid darf nicht null sein"), // bracket because it's from a list of violations MEMBERSHIP_UUID_NOT_FOUND_OR_NOT_ACCESSIBLE( requestBody -> requestBody.with("membership.uuid", UNAVAILABLE_UUID), - "membership.uuid " + UNAVAILABLE_UUID + " not found"), + "membership.uuid \"" + UNAVAILABLE_UUID + "\" nicht gefunden"), MEMBERSHIP_UUID_AND_MEMBER_NUMBER_MUST_NOT_BE_GIVEN_BOTH( requestBody -> requestBody @@ -543,92 +551,92 @@ class HsOfficeCoopAssetsTransactionControllerRestTest { .with("assetValue", "-128.00") .with("adoptingMembership.uuid", UNAVAILABLE_UUID) .with("adoptingMembership.memberNumber", UNAVAILABLE_MEMBER_NUMBER), - "either adoptingMembership.uuid or adoptingMembership.memberNumber can be given, not both"), + "entweder adoptingMembership.uuid oder adoptingMembership.memberNumber muss angegeben werden, nicht beide"), MEMBERSHIP_UUID_OR_MEMBER_NUMBER_MUST_BE_GIVEN( requestBody -> requestBody .with("transactionType", TRANSFER) .with("assetValue", "-128.00"), - "either adoptingMembership.uuid or adoptingMembership.memberNumber must be given for transactionType=TRANSFER"), + "für transactionType=TRANSFER muss entweder adoptingMembership.uuid oder adoptingMembership.memberNumber angegeben werden"), REVERSAL_ASSET_TRANSACTION_REQUIRES_REVERTED_ASSET_TX_UUID( requestBody -> requestBody .with("transactionType", REVERSAL) .with("assetValue", "-128.00"), - "REVERSAL asset transaction requires revertedAssetTx.uuid"), + "eine REVERSAL Geschäftsguthaben-Transaktion erfordert die Angabe einer revertedAssetTx.uuid"), REVERSAL_ASSET_TRANSACTION_REQUIRES_AVAILABLE_REVERTED_ASSET_TX_UUID( requestBody -> requestBody .with("transactionType", REVERSAL) .with("assetValue", "-128.00") .with("revertedAssetTx.uuid", UNAVAILABLE_UUID), - "revertedAssetTx.uuid " + UNAVAILABLE_UUID + " not found"), + "revertedAssetTx.uuid \"" + UNAVAILABLE_UUID + "\" nicht gefunden"), REVERSAL_ASSET_TRANSACTION_MUST_NEGATE_VALUE_OF_REVERTED_ASSET_TX( requestBody -> requestBody .with("transactionType", REVERSAL) .with("assetValue", "128.00") .with("revertedAssetTx.uuid", SOME_EXISTING_LOSS_ASSET_TX_UUID), - "given assetValue=128.00 but must be negative value from reverted asset tx: -64"), + "assetValue=128,00 muss dem negativen Wert des Wertes der stornierten Geschäftsguthaben-Transaktion entsprechen: -64,00"), TRANSACTION_TYPE_MISSING( requestBody -> requestBody.without("transactionType"), - "[transactionType must not be null but is \"null\"]"), + "[transactionType darf nicht null sein"), VALUE_DATE_MISSING( requestBody -> requestBody.without("valueDate"), - "[valueDate must not be null but is \"null\"]"), + "[valueDate darf nicht null sein"), ASSETS_VALUE_FOR_DEPOSIT_MUST_BE_POSITIVE( requestBody -> requestBody .with("transactionType", DEPOSIT) .with("assetValue", -64.00), - "[for DEPOSIT, assetValue must be positive but is \"-64.00\"]"), + "[für transactionType=DEPOSIT, muss assetValue positiv sein, ist aber -64,00]"), ASSETS_VALUE_FOR_DISBURSAL_MUST_BE_NEGATIVE( requestBody -> requestBody .with("transactionType", DISBURSAL) .with("assetValue", 64.00), - "[for DISBURSAL, assetValue must be negative but is \"64.00\"]"), + "[für transactionType=DISBURSAL, muss assetValue negativ sein, ist aber 64,00]"), ADOPTING_MEMBERSHIP_MUST_NOT_BE_THE_SAME( requestBody -> requestBody .with("transactionType", TRANSFER) .with("assetValue", -64.00) .with("adoptingMembership.uuid", ORIGIN_MEMBERSHIP_UUID), - "transferring and adopting membership must be different, but both are M-1111100"), + "übertragende und annehmende Mitgliedschaft müssen unterschiedlich sein, aber beide sind M-1111100"), ADOPTING_MEMBERSHIP_NUMBER_FOR_TRANSFER_MUST_BE_GIVEN_AND_AVAILABLE( requestBody -> requestBody .with("transactionType", TRANSFER) .with("assetValue", -64.00) .with("adoptingMembership.memberNumber", UNAVAILABLE_MEMBER_NUMBER), - "adoptingMembership.memberNumber='M-1234699' not found or not accessible"), + "adoptingMembership.memberNumber \"M-1234699\" nicht gefunden oder nicht zugänglich"), ADOPTING_MEMBERSHIP_UUID_FOR_TRANSFER_MUST_BE_GIVEN_AND_AVAILABLE( requestBody -> requestBody .with("transactionType", TRANSFER) .with("assetValue", -64.00) .with("adoptingMembership.uuid", UNAVAILABLE_UUID), - "adoptingMembership.uuid='" + UNAVAILABLE_UUID + "' not found or not accessible"), + "adoptingMembership.uuid \"" + UNAVAILABLE_UUID + "\" nicht gefunden oder nicht zugänglich"), ASSETS_VALUE_MUST_NOT_BE_NULL( requestBody -> requestBody .with("transactionType", REVERSAL) .with("assetValue", 0.00), - "[assetValue must not be 0 but is \"0.00\"]"), + "[assetValue darf nicht 0 sein]"), REFERENCE_MISSING( requestBody -> requestBody.without("reference"), - "[reference must not be null but is \"null\"]"), + "[reference darf nicht null sein"), REFERENCE_TOO_SHORT( requestBody -> requestBody.with("reference", "12345"), - "[reference size must be between 6 and 48 but is \"12345\"]"), + "[reference Größe muss zwischen 6 und 48 sein"), // OpenAPI Spring templates uses @Size, but should use @Length REFERENCE_TOO_LONG( requestBody -> requestBody.with("reference", "0123456789012345678901234567890123456789012345678"), - "[reference size must be between 6 and 48 but is \"0123456789012345678901234567890123456789012345678\"]"); + "[reference Größe muss zwischen 6 und 48 sein"); // OpenAPI Spring templates uses @Size, but should use @Length private final Function givenBodyTransformation; private final String expectedErrorMessage; @@ -652,12 +660,13 @@ class HsOfficeCoopAssetsTransactionControllerRestTest { // - set SINGLE_TEST_CASE_EXECUTION to true - see above // - select the test case enum value you want to run assumeThat(!SINGLE_TEST_CASE_EXECUTION || - testCase == BadRequestTestCases.ADOPTING_MEMBERSHIP_MUST_NOT_BE_THE_SAME).isTrue(); + testCase == BadRequestTestCases.MEMBERSHIP_UUID_OR_MEMBER_NUMBER_MUST_BE_GIVEN).isTrue(); // when mockMvc.perform(MockMvcRequestBuilders .post("/api/hs/office/coopassetstransactions") .header("Authorization", "Bearer superuser-alex@hostsharing.net") + .header("Accept-Language", "de") .contentType(MediaType.APPLICATION_JSON) .content(testCase.givenRequestBody()) .accept(MediaType.APPLICATION_JSON)) @@ -665,7 +674,7 @@ class HsOfficeCoopAssetsTransactionControllerRestTest { // then .andExpect(jsonPath("statusCode", is(400))) .andExpect(jsonPath("statusPhrase", is("Bad Request"))) - .andExpect(jsonPath("message", is("ERROR: [400] " + testCase.expectedErrorMessage))) + .andExpect(jsonPath("message", startsWith("ERROR: [400] " + testCase.expectedErrorMessage))) .andExpect(status().is4xxClientError()); } @@ -944,4 +953,5 @@ class HsOfficeCoopAssetsTransactionControllerRestTest { private String suffixOf(final String memberNumber) { return memberNumber.substring("M-".length() + 5); } + } diff --git a/src/test/java/net/hostsharing/hsadminng/hs/office/coopshares/HsOfficeCoopSharesTransactionControllerRestTest.java b/src/test/java/net/hostsharing/hsadminng/hs/office/coopshares/HsOfficeCoopSharesTransactionControllerRestTest.java index b005bbfd..e3306853 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/office/coopshares/HsOfficeCoopSharesTransactionControllerRestTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/office/coopshares/HsOfficeCoopSharesTransactionControllerRestTest.java @@ -1,5 +1,6 @@ package net.hostsharing.hsadminng.hs.office.coopshares; +import net.hostsharing.hsadminng.config.MessageTranslator; import net.hostsharing.hsadminng.context.Context; import net.hostsharing.hsadminng.hs.office.membership.HsOfficeMembershipRepository; import net.hostsharing.hsadminng.mapper.StrictMapper; @@ -20,12 +21,13 @@ import java.util.UUID; import java.util.function.Function; import static net.hostsharing.hsadminng.rbac.test.JsonBuilder.jsonObject; +import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.is; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @WebMvcTest(HsOfficeCoopSharesTransactionController.class) -@Import(DisableSecurityConfig.class) +@Import({DisableSecurityConfig.class, MessageTranslator.class}) @ActiveProfiles("test") class HsOfficeCoopSharesTransactionControllerRestTest { @@ -59,45 +61,45 @@ class HsOfficeCoopSharesTransactionControllerRestTest { enum BadRequestTestCases { MEMBERSHIP_UUID_MISSING( requestBody -> requestBody.without("membership.uuid"), - "[membershipUuid must not be null but is \"null\"]"), + "membershipUuid must not be null"), TRANSACTION_TYPE_MISSING( requestBody -> requestBody.without("transactionType"), - "[transactionType must not be null but is \"null\"]"), + "transactionType must not be null"), VALUE_DATE_MISSING( requestBody -> requestBody.without("valueDate"), - "[valueDate must not be null but is \"null\"]"), + "valueDate must not be null"), SHARES_COUNT_FOR_SUBSCRIPTION_MUST_BE_POSITIVE( requestBody -> requestBody .with("transactionType", "SUBSCRIPTION") .with("shareCount", -1), - "[for SUBSCRIPTION, shareCount must be positive but is \"-1\"]"), + "for SUBSCRIPTION, shareCount must be positive but is \"-1\""), SHARES_COUNT_FOR_CANCELLATION_MUST_BE_NEGATIVE( requestBody -> requestBody .with("transactionType", "CANCELLATION") .with("shareCount", 1), - "[for CANCELLATION, shareCount must be negative but is \"1\"]"), + "for CANCELLATION, shareCount must be negative but is \"1\""), SHARES_COUNT_MUST_NOT_BE_NULL( requestBody -> requestBody .with("transactionType", "REVERSAL") .with("shareCount", 0), - "[shareCount must not be 0 but is \"0\"]"), + "shareCount must not be 0 but is \"0\""), REFERENCE_MISSING( requestBody -> requestBody.without("reference"), - "[reference must not be null but is \"null\"]"), + "reference must not be null"), REFERENCE_TOO_SHORT( requestBody -> requestBody.with("reference", "12345"), - "[reference size must be between 6 and 48 but is \"12345\"]"), + "reference size must be between 6 and 48 but is \"12345\""), REFERENCE_TOO_LONG( requestBody -> requestBody.with("reference", "0123456789012345678901234567890123456789012345678"), - "[reference size must be between 6 and 48 but is \"0123456789012345678901234567890123456789012345678\"]"); + "reference size must be between 6 and 48 but is \"0123456789012345678901234567890123456789012345678\""); private final Function givenBodyTransformation; private final String expectedErrorMessage; @@ -130,7 +132,7 @@ class HsOfficeCoopSharesTransactionControllerRestTest { .andExpect(status().is4xxClientError()) .andExpect(jsonPath("statusCode", is(400))) .andExpect(jsonPath("statusPhrase", is("Bad Request"))) - .andExpect(jsonPath("message", is("ERROR: [400] " + testCase.expectedErrorMessage))); + .andExpect(jsonPath("message", containsString(testCase.expectedErrorMessage))); } } diff --git a/src/test/java/net/hostsharing/hsadminng/hs/office/membership/HsOfficeMembershipControllerRestTest.java b/src/test/java/net/hostsharing/hsadminng/hs/office/membership/HsOfficeMembershipControllerRestTest.java index 98938725..302d2c34 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/office/membership/HsOfficeMembershipControllerRestTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/office/membership/HsOfficeMembershipControllerRestTest.java @@ -1,5 +1,6 @@ package net.hostsharing.hsadminng.hs.office.membership; +import net.hostsharing.hsadminng.config.MessageTranslator; import net.hostsharing.hsadminng.context.Context; import net.hostsharing.hsadminng.hs.office.coopassets.HsOfficeCoopAssetsTransactionRepository; import net.hostsharing.hsadminng.hs.office.partner.HsOfficePartnerRbacEntity; @@ -30,13 +31,14 @@ import static net.hostsharing.hsadminng.test.JsonMatcher.lenientlyEquals; import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.hasSize; import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.startsWith; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.when; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @WebMvcTest(HsOfficeMembershipController.class) -@Import({StrictMapper.class, DisableSecurityConfig.class}) +@Import({StrictMapper.class, DisableSecurityConfig.class, MessageTranslator.class}) @ActiveProfiles("test") public class HsOfficeMembershipControllerRestTest { @@ -74,6 +76,9 @@ public class HsOfficeMembershipControllerRestTest { @MockitoBean Context contextMock; + @Autowired + MessageTranslator messageTranslator; + @MockitoBean HsOfficeCoopAssetsTransactionRepository coopAssetsTransactionRepo; @@ -249,7 +254,7 @@ public class HsOfficeMembershipControllerRestTest { .andExpect(jsonPath("statusCode", is(400))) .andExpect(jsonPath("statusPhrase", is("Bad Request"))) // FYI: the brackets around the message are here because it's actually an array, in this case of size 1 - .andExpect(jsonPath("message", is("ERROR: [400] [partnerUuid must not be null but is \"null\"]"))); + .andExpect(jsonPath("message", startsWith("ERROR: [400] [partnerUuid must not be null"))); } @Test @@ -310,7 +315,7 @@ public class HsOfficeMembershipControllerRestTest { } public enum InvalidMemberSuffixVariants { - MISSING("", "[memberNumberSuffix must not be null but is \"null\"]"), + MISSING("", "[memberNumberSuffix must not be null"), TOO_SMALL("\"memberNumberSuffix\": \"9\",", "memberNumberSuffix must match \"[0-9]{2}\" but is \"9\""), TOO_LARGE("\"memberNumberSuffix\": \"100\",", "memberNumberSuffix must match \"[0-9]{2}\" but is \"100\""), NOT_NUMERIC("\"memberNumberSuffix\": \"AA\",", "memberNumberSuffix must match \"[0-9]{2}\" but is \"AA\""), diff --git a/src/test/java/net/hostsharing/hsadminng/hs/office/partner/HsOfficePartnerControllerRestTest.java b/src/test/java/net/hostsharing/hsadminng/hs/office/partner/HsOfficePartnerControllerRestTest.java index 9686ed79..b63a3d7e 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/office/partner/HsOfficePartnerControllerRestTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/office/partner/HsOfficePartnerControllerRestTest.java @@ -1,5 +1,7 @@ package net.hostsharing.hsadminng.hs.office.partner; +import net.hostsharing.hsadminng.config.DisableSecurityConfig; +import net.hostsharing.hsadminng.config.MessageTranslator; import net.hostsharing.hsadminng.context.Context; import net.hostsharing.hsadminng.hs.office.contact.HsOfficeContactFromResourceConverter; import net.hostsharing.hsadminng.hs.office.contact.HsOfficeContactRbacEntity; @@ -8,17 +10,18 @@ import net.hostsharing.hsadminng.hs.office.relation.HsOfficeRelationRealEntity; import net.hostsharing.hsadminng.hs.office.relation.HsOfficeRelationRealRepository; import net.hostsharing.hsadminng.mapper.StrictMapper; import net.hostsharing.hsadminng.persistence.EntityManagerWrapper; -import net.hostsharing.hsadminng.config.DisableSecurityConfig; +import net.hostsharing.hsadminng.config.MessagesResourceConfig; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.mockito.Mock; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; -import org.springframework.test.context.bean.override.mockito.MockitoBean; +import org.springframework.context.MessageSource; import org.springframework.context.annotation.Import; import org.springframework.http.MediaType; import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.bean.override.mockito.MockitoBean; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; @@ -30,7 +33,7 @@ import java.util.Optional; import java.util.UUID; import static org.hamcrest.Matchers.is; -import static org.hamcrest.Matchers.startsWith; +import static org.hamcrest.Matchers.equalTo; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.lenient; @@ -39,7 +42,11 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers. import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @WebMvcTest(HsOfficePartnerController.class) -@Import({ StrictMapper.class, HsOfficeContactFromResourceConverter.class, DisableSecurityConfig.class}) +@Import({ StrictMapper.class, + MessagesResourceConfig.class, + MessageTranslator.class, + HsOfficeContactFromResourceConverter.class, + DisableSecurityConfig.class }) @ActiveProfiles("test") class HsOfficePartnerControllerRestTest { @@ -54,6 +61,12 @@ class HsOfficePartnerControllerRestTest { @MockitoBean Context contextMock; + @Autowired + MessageSource messageSource; + + @Autowired + MessageTranslator translator; + @MockitoBean HsOfficePartnerRbacRepository partnerRepo; @@ -100,6 +113,7 @@ class HsOfficePartnerControllerRestTest { mockMvc.perform(MockMvcRequestBuilders .post("/api/hs/office/partners") .header("Authorization", "Bearer superuser-alex@hostsharing.net") + .header("Accept-Language", "de") .contentType(MediaType.APPLICATION_JSON) .content(""" { @@ -124,7 +138,8 @@ class HsOfficePartnerControllerRestTest { .andExpect(status().is4xxClientError()) .andExpect(jsonPath("statusCode", is(400))) .andExpect(jsonPath("statusPhrase", is("Bad Request"))) - .andExpect(jsonPath("message", startsWith("ERROR: [400] Cannot resolve HsOfficePersonRealEntity with uuid "))); + .andExpect(jsonPath("message", equalTo( + "ERROR: [400] RealPerson \"00000000-0000-0000-0000-000000000000\" nicht gefunden"))); } @Test @@ -157,7 +172,8 @@ class HsOfficePartnerControllerRestTest { .andExpect(status().is4xxClientError()) .andExpect(jsonPath("statusCode", is(400))) .andExpect(jsonPath("statusPhrase", is("Bad Request"))) - .andExpect(jsonPath("message", startsWith("ERROR: [400] Cannot resolve HsOfficeContactRealEntity with uuid "))); + .andExpect(jsonPath("message", equalTo( + "ERROR: [400] RealContact \"00000000-0000-0000-0000-000000000000\" not found"))); } } @@ -173,14 +189,14 @@ class HsOfficePartnerControllerRestTest { // when mockMvc.perform(MockMvcRequestBuilders - .get("/api/hs/office/partners/P-12345") - .header("Authorization", "Bearer superuser-alex@hostsharing.net") - .contentType(MediaType.APPLICATION_JSON) - .accept(MediaType.APPLICATION_JSON)) + .get("/api/hs/office/partners/P-12345") + .header("Authorization", "Bearer superuser-alex@hostsharing.net") + .contentType(MediaType.APPLICATION_JSON) + .accept(MediaType.APPLICATION_JSON)) - // then - .andExpect(status().isOk()) - .andExpect(jsonPath("partnerNumber", is("P-12345"))); + // then + .andExpect(status().isOk()) + .andExpect(jsonPath("partnerNumber", is("P-12345"))); } @Test @@ -190,13 +206,13 @@ class HsOfficePartnerControllerRestTest { // when mockMvc.perform(MockMvcRequestBuilders - .get("/api/hs/office/partners/P-12345") - .header("Authorization", "Bearer superuser-alex@hostsharing.net") - .contentType(MediaType.APPLICATION_JSON) - .accept(MediaType.APPLICATION_JSON)) + .get("/api/hs/office/partners/P-12345") + .header("Authorization", "Bearer superuser-alex@hostsharing.net") + .contentType(MediaType.APPLICATION_JSON) + .accept(MediaType.APPLICATION_JSON)) - // then - .andExpect(status().isNotFound()); + // then + .andExpect(status().isNotFound()); } } diff --git a/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/HsOfficeScenarioTests.java b/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/HsOfficeScenarioTests.java index 4c96abc3..ee9af1d1 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/HsOfficeScenarioTests.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/HsOfficeScenarioTests.java @@ -273,7 +273,7 @@ class HsOfficeScenarioTests extends ScenarioTest { void shouldCreateSelfDebitorForPartnerWithIdenticalContactData() { new CreateSelfDebitorForPartnerWithIdenticalContactData(scenarioTest) .given("partnerNumber", "P-31011") - .given("debitorNumberSuffix", "00") // TODO.impl: could be assigned automatically, but is not yet + .given("debitorNumberSuffix", "00") // TODO.impl: could be assigned automatically but is not yet .given("billable", true) .given("vatBusiness", false) .given("vatReverseCharge", false) @@ -291,7 +291,7 @@ class HsOfficeScenarioTests extends ScenarioTest { .given("partnerPersonTradeName", "Test AG") .given("billingContactCaption", "Test AG - billing department") .given("billingContactEmailAddress", "billing@test-ag.example.org") - .given("debitorNumberSuffix", "00") // TODO.impl: could be assigned automatically, but is not yet + .given("debitorNumberSuffix", "00") // TODO.impl: could be assigned automaticallybut is not yet .given("billable", true) .given("vatId", "VAT123456") .given("vatCountryCode", "DE") diff --git a/src/test/java/net/hostsharing/hsadminng/ping/PingControllerAcceptanceTest.java b/src/test/java/net/hostsharing/hsadminng/ping/PingControllerAcceptanceTest.java new file mode 100644 index 00000000..df7516bf --- /dev/null +++ b/src/test/java/net/hostsharing/hsadminng/ping/PingControllerAcceptanceTest.java @@ -0,0 +1,75 @@ +package net.hostsharing.hsadminng.ping; + +import io.restassured.RestAssured; +import net.hostsharing.hsadminng.HsadminNgApplication; +import net.hostsharing.hsadminng.config.DisableSecurityConfig; +import net.hostsharing.hsadminng.context.Context; +import net.hostsharing.hsadminng.rbac.test.JpaAttempt; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.EnumSource; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.web.server.LocalServerPort; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.transaction.annotation.Transactional; + +import java.util.Locale; + +import static org.assertj.core.api.Assertions.assertThat; + +@SpringBootTest( + webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, + classes = { + HsadminNgApplication.class, + DisableSecurityConfig.class, + JpaAttempt.class + } +) +@ActiveProfiles("test") +@Transactional +@Tag("generalIntegrationTest") +class PingControllerAcceptanceTest { + + @LocalServerPort + private Integer port; + + @Autowired + Context context; + + @Autowired + Context contextMock; + + enum PingTranslationTestCase { + EN(Locale.ENGLISH, "pong superuser-alex@hostsharing.net - in English"), + DE(Locale.GERMAN, "pong superuser-alex@hostsharing.net - auf Deutsch"), + FR(Locale.FRENCH, "pong superuser-alex@hostsharing.net - in English [fr translation missing]"); + + Locale givenLocale; + CharSequence expectedPongTranslation; + + PingTranslationTestCase(final Locale givenLocale, final String expectedPongTranslation) { + this.givenLocale = givenLocale; + this.expectedPongTranslation = expectedPongTranslation; + } + } + + @ParameterizedTest + @EnumSource(PingTranslationTestCase.class) + void pingRepliesWithTranslatedPongResponse(final PingTranslationTestCase testCase) { + final var responseBody = RestAssured // @formatter:off + .given() + .header("Authorization", "Bearer superuser-alex@hostsharing.net") + .header("Accept-Language", testCase.givenLocale) + .port(port) + .when() + .get("http://localhost/api/ping") + .then().log().all().assertThat() + .statusCode(200) + .contentType("text/plain;charset=UTF-8") + .extract().body().asString(); + // @formatter:on + + assertThat(responseBody).isEqualTo(testCase.expectedPongTranslation + "\n"); + } +} diff --git a/src/test/java/net/hostsharing/hsadminng/ping/PingControllerRestTest.java b/src/test/java/net/hostsharing/hsadminng/ping/PingControllerRestTest.java new file mode 100644 index 00000000..5715befd --- /dev/null +++ b/src/test/java/net/hostsharing/hsadminng/ping/PingControllerRestTest.java @@ -0,0 +1,67 @@ +package net.hostsharing.hsadminng.ping; + +import lombok.RequiredArgsConstructor; +import net.hostsharing.hsadminng.config.DisableSecurityConfig; +import net.hostsharing.hsadminng.config.JsonObjectMapperConfiguration; +import net.hostsharing.hsadminng.config.MessageTranslator; +import net.hostsharing.hsadminng.config.MessagesResourceConfig; +import net.hostsharing.hsadminng.context.Context; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.EnumSource; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.context.annotation.Import; +import org.springframework.http.MediaType; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.bean.override.mockito.MockitoBean; +import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; + +import static org.hamcrest.Matchers.startsWith; +import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@WebMvcTest(PingController.class) +@Import({ MessagesResourceConfig.class, + MessageTranslator.class, + JsonObjectMapperConfiguration.class, + DisableSecurityConfig.class }) +@RunWith(SpringRunner.class) +@ActiveProfiles("test") +class PingControllerRestTest { + + @Autowired + MockMvc mockMvc; + + @MockitoBean + Context contextMock; + + @RequiredArgsConstructor + enum I18nTestCases { + EN("en", "pong anonymousUser - in English"), + DE("de", "pong anonymousUser - auf Deutsch"); + + final String language; + final String expectedTranslation; + } + + @ParameterizedTest + @EnumSource(I18nTestCases.class) + void pingReturnsPongInEnglish(final I18nTestCases testCase) throws Exception { + + // when + final var request = mockMvc.perform(MockMvcRequestBuilders + .get("/api/ping") + .header("Accept-Language", testCase.language) + .accept(MediaType.TEXT_PLAIN)) + .andDo(print()); + + // then + request + .andExpect(status().isOk()) + .andExpect(content().string(startsWith(testCase.expectedTranslation))); + } +} diff --git a/src/test/java/net/hostsharing/hsadminng/rbac/role/RbacRoleControllerRestTest.java b/src/test/java/net/hostsharing/hsadminng/rbac/role/RbacRoleControllerRestTest.java index 318849e7..cd7b12b3 100644 --- a/src/test/java/net/hostsharing/hsadminng/rbac/role/RbacRoleControllerRestTest.java +++ b/src/test/java/net/hostsharing/hsadminng/rbac/role/RbacRoleControllerRestTest.java @@ -1,5 +1,6 @@ package net.hostsharing.hsadminng.rbac.role; +import net.hostsharing.hsadminng.config.MessageTranslator; import net.hostsharing.hsadminng.context.Context; import net.hostsharing.hsadminng.mapper.StrictMapper; import net.hostsharing.hsadminng.persistence.EntityManagerWrapper; @@ -31,7 +32,7 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers. import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @WebMvcTest(RbacRoleController.class) -@Import({StrictMapper.class, DisableSecurityConfig.class}) +@Import({StrictMapper.class, DisableSecurityConfig.class, MessageTranslator.class}) @ActiveProfiles("test") @RunWith(SpringRunner.class) class RbacRoleControllerRestTest { diff --git a/src/test/java/net/hostsharing/hsadminng/rbac/subject/RbacSubjectControllerRestTest.java b/src/test/java/net/hostsharing/hsadminng/rbac/subject/RbacSubjectControllerRestTest.java index 01156fbd..6d7f30fe 100644 --- a/src/test/java/net/hostsharing/hsadminng/rbac/subject/RbacSubjectControllerRestTest.java +++ b/src/test/java/net/hostsharing/hsadminng/rbac/subject/RbacSubjectControllerRestTest.java @@ -1,5 +1,6 @@ package net.hostsharing.hsadminng.rbac.subject; +import net.hostsharing.hsadminng.config.MessageTranslator; import net.hostsharing.hsadminng.context.Context; import net.hostsharing.hsadminng.mapper.StrictMapper; import net.hostsharing.hsadminng.persistence.EntityManagerWrapper; @@ -27,7 +28,7 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers. import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @WebMvcTest(RbacSubjectController.class) -@Import({StrictMapper.class, DisableSecurityConfig.class}) +@Import({StrictMapper.class, DisableSecurityConfig.class, MessageTranslator.class}) @ActiveProfiles("test") @RunWith(SpringRunner.class) class RbacSubjectControllerRestTest {