1
0

scoped programmatic i18n-keys (#190)

Co-authored-by: Michael Hoennig <michael@hoennig.de>
Reviewed-on: https://dev.hostsharing.net/hostsharing/hs.hsadmin.ng/pulls/190
Reviewed-by: Timotheus Pokorra <timotheus.pokorra@hostsharing.net>
This commit is contained in:
Michael Hoennig
2025-08-26 15:28:42 +02:00
parent 2a6e86aca8
commit 68e642c034
29 changed files with 283 additions and 104 deletions
@@ -1,5 +1,7 @@
package net.hostsharing.hsadminng.config; package net.hostsharing.hsadminng.config;
import lombok.extern.slf4j.Slf4j;
import lombok.val;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.MessageSource; import org.springframework.context.MessageSource;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
@@ -10,6 +12,7 @@ import java.util.Locale;
@Service @Service
@RequestScope @RequestScope
@Slf4j
public class MessageTranslator { public class MessageTranslator {
@Autowired @Autowired
@@ -21,17 +24,24 @@ public class MessageTranslator {
public String translateTo(final Locale locale, final String messageKey, final Object... args) { public String translateTo(final Locale locale, final String messageKey, final Object... args) {
try { try {
// we don't use the method which also takes a default message right away ... // we don't use the method which also takes a default message right away ...
final var translatedMessage = messageSource.getMessage(messageKey, args, locale); val translatedMessage = messageSource.getMessage(messageKey, args, locale);
return translatedMessage; return translatedMessage;
} catch (final Exception e) { } catch (final Exception e) {
final var defaultMessage = messageKey.replace("'", "''"); // ... but log the missing translation ...
final var translatedMessage = messageSource.getMessage(messageKey, args, defaultMessage, locale); log.error("Missing translation for message key '{}' in locale '{}'", messageKey, locale, e);
if (locale != Locale.ENGLISH) {
// ... because we want to add a hint that the translation is missing, even if placeholders got replaced // and decorate the default message to mark it as not really translated:
return translatedMessage + " [" + locale + " translation missing]"; val defaultMessage = messageKey.substring(messageKey.indexOf('.') + 1)
.replaceAll("--+", " - ")
.replaceAll("(?<! )-(?! )", " ")
.replace("'", "''");
val fallbackMessage = messageSource.getMessage(messageKey, args, defaultMessage, Locale.ENGLISH);
return decorateMissingTranslation(fallbackMessage);
} }
return translatedMessage;
} }
private static String decorateMissingTranslation(final String translatedMessage) {
return "【⍰" + translatedMessage + "⍰】";
} }
public String translate(final String messageKey, final Object... args) { public String translate(final String messageKey, final Object... args) {
@@ -11,6 +11,8 @@ public class MessagesResourceConfig {
final var source = new ResourceBundleMessageSource(); final var source = new ResourceBundleMessageSource();
source.setBasenames("i18n/messages"); source.setBasenames("i18n/messages");
source.setDefaultEncoding("UTF-8"); source.setDefaultEncoding("UTF-8");
source.setFallbackToSystemLocale(false);
source.setUseCodeAsDefaultMessage(false);
return source; return source;
} }
@@ -57,7 +57,7 @@ public class RealCasAuthenticator implements CasAuthenticator {
private Document verifyServiceTicket(final String serviceTicket) throws SAXException, IOException, ParserConfigurationException { private Document verifyServiceTicket(final String serviceTicket) throws SAXException, IOException, ParserConfigurationException {
if ( !serviceTicket.startsWith("ST-") ) { if ( !serviceTicket.startsWith("ST-") ) {
throwBadCredentialsException("unknown authorization ticket"); throwBadCredentialsException("auth.unknown-authorization-ticket");
} }
final var url = casServerUrl + "/cas/p3/serviceValidate" + final var url = casServerUrl + "/cas/p3/serviceValidate" +
@@ -74,7 +74,7 @@ public class RealCasAuthenticator implements CasAuthenticator {
private String extractUserName(final Document verification) { private String extractUserName(final Document verification) {
if (verification.getElementsByTagName("cas:authenticationSuccess").getLength() == 0) { if (verification.getElementsByTagName("cas:authenticationSuccess").getLength() == 0) {
throwBadCredentialsException("CAS service-ticket could not be verified"); throwBadCredentialsException("auth.cas-service-ticket-could-not-be-verified");
} }
return verification.getElementsByTagName("cas:user").item(0).getTextContent(); return verification.getElementsByTagName("cas:user").item(0).getTextContent();
} }
@@ -8,7 +8,7 @@ import static java.util.Locale.ENGLISH;
public class ReferenceNotFoundException extends RuntimeException { public class ReferenceNotFoundException extends RuntimeException {
private final String TRANSLATABLE_MESSAGE = "{0} \"{1}\" not found"; private final String TRANSLATABLE_MESSAGE = "general.{0}-{1}-not-found";
private final MessageTranslator translator; private final MessageTranslator translator;
@@ -156,7 +156,7 @@ public class RestResponseEntityExceptionHandler
} }
private Function<FieldError, String> toEnrichedFieldErrorMessage() { private Function<FieldError, String> toEnrichedFieldErrorMessage() {
final var translatedButIsLiteral = messageTranslator.translate("but is"); final var translatedButIsLiteral = messageTranslator.translate("general.but-is");
// TODO.i18n: the following does not work in all languages, e.g. not in right-to-left languages // TODO.i18n: the following does not work in all languages, e.g. not in right-to-left languages
return fieldError -> fieldError.getField() + " " + fieldError.getDefaultMessage() + return fieldError -> fieldError.getField() + " " + fieldError.getDefaultMessage() +
" " + translatedButIsLiteral + " " + optionallyQuoted(fieldError.getRejectedValue()); " " + translatedButIsLiteral + " " + optionallyQuoted(fieldError.getRejectedValue());
@@ -52,13 +52,15 @@ public class CredentialContextResourceToEntityMapper {
final var existingContextEntity = em.find(HsCredentialsContextRealEntity.class, resource.getUuid()); final var existingContextEntity = em.find(HsCredentialsContextRealEntity.class, resource.getUuid());
if (existingContextEntity == null) { if (existingContextEntity == null) {
throw new EntityNotFoundException( throw new EntityNotFoundException(
messageTranslator.translate("{0} \"{1}\" not found or not accessible", messageTranslator.translate(
"general.{0}-{1}-not-found-or-not-accessible",
"credentials uuid", resource.getUuid())); "credentials uuid", resource.getUuid()));
} }
if ((resource.getType() != null && !existingContextEntity.getType().equals(resource.getType())) || if ((resource.getType() != null && !existingContextEntity.getType().equals(resource.getType())) ||
(resource.getQualifier() != null && !existingContextEntity.getQualifier().equals(resource.getQualifier()))) { (resource.getQualifier() != null && !existingContextEntity.getQualifier().equals(resource.getQualifier()))) {
throw new EntityNotFoundException( throw new EntityNotFoundException(
messageTranslator.translate("existing {0} does not match given resource {1}", messageTranslator.translate(
"credentials.existing-{0}-does-not-match-given-resource-{1}",
existingContextEntity, resource)); existingContextEntity, resource));
} }
entities.add(existingContextEntity); entities.add(existingContextEntity);
@@ -203,12 +203,12 @@ public class HsCredentialsController implements CredentialsApi {
private void validate(final HsCredentialsEntity newCredentialsEntity) { private void validate(final HsCredentialsEntity newCredentialsEntity) {
// the referenced person must be represented by currently logged in person // the referenced person must be represented by currently logged in person
final var personUuid = newCredentialsEntity.getPerson().getUuid(); final var personUuid = newCredentialsEntity.getPerson().getUuid();
final var representedPersonUuids = rbacPersonRepo.findPersonsrepresentedByPersonWithUuid(personUuid) final var representedPersonUuids = rbacPersonRepo.findPersonsRepresentedByPersonWithUuid(personUuid)
.stream().map(HsOfficePerson::getUuid).toList(); .stream().map(HsOfficePerson::getUuid).toList();
if ( !representedPersonUuids.contains(personUuid)) { if ( !representedPersonUuids.contains(personUuid)) {
throw new ValidationException( throw new ValidationException(
messageTranslator.translate( messageTranslator.translate(
"access-denied-personUuid-{0}-not-represented-by-currently-logged-in-person", "credentials.access-denied-person-uuid-{0}-not-represented-by-currently-logged-in-person",
personUuid)); personUuid));
} }
} }
@@ -224,7 +224,7 @@ public class HsCredentialsController implements CredentialsApi {
private List<HsCredentialsEntity> findByPersonUuid(final UUID personUuid) { private List<HsCredentialsEntity> findByPersonUuid(final UUID personUuid) {
final var person = rbacPersonRepo.findByUuid(personUuid).orElseThrow( final var person = rbacPersonRepo.findByUuid(personUuid).orElseThrow(
() -> new EntityNotFoundException( () -> new EntityNotFoundException(
messageTranslator.translate("{0} \"{1}\" not found or not accessible", "personUuid", personUuid) messageTranslator.translate("general.{0}-{1}-not-found-or-not-accessible", "personUuid", personUuid)
) )
); );
@@ -269,7 +269,7 @@ public class HsCredentialsController implements CredentialsApi {
final BiConsumer<CredentialsInsertResource, HsCredentialsEntity> RESOURCE_TO_ENTITY_POSTMAPPER = (resource, entity) -> { final BiConsumer<CredentialsInsertResource, HsCredentialsEntity> RESOURCE_TO_ENTITY_POSTMAPPER = (resource, entity) -> {
final var person = rbacPersonRepo.findByUuid(resource.getPersonUuid()).orElseThrow( final var person = rbacPersonRepo.findByUuid(resource.getPersonUuid()).orElseThrow(
() -> new EntityNotFoundException( () -> new EntityNotFoundException(
messageTranslator.translate("{0} \"{1}\" not found or not accessible", "personUuid", resource.getPersonUuid()) messageTranslator.translate("general.{0}-{1}-not-found-or-not-accessible", "personUuid", resource.getPersonUuid())
) )
); );
@@ -17,7 +17,7 @@ public class HsCoopAssetTranslations implements RetroactiveTranslator {
@Override @Override
public boolean canTranslate(final String message) { public boolean canTranslate(final String message) {
return message.equals("ERROR: [400] coop assets transaction would result in a negative balance of assets"); return message.equals("ERROR: [400] office.coop-assets.transaction-would-result-in-a-negative-balance-of-assets");
} }
@Override @Override
@@ -26,7 +26,7 @@ public class HsCoopAssetTranslations implements RetroactiveTranslator {
// and in this case it's just one // and in this case it's just one
return ERROR_400_PREFIX + messageTranslator.translate(message.substring(ERROR_400_PREFIX.length())); 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: // HOWTO extract variable parts from messages which got created without i18n support:
// final var regex = "(?<propertyName>[^ ]+) (?<propertyValue>.+) not found"; // final var regex = "(?<propertyName>[^ ]+) (?<propertyValue>.+) not found";
// final var pattern = Pattern.compile(regex); // final var pattern = Pattern.compile(regex);
// final var matcher = pattern.matcher(message); // final var matcher = pattern.matcher(message);
@@ -141,7 +141,8 @@ public class HsOfficeCoopAssetsTransactionController implements HsOfficeCoopAsse
final ArrayList<String> violations) { final ArrayList<String> violations) {
if (List.of(DEPOSIT, HsOfficeCoopAssetsTransactionTypeResource.ADOPTION).contains(requestBody.getTransactionType()) if (List.of(DEPOSIT, HsOfficeCoopAssetsTransactionTypeResource.ADOPTION).contains(requestBody.getTransactionType())
&& requestBody.getAssetValue().signum() < 0) { && requestBody.getAssetValue().signum() < 0) {
violations.add(messageTranslator.translate("for transactionType={0}, assetValue must be positive but is {1,number,#0.00}", violations.add(messageTranslator.translate(
"office.coop-assets.for-transactiontype-{0}-assetvalue-must-be-positive-but-is-{1,number,#0.00}",
requestBody.getTransactionType(), requestBody.getAssetValue())); requestBody.getTransactionType(), requestBody.getAssetValue()));
} }
} }
@@ -152,7 +153,8 @@ public class HsOfficeCoopAssetsTransactionController implements HsOfficeCoopAsse
if (List.of(DISBURSAL, HsOfficeCoopAssetsTransactionTypeResource.TRANSFER, CLEARING, LOSS) if (List.of(DISBURSAL, HsOfficeCoopAssetsTransactionTypeResource.TRANSFER, CLEARING, LOSS)
.contains(requestBody.getTransactionType()) .contains(requestBody.getTransactionType())
&& requestBody.getAssetValue().signum() > 0) { && requestBody.getAssetValue().signum() > 0) {
violations.add(messageTranslator.translate("for transactionType={0}, assetValue must be negative but is {1,number,#0.00}", violations.add(messageTranslator.translate(
"office.coop-assets.for-transactiontype-{0}-assetvalue-must-be-negative-but-is-{1,number,#0.00}",
requestBody.getTransactionType(), requestBody.getAssetValue())); requestBody.getTransactionType(), requestBody.getAssetValue()));
} }
} }
@@ -161,7 +163,7 @@ public class HsOfficeCoopAssetsTransactionController implements HsOfficeCoopAsse
final HsOfficeCoopAssetsTransactionInsertResource requestBody, final HsOfficeCoopAssetsTransactionInsertResource requestBody,
final ArrayList<String> violations) { final ArrayList<String> violations) {
if (requestBody.getAssetValue().signum() == 0) { if (requestBody.getAssetValue().signum() == 0) {
violations.add(messageTranslator.translate("assetValue must not be 0")); violations.add(messageTranslator.translate("office.coop-assets.assetvalue-must-not-be-0"));
} }
} }
@@ -227,18 +229,18 @@ public class HsOfficeCoopAssetsTransactionController implements HsOfficeCoopAsse
resource.getMembershipUuid())) resource.getMembershipUuid()))
.orElseThrow(() -> new EntityNotFoundException( .orElseThrow(() -> new EntityNotFoundException(
messageTranslator.translate( messageTranslator.translate(
"{0} \"{1}\" not found", "membership.uuid", resource.getMembershipUuid()))); "general.{0}-{1}-not-found", "membership.uuid", resource.getMembershipUuid())));
entity.setMembership(membership); entity.setMembership(membership);
} }
if (entity.getTransactionType() == REVERSAL) { if (entity.getTransactionType() == REVERSAL) {
if (resource.getRevertedAssetTxUuid() == null) { if (resource.getRevertedAssetTxUuid() == null) {
throw new ValidationException(messageTranslator.translate( throw new ValidationException(messageTranslator.translate(
"a REVERSAL asset transaction requires specifying a revertedAssetTx.uuid")); "office.coop-assets.a-reversal-asset-transaction-requires-specifying-a-revertedassettx-uuid"));
} }
final var revertedAssetTx = coopAssetsTransactionRepo.findByUuid(resource.getRevertedAssetTxUuid()) final var revertedAssetTx = coopAssetsTransactionRepo.findByUuid(resource.getRevertedAssetTxUuid())
.orElseThrow(() -> new EntityNotFoundException(messageTranslator.translate( .orElseThrow(() -> new EntityNotFoundException(messageTranslator.translate(
"{0} \"{1}\" not found", "general.{0}-{1}-not-found",
"revertedAssetTx.uuid", "revertedAssetTx.uuid",
resource.getRevertedAssetTxUuid()))); resource.getRevertedAssetTxUuid())));
revertedAssetTx.setReversalAssetTx(entity); revertedAssetTx.setReversalAssetTx(entity);
@@ -246,7 +248,7 @@ public class HsOfficeCoopAssetsTransactionController implements HsOfficeCoopAsse
if (resource.getAssetValue().negate().compareTo(revertedAssetTx.getAssetValue()) != 0) { if (resource.getAssetValue().negate().compareTo(revertedAssetTx.getAssetValue()) != 0) {
throw new ValidationException( throw new ValidationException(
messageTranslator.translate( messageTranslator.translate(
"given assetValue {0,number,#0.00} must be the negative value of the reverted asset transaction: {1,number,#0.00}", "office.coop-assets.given-assetvalue-{0,number,#0.00}-must-be-the-negative-value-of-the-reverted-asset-transaction-{1,number,#0.00}",
resource.getAssetValue(), revertedAssetTx.getAssetValue())); resource.getAssetValue(), revertedAssetTx.getAssetValue()));
} }
@@ -270,7 +272,7 @@ public class HsOfficeCoopAssetsTransactionController implements HsOfficeCoopAsse
final var adoptingMembership = determineAdoptingMembership(resource); final var adoptingMembership = determineAdoptingMembership(resource);
if ( entity.getMembership() == adoptingMembership) { if ( entity.getMembership() == adoptingMembership) {
throw new ValidationException(messageTranslator.translate( throw new ValidationException(messageTranslator.translate(
"transferring and adopting membership must be different, but both are {0}", "office.coop-assets.transferring-and-adopting-membership-must-be-different-but-both-are-{0}",
adoptingMembership.getTaggedMemberNumber())); adoptingMembership.getTaggedMemberNumber()));
} }
final var adoptingAssetTx = createAdoptingAssetTx(entity, adoptingMembership); final var adoptingAssetTx = createAdoptingAssetTx(entity, adoptingMembership);
@@ -285,8 +287,8 @@ public class HsOfficeCoopAssetsTransactionController implements HsOfficeCoopAsse
// @formatter:off // @formatter:off
final var message = messageTranslator.translate( final var message = messageTranslator.translate(
resource.getTransactionType() == HsOfficeCoopAssetsTransactionTypeResource.TRANSFER resource.getTransactionType() == HsOfficeCoopAssetsTransactionTypeResource.TRANSFER
? "either {0} or {1} must be given, not both" ? "office.coop-assets.either-{0}-or-{1}-must-be-given-not-both"
: "neither {0} nor {1} must be given for transactionType={2}", : "office.coop-assets.neither-{0}-nor-{1}-must-be-given-for-transactiontype-{2}",
"adoptingMembership.uuid", "adoptingMembership.uuid",
"adoptingMembership.memberNumber", "adoptingMembership.memberNumber",
resource.getTransactionType()); resource.getTransactionType());
@@ -298,7 +300,7 @@ public class HsOfficeCoopAssetsTransactionController implements HsOfficeCoopAsse
final var adoptingMembership = membershipRepo.findByUuid(adoptingMembershipUuid); final var adoptingMembership = membershipRepo.findByUuid(adoptingMembershipUuid);
return adoptingMembership.orElseThrow(() -> return adoptingMembership.orElseThrow(() ->
new ValidationException(messageTranslator.translate( new ValidationException(messageTranslator.translate(
"{0} \"{1}\" not found or not accessible", "general.{0}-{1}-not-found-or-not-accessible",
"adoptingMembership.uuid", "adoptingMembership.uuid",
adoptingMembershipUuid))); adoptingMembershipUuid)));
} }
@@ -309,14 +311,14 @@ public class HsOfficeCoopAssetsTransactionController implements HsOfficeCoopAsse
return adoptingMembership.orElseThrow( () -> return adoptingMembership.orElseThrow( () ->
new ValidationException( new ValidationException(
messageTranslator.translate( messageTranslator.translate(
"{0} \"{1}\" not found or not accessible", "general.{0}-{1}-not-found-or-not-accessible",
"adoptingMembership.memberNumber", "adoptingMembership.memberNumber",
adoptingMembershipMemberNumber))); adoptingMembershipMemberNumber)));
} }
throw new ValidationException( throw new ValidationException(
messageTranslator.translate( messageTranslator.translate(
"either {0} or {1} must be given for transactionType={2}", "office.coop-assets.either-{0}-or-{1}-must-be-given-for-transactiontype-{2}",
"adoptingMembership.uuid", "adoptingMembership.uuid",
"adoptingMembership.memberNumber", "adoptingMembership.memberNumber",
resource.getTransactionType() resource.getTransactionType()
@@ -16,7 +16,7 @@ public class HsCoopShareTranslations implements RetroactiveTranslator {
@Override @Override
public boolean canTranslate(final String message) { public boolean canTranslate(final String message) {
return message.equals("ERROR: [400] coop shares transaction would result in a negative number of shares"); return message.equals("ERROR: [400] office.coop-shares.transaction-would-result-in-a-negative-number-of-shares");
} }
@Override @Override
@@ -129,7 +129,8 @@ public class HsOfficeCoopSharesTransactionController implements HsOfficeCoopShar
final ArrayList<String> violations) { final ArrayList<String> violations) {
if (requestBody.getTransactionType() == SUBSCRIPTION if (requestBody.getTransactionType() == SUBSCRIPTION
&& requestBody.getShareCount() < 0) { && requestBody.getShareCount() < 0) {
violations.add(messageTranslator.translate("for transactionType={0}, shareCount must be positive but is {1}", violations.add(messageTranslator.translate(
"office.coop-shares.for-transactiontype-{0}-sharecount-must-be-positive-but-is-{1}",
requestBody.getTransactionType(), requestBody.getShareCount())); requestBody.getTransactionType(), requestBody.getShareCount()));
} }
} }
@@ -139,7 +140,8 @@ public class HsOfficeCoopSharesTransactionController implements HsOfficeCoopShar
final ArrayList<String> violations) { final ArrayList<String> violations) {
if (requestBody.getTransactionType() == CANCELLATION if (requestBody.getTransactionType() == CANCELLATION
&& requestBody.getShareCount() > 0) { && requestBody.getShareCount() > 0) {
violations.add(messageTranslator.translate("for transactionType={0}, shareCount must be negative but is {1}", violations.add(messageTranslator.translate(
"office.coop-shares.for-transactiontype-{0}-sharecount-must-be-negative-but-is-{1}",
requestBody.getTransactionType(), requestBody.getShareCount())); requestBody.getTransactionType(), requestBody.getShareCount()));
} }
} }
@@ -148,7 +150,7 @@ public class HsOfficeCoopSharesTransactionController implements HsOfficeCoopShar
final HsOfficeCoopSharesTransactionInsertResource requestBody, final HsOfficeCoopSharesTransactionInsertResource requestBody,
final ArrayList<String> violations) { final ArrayList<String> violations) {
if (requestBody.getShareCount() == 0) { if (requestBody.getShareCount() == 0) {
violations.add(messageTranslator.translate("shareCount must not be 0")); violations.add(messageTranslator.translate("office.coop-shares.sharecount-must-not-be-0"));
} }
} }
@@ -42,7 +42,7 @@ public class HsOfficePersonController implements HsOfficePersonsApi {
context.assumeRoles(assumedRoles); context.assumeRoles(assumedRoles);
final var entities = representedByPersonUuid != null final var entities = representedByPersonUuid != null
? personRepo.findPersonsrepresentedByPersonWithUuid(representedByPersonUuid) ? personRepo.findPersonsRepresentedByPersonWithUuid(representedByPersonUuid)
: personRepo.findPersonByOptionalNameLike(name); : personRepo.findPersonByOptionalNameLike(name);
final var resources = mapper.mapList(entities, HsOfficePersonResource.class); final var resources = mapper.mapList(entities, HsOfficePersonResource.class);
@@ -37,7 +37,7 @@ public interface HsOfficePersonRbacRepository extends Repository<HsOfficePersonR
OR person.uuid = :personUuid OR person.uuid = :personUuid
""", nativeQuery = true) """, nativeQuery = true)
@Timed("app.office.persons.repo.findRepresentedPersons.rbac") @Timed("app.office.persons.repo.findRepresentedPersons.rbac")
List<HsOfficePersonRbacEntity> findPersonsrepresentedByPersonWithUuid(UUID personUuid); List<HsOfficePersonRbacEntity> findPersonsRepresentedByPersonWithUuid(UUID personUuid);
@Timed("app.office.persons.repo.save.rbac") @Timed("app.office.persons.repo.save.rbac")
HsOfficePersonRbacEntity save(final HsOfficePersonRbacEntity entity); HsOfficePersonRbacEntity save(final HsOfficePersonRbacEntity entity);
@@ -21,9 +21,8 @@ public class PingController implements TestApi {
@PreAuthorize("permitAll()") @PreAuthorize("permitAll()")
@Timed("app.api.ping") @Timed("app.api.ping")
public ResponseEntity<String> ping() { public ResponseEntity<String> ping() {
final var userName = SecurityContextHolder.getContext().getAuthentication().getName();
// HOWTO translate text with placeholders - also see in resource files i18n/messages_*.properties. // HOWTO translate text with placeholders - also see in resource files i18n/messages_*.properties.
final var translatedMessage = messageTranslator.translate("pong {0} - in English", userName); final var translatedMessage = messageTranslator.translate("test.pinged--in-your-language");
return ResponseEntity.ok(translatedMessage + "\n"); return ResponseEntity.ok(translatedMessage + "\n");
} }
+3 -3
View File
@@ -72,9 +72,9 @@ metrics:
logging: logging:
level: level:
org.springframework.security: info org.springframework.security: info
org.springframework.web: DEBUG # org.springframework.web: DEBUG
org.springframework.web.method.annotation: DEBUG # org.springframework.web.method.annotation: DEBUG
org.springframework.validation: DEBUG # org.springframework.validation: DEBUG
# HOWTO configure logging, e.g. logging to a separate file, see: # HOWTO configure logging, e.g. logging to a separate file, see:
# https://docs.spring.io/spring-boot/reference/features/logging.html # https://docs.spring.io/spring-boot/reference/features/logging.html
@@ -33,7 +33,7 @@ alter table hs_office.coopsharetx
--// --//
-- ============================================================================ -- ============================================================================
--changeset marc.sandlus:hs-office-coopshares-SHARE-COUNT-CONSTRAINT-BY-TRIGGER endDelimiter:--// --changeset marc.sandlus:hs-office-coopshares-SHARE-COUNT-CONSTRAINT-BY-TRIGGER runOnChange:true validCheckSum:ANY endDelimiter:--//
-- ---------------------------------------------------------------------------- -- ----------------------------------------------------------------------------
alter table hs_office.coopsharetx drop constraint if exists check_positive_total_shares_count; alter table hs_office.coopsharetx drop constraint if exists check_positive_total_shares_count;
@@ -42,7 +42,6 @@ drop function if exists hs_office.coopsharestx_check_positive_total cascade;
create or replace function hs_office.coopsharetx_enforce_positive_total() create or replace function hs_office.coopsharetx_enforce_positive_total()
returns trigger as $$ returns trigger as $$
declare declare
currentShareCount integer; currentShareCount integer;
totalShareCount integer; totalShareCount integer;
@@ -53,12 +52,13 @@ begin
into currentShareCount; into currentShareCount;
totalShareCount := currentShareCount + NEW.shareCount; totalShareCount := currentShareCount + NEW.shareCount;
if totalShareCount < 0 then if totalShareCount < 0 then
raise exception '[400] coop shares transaction would result in a negative number of shares'; raise exception '[400] office.coop-shares.transaction-would-result-in-a-negative-number-of-shares';
end if; end if;
return NEW; return NEW;
end; end;
$$ LANGUAGE plpgsql;; $$ LANGUAGE plpgsql;;
drop trigger if exists positive_total_shares_count_tg on hs_office.coopsharetx;
create trigger positive_total_shares_count_tg before insert create trigger positive_total_shares_count_tg before insert
on hs_office.coopsharetx on hs_office.coopsharetx
@@ -73,7 +73,7 @@ CREATE TRIGGER enforce_transaction_constraints
--// --//
-- ============================================================================ -- ============================================================================
--changeset marc.sandlus:hs-office-coopassets-ASSET-VALUE-CONSTRAINT-BY-TRIGGER endDelimiter:--// --changeset marc.sandlus:hs-office-coopassets-ASSET-VALUE-CONSTRAINT-BY-TRIGGER runOnChange:true validCheckSum:ANY endDelimiter:--//
-- ---------------------------------------------------------------------------- -- ----------------------------------------------------------------------------
alter table hs_office.coopassettx alter table hs_office.coopassettx
@@ -83,7 +83,6 @@ drop function if exists hs_office.coopassetstx_check_positive_total cascade;
create or replace function hs_office.coopassettx_enforce_positive_total() create or replace function hs_office.coopassettx_enforce_positive_total()
returns trigger as $$ returns trigger as $$
declare declare
currentAssetValue numeric(12,2); currentAssetValue numeric(12,2);
totalAssetValue numeric(12,2); totalAssetValue numeric(12,2);
@@ -94,12 +93,14 @@ begin
into currentAssetValue; into currentAssetValue;
totalAssetValue := currentAssetValue + NEW.assetValue; totalAssetValue := currentAssetValue + NEW.assetValue;
if totalAssetValue::numeric < 0 then if totalAssetValue::numeric < 0 then
raise exception '[400] coop assets transaction would result in a negative balance of assets'; raise exception '[400] office.coop-assets.transaction-would-result-in-a-negative-balance-of-assets';
end if; end if;
return NEW; return NEW;
end; end;
$$ LANGUAGE plpgsql;; $$ LANGUAGE plpgsql;;
drop trigger if exists positive_total_assets_count_tg on hs_office.coopassettx;
create trigger positive_total_assets_count_tg before insert create trigger positive_total_assets_count_tg before insert
on hs_office.coopassettx on hs_office.coopassettx
for each row execute function hs_office.coopassettx_enforce_positive_total(); for each row execute function hs_office.coopassettx_enforce_positive_total();
+26 -24
View File
@@ -1,36 +1,38 @@
# This file must be in UTF-8 encoding. Check if you see umlauts or garbage: äöüÄÖÜß. # This file must be in UTF-8 encoding. Check if you see umlauts or garbage: äöüÄÖÜß.
# HINT IntelliJ IDEA shows unused keys in gray. # HINT IntelliJ IDEA shows unused keys in gray.
pong\ {0}\ -\ in\ English=pong {0} - auf Deutsch test.pinged--in-your-language=pinged - auf Deutsch
test.ponged-{0}--in-your-language=ponged {0} - auf Deutsch
test.available-in-all-properties-files=Hallo {0} - DE!
# config (including authorization) # authorization
CAS\ service-ticket\ could\ not\ be\ verified=CAS Service-Ticket konnte nicht verifiziert werden auth.cas-service-ticket-could-not-be-verified=CAS Service-Ticket konnte nicht verifiziert werden
unknown\ authorization\ ticket=unbekanntes Autorisierungs-Ticket auth.unknown-authorization-ticket=unbekanntes Autorisierungs-Ticket
# general validations # general validations
{0}\ "{1}"\ not\ found={0} "{1}" nicht gefunden general.{0}-{1}-not-found={0} "{1}" nicht gefunden
{0}\ "{1}"\ not\ found\ or\ not\ accessible={0} "{1}" nicht gefunden oder nicht zugänglich general.{0}-{1}-not-found-or-not-accessible={0} "{1}" nicht gefunden oder nicht zugänglich
but\ is=ist aber general.but-is=ist aber
# credentials validations # credentials validations
existing\ {0}\ does\ not\ match\ given\ resource\ {1}=existierender Credentials-Context {0} passt nicht zum angegebenen {1} credentials.existing-{0}-does-not-match-given-resource-{1}=existierender Credentials-Context {0} passt nicht zum angegebenen {1}
access-denied-personUuid-{0}-not-represented-by-currently-logged-in-person=Zugriff verweigert: personUuid "{0}" wird von der eingeloggten Person nicht repräsentiert credentials.access-denied-person-uuid-{0}-not-represented-by-currently-logged-in-person=Zugriff verweigert: personUuid "{0}" wird von der eingeloggten Person nicht repräsentiert
# office.coop-shares # office.coop-shares
for\ transactionType\={0},\ shareCount\ must\ be\ positive\ but\ is\ {1}=für transactionType={0}, muss shareCount positiv sein, ist aber {1} office.coop-shares.for-transactiontype-{0}-sharecount-must-be-positive-but-is-{1}=für transactionType={0}, muss shareCount positiv sein, ist aber {1}
for\ transactionType\={0},\ shareCount\ must\ be\ negative\ but\ is\ {1}=für transactionType={0}, muss shareCount negativ sein, ist aber {1} office.coop-shares.for-transactiontype-{0}-sharecount-must-be-negative-but-is-{1}=für transactionType={0}, muss shareCount negativ sein, ist aber {1}
shareCount\ must\ not\ be\ 0=shareCount darf nicht 0 sein office.coop-shares.sharecount-must-not-be-0=shareCount darf nicht 0 sein
coop\ shares\ transaction\ would\ result\ in\ a\ negative\ number\ of\ shares=Geschäftsanteile-Transaktion würde zu negativen Geschäftsanteilen führen office.coop-shares.transaction-would-result-in-a-negative-number-of-shares=Geschäftsanteile-Transaktion würde zu negativen Geschäftsanteilen führen
# office.coop-assets # office.coop-assets
either\ {0}\ or\ {1}\ must\ be\ given=entweder {0} oder {1} muss angegeben werden 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 office.coop-assets.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 office.coop-assets.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 office.coop-assets.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 office.coop-assets.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} office.coop-assets.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} office.coop-assets.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} office.coop-assets.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 office.coop-assets.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} office.coop-assets.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 office.coop-assets.transaction-would-result-in-a-negative-balance-of-assets=Geschäftsguthaben-Transaktion würde zu einem negativen Geschäftsguthaben-Saldo führen
+34 -4
View File
@@ -1,9 +1,39 @@
# This file must be in UTF-8 encoding. Check if you see umlauts or garbage: äöüÄÖÜß # This file must be in UTF-8 encoding. Check if you see umlauts or garbage: äöüÄÖÜß
# HINT IntelliJ IDEA shows unused keys in gray. # 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. test.pinged--in-your-language=pinged - in English
# But in that case, you can NOT use a prefix - or the prefix would be shown to the user as well. test.ponged-{0}--in-your-language=ponged {0} - in English
# I'm not sure, though, if using the english default translations as keys is really a good idea. test.available-in-all-properties-files=Hello {0} - EN!
# authorization
auth.cas-service-ticket-could-not-be-verified=CAS service-ticket could not be verified
auth.unknown-authorization-ticket=unknown authorization ticket
# general validations
general.{0}-{1}-not-found={0} "{1}" not found
general.{0}-{1}-not-found-or-not-accessible={0} "{1}" not found or not accessible
general.but-is=but is
# credentials validations # credentials validations
access-denied-personUuid-{0}-not-represented-by-currently-logged-in-person=access denied: personUuid "{0}" not represented by currently logged in person credentials.existing-{0}-does-not-match-given-resource-{1}=existing {0} does not match given resource {1}
credentials.access-denied-person-uuid-{0}-not-represented-by-currently-logged-in-person=access denied: personUuid "{0}" not represented by currently logged in person
# office.coop-shares
office.coop-shares.for-transactiontype-{0}-sharecount-must-be-positive-but-is-{1}=for transactiontType {0} shareCount must be positive but is {1}
office.coop-shares.for-transactiontype-{0}-sharecount-must-be-negative-but-is-{1}=for transactiontType {0} shareCount must be negative but is {1}
office.coop-shares.sharecount-must-not-be-0=shareCount must not be 0
office.coop-shares.transaction-would-result-in-a-negative-number-of-shares=coop shares transaction would result in a negative number of shares
# office.coop-assets
office.coop-assets.either-{0}-or-{1}-must-be-given=either {0} or {1} must be given
office.coop-assets.either-{0}-or-{1}-must-be-given-not-both=either {0} or {1} must be given not both
office.coop-assets.either-{0}-or-{1}-must-be-given-for-transactiontype-{2}=either {0} or {1} must be given for transactionType {2}
office.coop-assets.neither-{0}-nor-{1}-must-be-given-for-transactiontype-{2}=neither {0} nor {1} must be given for transactionType {2}
office.coop-assets.assetvalue-must-not-be-0=assetvalue must not be 0
office.coop-assets.for-transactiontype-{0}-assetvalue-must-be-positive-but-is-{1,number,#0.00}=for transactionType {0} assetValue must be positive but is {1,number,#0.00}
office.coop-assets.for-transactiontype-{0}-assetvalue-must-be-negative-but-is-{1,number,#0.00}=for transactionType {0} assetValue must be negative but is {1,number,#0.00}
office.coop-assets.given-assetvalue-{0,number,#0.00}-must-be-the-negative-value-of-the-reverted-asset-transaction-{1,number,#0.00}=given assetValue {0,number,#0.00} must be the negative value of the reverted asset transaction {1,number,#0.00}
office.coop-assets.a-reversal-asset-transaction-requires-specifying-a-revertedassettx-uuid=a reversal asset transaction requires specifying a revertedAssetTx uuid
office.coop-assets.transferring-and-adopting-membership-must-be-different-but-both-are-{0}=transferring and adopting membership must be different but both are {0}
office.coop-assets.transaction-would-result-in-a-negative-balance-of-assets=coop assets transaction would result in a negative balance of assets
@@ -0,0 +1,38 @@
# This file must be in UTF-8 encoding. Check if you see umlauts or garbage: äöüÄÖÜß
# HINT IntelliJ IDEA shows unused keys in gray.
# test.ponged--in-your-language=this translation is deliberately missing
test.pinged--in-your-language=ponged {0} - en Francais
test.available-in-all-properties-files=Salut {0} - FR!
# authorization
auth.cas-service-ticket-could-not-be-verified=Le ticket de service CAS n'a pas pu être vérifié
auth.unknown-authorization-ticket=ticket d'autorisation inconnu
# general validations
general.{0}-{1}-not-found={0} "{1}" non trouvé
general.{0}-{1}-not-found-or-not-accessible={0} "{1}" non trouvé ou non accessible
general.but-is=mais c'est
# credentials validations
credentials.existing-{0}-does-not-match-given-resource-{1}={0} existant ne correspond pas à la ressource donnée {1}
credentials.access-denied-person-uuid-{0}-not-represented-by-currently-logged-in-person=accès refusé : personUuid "{0}" non représenté par la personne actuellement connectée
# office.coop-shares
office.coop-shares.for-transactiontype-{0}-sharecount-must-be-positive-but-is-{1}=pour le type de transaction {0}, shareCount doit être positif mais est {1}
office.coop-shares.for-transactiontype-{0}-sharecount-must-be-negative-but-is-{1}=pour le type de transaction {0}, shareCount doit être négatif mais est {1}
office.coop-shares.sharecount-must-not-be-0=shareCount ne doit pas être 0
office.coop-shares.transaction-would-result-in-a-negative-number-of-shares=la transaction de parts coopératives résulterait en un nombre négatif de parts
# office.coop-assets
office.coop-assets.either-{0}-or-{1}-must-be-given=soit {0} soit {1} doit être fourni
office.coop-assets.either-{0}-or-{1}-must-be-given-not-both=soit {0} soit {1} doit être fourni, pas les deux
office.coop-assets.either-{0}-or-{1}-must-be-given-for-transactiontype-{2}=soit {0} soit {1} doit être fourni pour le type de transaction {2}
office.coop-assets.neither-{0}-nor-{1}-must-be-given-for-transactiontype-{2}=ni {0} ni {1} ne doit être fourni pour le type de transaction {2}
office.coop-assets.assetvalue-must-not-be-0=assetvalue ne doit pas être 0
office.coop-assets.for-transactiontype-{0}-assetvalue-must-be-positive-but-is-{1,number,#0.00}=pour le type de transaction {0}, assetValue doit être positif mais est {1,number,#0.00}
office.coop-assets.for-transactiontype-{0}-assetvalue-must-be-negative-but-is-{1,number,#0.00}=pour le type de transaction {0}, assetValue doit être négatif mais est {1,number,#0.00}
office.coop-assets.given-assetvalue-{0,number,#0.00}-must-be-the-negative-value-of-the-reverted-asset-transaction-{1,number,#0.00}=assetValue donné {0,number,#0.00} doit être la valeur négative de la transaction d'actif annulée {1,number,#0.00}
office.coop-assets.a-reversal-asset-transaction-requires-specifying-a-revertedassettx-uuid=une transaction d'actif d'annulation nécessite de spécifier un uuid revertedAssetTx
office.coop-assets.transferring-and-adopting-membership-must-be-different-but-both-are-{0}=l'adhésion transférant et adoptante doit être différente, mais les deux sont {0}
office.coop-assets.transaction-would-result-in-a-negative-balance-of-assets=la transaction d'actifs coopératifs résulterait en un solde négatif d'actifs
@@ -69,7 +69,7 @@ class CasAuthenticationFilterIntegrationTest {
// when // when
final var result = restTemplate.exchange( final var result = restTemplate.exchange(
"http://localhost:" + this.serverPort + "/api/ping", "http://localhost:" + this.serverPort + "/api/pong",
HttpMethod.GET, HttpMethod.GET,
new HttpEntity<>(null, headers(entry("Authorization", "ST-valid"))), new HttpEntity<>(null, headers(entry("Authorization", "ST-valid"))),
String.class String.class
@@ -77,7 +77,7 @@ class CasAuthenticationFilterIntegrationTest {
// then // then
assertThat(result.getStatusCode()).isEqualTo(HttpStatus.OK); assertThat(result.getStatusCode()).isEqualTo(HttpStatus.OK);
assertThat(result.getBody()).startsWith("pong " + username); assertThat(result.getBody()).startsWith("ponged " + username);
// HOWTO assert log messages // HOWTO assert log messages
assertThat(capturedOutput.getOut()).containsPattern( assertThat(capturedOutput.getOut()).containsPattern(
LogbackLogPattern.of(LogLevel.DEBUG, RealCasAuthenticator.class, "CAS-user: " + username)); LogbackLogPattern.of(LogLevel.DEBUG, RealCasAuthenticator.class, "CAS-user: " + username));
@@ -0,0 +1,72 @@
package net.hostsharing.hsadminng.config;
import lombok.AllArgsConstructor;
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.test.context.ActiveProfiles;
import org.springframework.test.context.bean.override.mockito.MockitoBean;
import org.springframework.web.context.WebApplicationContext;
import net.hostsharing.hsadminng.context.Context;
import java.util.Locale;
import static org.assertj.core.api.Assertions.assertThat;
import lombok.val;
@SpringBootTest(classes = {
MessagesResourceConfig.class,
MessageTranslator.class
})
@ActiveProfiles("test")
@Tag("generalIntegrationTest")
class MessageTranslatorIntegrationTest {
@Autowired
private WebApplicationContext webApplicationContext;
@MockitoBean
private Context contextMock; // avoiding dependency issues
@AllArgsConstructor
enum TestCases {
ENGLISH_KNOWN(Locale.ENGLISH, "test.ponged-{0}--in-your-language",
"ponged testUser - in English"),
ENGLISH_UNKNOWN(Locale.ENGLISH, "test.ponged-{0}--unknown-key",
"【⍰ponged testUser - unknown key⍰】"),
ENGLISH_US(Locale.of("en", "US"), "test.ponged-{0}--in-your-language",
"ponged testUser - in English"),
ENGLISH_UK(Locale.of("en", "UK"), "test.ponged-{0}--in-your-language",
"ponged testUser - in English"),
GERMAN_KNOWN(Locale.GERMAN, "test.ponged-{0}--in-your-language",
"ponged testUser - auf Deutsch"),
FRENCH_UNKNOWN_BUT_ENGLISH_KNOWN(Locale.FRENCH, "test.ponged-{0}--in-your-language",
"【⍰ponged testUser - in English⍰】"),
FRENCH_UNKNOWN_AND_ENGLISH_UNKNOWN(Locale.FRENCH, "test.ponged-{0}--unknown-key",
"【⍰ponged testUser - unknown key⍰】"),
UNKNOWN_LOCALE_AND_ENGLISH_KNOWN(Locale.TRADITIONAL_CHINESE,
"test.ponged-{0}--in-your-language", "【⍰ponged testUser - in English⍰】"),
UNKNOWN_LOCALE_AND_ENGLISH_UNKNOWN(Locale.TRADITIONAL_CHINESE, "test.ponged-{0}--unknown-key",
"【⍰ponged testUser - unknown key⍰】");
final Locale locale;
final String messageKey;
final String expectedTranslation;
}
@ParameterizedTest
@EnumSource(TestCases.class)
void shouldHandleDifferentLocalesAppropriately(final TestCases testCase) {
// given
val messageTranslator = webApplicationContext.getBean(MessageTranslator.class);
// when
val result = messageTranslator.translateTo(testCase.locale, testCase.messageKey, "testUser");
// then
assertThat(result).isEqualTo(testCase.expectedTranslation);
}
}
@@ -67,14 +67,14 @@ class WebSecurityConfigIntegrationTest {
// http request // http request
final var result = restTemplate.exchange( final var result = restTemplate.exchange(
"http://localhost:" + this.serverPort + "/api/ping", "http://localhost:" + this.serverPort + "/api/pong",
HttpMethod.GET, HttpMethod.GET,
httpHeaders(entry("Authorization", "Bearer ST-fake-cas-ticket")), httpHeaders(entry("Authorization", "Bearer ST-fake-cas-ticket")),
String.class String.class
); );
assertThat(result.getStatusCode()).isEqualTo(HttpStatus.OK); assertThat(result.getStatusCode()).isEqualTo(HttpStatus.OK);
assertThat(result.getBody()).startsWith("pong fake-user-name"); assertThat(result.getBody()).startsWith("ponged fake-user-name");
} }
@Test @Test
@@ -85,18 +85,18 @@ class WebSecurityConfigIntegrationTest {
// http request // http request
final var result = restTemplate.exchange( final var result = restTemplate.exchange(
"http://localhost:" + this.serverPort + "/api/ping", "http://localhost:" + this.serverPort + "/api/pong",
HttpMethod.GET, HttpMethod.GET,
httpHeaders(entry("Authorization", "Bearer TGT-fake-cas-ticket")), httpHeaders(entry("Authorization", "Bearer TGT-fake-cas-ticket")),
String.class String.class
); );
assertThat(result.getStatusCode()).isEqualTo(HttpStatus.OK); assertThat(result.getStatusCode()).isEqualTo(HttpStatus.OK);
assertThat(result.getBody()).startsWith("pong fake-user-name"); assertThat(result.getBody()).startsWith("ponged fake-user-name");
} }
@Test @Test
void accessToApiWithInvalidTicketGrantingTicketShouldBePermitted() { void accessToOpenApiWithInvalidTicketGrantingTicketShouldBePermitted() {
// given // given
givenCasServiceTicketForTicketGrantingTicket("TGT-fake-cas-ticket", "ST-fake-cas-ticket"); givenCasServiceTicketForTicketGrantingTicket("TGT-fake-cas-ticket", "ST-fake-cas-ticket");
givenCasTicketValidationResponse("ST-fake-cas-ticket", "fake-user-name"); givenCasTicketValidationResponse("ST-fake-cas-ticket", "fake-user-name");
@@ -113,14 +113,14 @@ class WebSecurityConfigIntegrationTest {
} }
@Test @Test
void accessToPingApiWithoutTokenShouldBePermitted() { void accessToOpenApiWithoutTokenShouldBePermitted() {
final var result = this.restTemplate.getForEntity( final var result = this.restTemplate.getForEntity(
"http://localhost:" + this.serverPort + "/api/ping", String.class); "http://localhost:" + this.serverPort + "/api/ping", String.class);
assertThat(result.getStatusCode()).isEqualTo(HttpStatus.OK); assertThat(result.getStatusCode()).isEqualTo(HttpStatus.OK);
} }
@Test @Test
void accessToPongApiWithValidTokenShouldBePermitted() { void accessToProtectedApiWithValidTokenShouldBePermitted() {
// given // given
givenCasTicketValidationResponse("ST-fake-cas-ticket", "fake-user-name"); givenCasTicketValidationResponse("ST-fake-cas-ticket", "fake-user-name");
@@ -137,7 +137,7 @@ class WebSecurityConfigIntegrationTest {
} }
@Test @Test
void accessToPongApiWithInvalidTokenShouldBeDenied() { void accessToProtectedApiWithInvalidTokenShouldBeDenied() {
// given // given
givenCasTicketValidationResponse("ST-fake-cas-ticket", "fake-user-name"); givenCasTicketValidationResponse("ST-fake-cas-ticket", "fake-user-name");
@@ -190,7 +190,7 @@ class HsCredentialsControllerRestTest {
given(rbacPersonRepo.findByUuid(personUuid)).willReturn(Optional.of( given(rbacPersonRepo.findByUuid(personUuid)).willReturn(Optional.of(
HsOfficePersonRbacEntity.builder().uuid(personUuid).personType(NATURAL_PERSON).build() HsOfficePersonRbacEntity.builder().uuid(personUuid).personType(NATURAL_PERSON).build()
)); ));
given(rbacPersonRepo.findPersonsrepresentedByPersonWithUuid(personUuid)).willReturn(List.of( given(rbacPersonRepo.findPersonsRepresentedByPersonWithUuid(personUuid)).willReturn(List.of(
// some persons, but not the one from the login-user itself // some persons, but not the one from the login-user itself
HsOfficePersonRbacEntity.builder().uuid(UUID.randomUUID()).personType(NATURAL_PERSON).build(), HsOfficePersonRbacEntity.builder().uuid(UUID.randomUUID()).personType(NATURAL_PERSON).build(),
HsOfficePersonRbacEntity.builder().uuid(UUID.randomUUID()).personType(LEGAL_PERSON).build() HsOfficePersonRbacEntity.builder().uuid(UUID.randomUUID()).personType(LEGAL_PERSON).build()
@@ -4,6 +4,7 @@ import io.restassured.RestAssured;
import io.restassured.http.ContentType; import io.restassured.http.ContentType;
import net.hostsharing.hsadminng.HsadminNgApplication; import net.hostsharing.hsadminng.HsadminNgApplication;
import net.hostsharing.hsadminng.config.MessageTranslator; import net.hostsharing.hsadminng.config.MessageTranslator;
import net.hostsharing.hsadminng.config.MessagesResourceConfig;
import net.hostsharing.hsadminng.context.Context; import net.hostsharing.hsadminng.context.Context;
import net.hostsharing.hsadminng.hs.office.membership.HsOfficeMembershipRepository; import net.hostsharing.hsadminng.hs.office.membership.HsOfficeMembershipRepository;
import net.hostsharing.hsadminng.rbac.test.ContextBasedTestWithCleanup; import net.hostsharing.hsadminng.rbac.test.ContextBasedTestWithCleanup;
@@ -36,7 +37,7 @@ import static org.hamcrest.Matchers.startsWith;
@SpringBootTest( @SpringBootTest(
webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT,
classes = { HsadminNgApplication.class, DisableSecurityConfig.class, JpaAttempt.class, classes = { HsadminNgApplication.class, DisableSecurityConfig.class, JpaAttempt.class,
MessageTranslator.class} MessagesResourceConfig.class, MessageTranslator.class}
) )
@ActiveProfiles("test") @ActiveProfiles("test")
@Transactional @Transactional
@@ -2,6 +2,7 @@ package net.hostsharing.hsadminng.hs.office.membership;
import io.hypersistence.utils.hibernate.type.range.Range; import io.hypersistence.utils.hibernate.type.range.Range;
import net.hostsharing.hsadminng.config.MessageTranslator; import net.hostsharing.hsadminng.config.MessageTranslator;
import net.hostsharing.hsadminng.config.MessagesResourceConfig;
import net.hostsharing.hsadminng.context.Context; import net.hostsharing.hsadminng.context.Context;
import net.hostsharing.hsadminng.hs.office.coopassets.HsOfficeCoopAssetsTransactionRepository; import net.hostsharing.hsadminng.hs.office.coopassets.HsOfficeCoopAssetsTransactionRepository;
import net.hostsharing.hsadminng.hs.office.partner.HsOfficePartnerRbacEntity; import net.hostsharing.hsadminng.hs.office.partner.HsOfficePartnerRbacEntity;
@@ -41,7 +42,7 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@WebMvcTest(HsOfficeMembershipController.class) @WebMvcTest(HsOfficeMembershipController.class)
@Import({StrictMapper.class, DisableSecurityConfig.class, MessageTranslator.class}) @Import({ StrictMapper.class, DisableSecurityConfig.class, MessagesResourceConfig.class, MessageTranslator.class})
@ActiveProfiles("test") @ActiveProfiles("test")
public class HsOfficeMembershipControllerRestTest { public class HsOfficeMembershipControllerRestTest {
@@ -259,14 +259,14 @@ class HsOfficePersonRbacRepositoryIntegrationTest extends ContextBasedTestWithCl
} }
@Test @Test
public void findPersonsrepresentedByPersonWithUuid() { public void findPersonsRepresentedByPersonWithUuid() {
// given // given
context("superuser-alex@hostsharing.net"); context("superuser-alex@hostsharing.net");
final var personUuid = personRbacRepo.findPersonByOptionalNameLike("Fouler").getFirst().getUuid(); final var personUuid = personRbacRepo.findPersonByOptionalNameLike("Fouler").getFirst().getUuid();
// when // when
@SuppressWarnings("unchecked") final List<HsOfficePersonRbacEntity> representedPersons = personRbacRepo.findPersonsrepresentedByPersonWithUuid(personUuid); @SuppressWarnings("unchecked") final List<HsOfficePersonRbacEntity> representedPersons = personRbacRepo.findPersonsRepresentedByPersonWithUuid(personUuid);
// then // then
assertThat(representedPersons).map(Object::toString).containsExactlyInAnyOrder( assertThat(representedPersons).map(Object::toString).containsExactlyInAnyOrder(
@@ -6,6 +6,7 @@ import net.hostsharing.hsadminng.config.DisableSecurityConfig;
import net.hostsharing.hsadminng.context.Context; import net.hostsharing.hsadminng.context.Context;
import net.hostsharing.hsadminng.rbac.test.JpaAttempt; import net.hostsharing.hsadminng.rbac.test.JpaAttempt;
import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.EnumSource; import org.junit.jupiter.params.provider.EnumSource;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
@@ -40,28 +41,44 @@ class PingControllerAcceptanceTest {
@Autowired @Autowired
Context contextMock; Context contextMock;
enum PingTranslationTestCase { enum PongTranslationTestCase {
EN(Locale.ENGLISH, "pong superuser-alex@hostsharing.net - in English"), EN(Locale.ENGLISH, "ponged superuser-alex@hostsharing.net - in English"),
DE(Locale.GERMAN, "pong superuser-alex@hostsharing.net - auf Deutsch"), DE(Locale.GERMAN, "ponged superuser-alex@hostsharing.net - auf Deutsch");
FR(Locale.FRENCH, "pong superuser-alex@hostsharing.net - in English [fr translation missing]");
Locale givenLocale; Locale givenLocale;
CharSequence expectedPongTranslation; CharSequence expectedPongTranslation;
PingTranslationTestCase(final Locale givenLocale, final String expectedPongTranslation) { PongTranslationTestCase(final Locale givenLocale, final String expectedPongTranslation) {
this.givenLocale = givenLocale; this.givenLocale = givenLocale;
this.expectedPongTranslation = expectedPongTranslation; this.expectedPongTranslation = expectedPongTranslation;
} }
} }
@ParameterizedTest @ParameterizedTest
@EnumSource(PingTranslationTestCase.class) @EnumSource(PongTranslationTestCase.class)
void pingRepliesWithTranslatedPongResponse(final PingTranslationTestCase testCase) { void pongRepliesWithTranslatedPongResponse(final PongTranslationTestCase testCase) {
final var responseBody = RestAssured // @formatter:off final var responseBody = RestAssured // @formatter:off
.given() .given()
.header("Authorization", "Bearer superuser-alex@hostsharing.net") .header("Authorization", "Bearer superuser-alex@hostsharing.net")
.header("Accept-Language", testCase.givenLocale) .header("Accept-Language", testCase.givenLocale)
.port(port) .port(port)
.when()
.get("http://localhost/api/pong")
.then().log().all().assertThat()
.statusCode(200)
.contentType("text/plain;charset=UTF-8")
.extract().body().asString();
// @formatter:on
assertThat(responseBody).isEqualTo(testCase.expectedPongTranslation + "\n");
}
@Test
void pingRepliesWithTranslatedPongResponse() {
final var responseBody = RestAssured // @formatter:off
.given()
.header("Accept-Language", Locale.GERMAN)
.port(port)
.when() .when()
.get("http://localhost/api/ping") .get("http://localhost/api/ping")
.then().log().all().assertThat() .then().log().all().assertThat()
@@ -70,6 +87,6 @@ class PingControllerAcceptanceTest {
.extract().body().asString(); .extract().body().asString();
// @formatter:on // @formatter:on
assertThat(responseBody).isEqualTo(testCase.expectedPongTranslation + "\n"); assertThat(responseBody).isEqualTo("pinged - auf Deutsch\n");
} }
} }
@@ -38,8 +38,8 @@ class PingControllerRestTest {
@RequiredArgsConstructor @RequiredArgsConstructor
enum I18nTestCases { enum I18nTestCases {
EN("en", "pong anonymousUser - in English"), EN("en", "pinged - in English"),
DE("de", "pong anonymousUser - auf Deutsch"); DE("de", "pinged - auf Deutsch");
final String language; final String language;
final String expectedTranslation; final String expectedTranslation;