From cb4aecb9c872f13103a21e05f96ef57ef8397b7a Mon Sep 17 00:00:00 2001 From: Michael Hoennig Date: Thu, 26 Sep 2024 10:51:27 +0200 Subject: [PATCH] refactoring for implicit creation of dependend hosting-assets (#108) Co-authored-by: Michael Hoennig Reviewed-on: https://dev.hostsharing.net/hostsharing/hs.hsadmin.ng/pulls/108 Reviewed-by: Timotheus Pokorra --- README.md | 27 +++- .../hsadminng/errors/DisplayAs.java | 12 +- .../booking/item/HsBookingItemController.java | 37 +++-- .../BookingItemEntitySaveProcessor.java | 131 ++++++++++++++++++ .../HsBookingItemEntityValidatorRegistry.java | 5 +- .../HsDomainSetupBookingItemValidator.java | 10 ++ .../project/HsBookingProjectController.java | 4 +- .../asset/HsHostingAssetController.java | 4 +- .../HsOfficeBankAccountController.java | 4 +- .../contact/HsOfficeContactController.java | 4 +- ...OfficeCoopAssetsTransactionController.java | 4 +- ...OfficeCoopSharesTransactionController.java | 4 +- .../debitor/HsOfficeDebitorController.java | 29 ++-- .../HsOfficeMembershipController.java | 4 +- .../HsOfficeMembershipEntityPatcher.java | 6 +- .../partner/HsOfficePartnerController.java | 4 +- .../person/HsOfficePersonController.java | 4 +- .../relation/HsOfficeRelationController.java | 4 +- .../HsOfficeSepaMandateController.java | 4 +- .../hs/validation/HsEntityValidator.java | 2 +- .../hostsharing/hsadminng/mapper/Mapper.java | 43 ++++-- .../hsadminng/mapper/MapperConfiguration.java | 13 -- .../hsadminng/mapper/StandardMapper.java | 14 ++ .../hsadminng/mapper/StrictMapper.java | 19 +++ .../persistence/EntityExistsValidator.java | 39 ++++++ .../rbac/grant/RbacGrantController.java | 4 +- .../rbac/role/RbacRoleController.java | 4 +- .../rbac/subject/RbacSubjectController.java | 4 +- .../test/cust/TestCustomerController.java | 4 +- .../rbac/test/pac/TestPackageController.java | 4 +- .../item/HsBookingItemControllerRestTest.java | 28 +++- ...mainSetupBookingItemValidatorUnitTest.java | 55 +++++++- .../HsHostingAssetControllerRestTest.java | 10 +- ...ainSetupHostingAssetValidatorUnitTest.java | 3 +- .../hs/migration/ImportHostingAssets.java | 8 +- ...HsOfficeBankAccountControllerRestTest.java | 4 +- ...opAssetsTransactionControllerRestTest.java | 4 +- ...opSharesTransactionControllerRestTest.java | 4 +- ...OfficeDebitorControllerAcceptanceTest.java | 2 +- .../HsOfficeMembershipControllerRestTest.java | 6 +- ...OfficeMembershipEntityPatcherUnitTest.java | 4 +- .../HsOfficePartnerControllerRestTest.java | 4 +- ...ceSepaMandateControllerAcceptanceTest.java | 4 +- .../rbac/context/ContextIntegrationTests.java | 4 +- .../rbac/role/RbacRoleControllerRestTest.java | 4 +- .../RbacSubjectControllerRestTest.java | 4 +- .../hsadminng/rbac/test/MapperUnitTest.java | 14 +- 47 files changed, 474 insertions(+), 139 deletions(-) create mode 100644 src/main/java/net/hostsharing/hsadminng/hs/booking/item/validators/BookingItemEntitySaveProcessor.java delete mode 100644 src/main/java/net/hostsharing/hsadminng/mapper/MapperConfiguration.java create mode 100644 src/main/java/net/hostsharing/hsadminng/mapper/StandardMapper.java create mode 100644 src/main/java/net/hostsharing/hsadminng/mapper/StrictMapper.java create mode 100644 src/main/java/net/hostsharing/hsadminng/persistence/EntityExistsValidator.java diff --git a/README.md b/README.md index 37777227..e1d1515b 100644 --- a/README.md +++ b/README.md @@ -550,12 +550,37 @@ Dependency versions can be automatically upgraded to the latest available versio gw useLatestVersions ``` -Afterwards, `gw check` is automatically started. +Afterward, `gw check` is automatically started. Please only commit+push to master if the check run shows no errors. More infos, e.g. on blacklists see on the [project's website](https://github.com/patrikerdes/gradle-use-latest-versions-plugin). +## Biggest Flaws in our Architecture + +### The RBAC System is too Complicated + +Now, where we have a better experience with what we really need from the RBAC system, we have learned +that and creates too many (grant- and role-) rows and too even tables which could be avoided completely. + +The basic idea is always to always have a fixed set of ordered role-types which apply for all DB-tables under RBAC, +e.g. OWNER>ADMIN>AGENT\[>PROXY?\]>TENENT>REFERRER. +Grants between these for the same DB-row would be implicit by order comparision. +This way we would get rid of all explicit grants within the same DB-row +and would not need the `rbac.role` table anymore. +We would also reduce the depth of the expensive recursive CTE-query. + +This has to be explored further. +For now, we just keep it in mind and + +### The Mapper is Error-Prone + +Where `org.modelmapper.ModelMapper` reduces bloat-code a lot and has some nice features about recursive data-structure mappings, +it often causes strange errors which are hard to fix. +E.g. the uuid of the target main object is often taken from an uuid of a sub-subject. +(For now, use `StrictMapper` to avoid this, for the case it happens.) + + ## How To ... ### How to Configure .pgpass for the Default PostgreSQL Database? diff --git a/src/main/java/net/hostsharing/hsadminng/errors/DisplayAs.java b/src/main/java/net/hostsharing/hsadminng/errors/DisplayAs.java index 020d006a..20723330 100644 --- a/src/main/java/net/hostsharing/hsadminng/errors/DisplayAs.java +++ b/src/main/java/net/hostsharing/hsadminng/errors/DisplayAs.java @@ -9,15 +9,25 @@ import java.lang.annotation.Target; @Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) public @interface DisplayAs { + class DisplayName { + public static String of(final Class clazz) { - final var displayNameAnnot = clazz.getAnnotation(DisplayAs.class); + final var displayNameAnnot = getDisplayNameAnnotation(clazz); return displayNameAnnot != null ? displayNameAnnot.value() : clazz.getSimpleName(); } public static String of(@NotNull final Object instance) { return of(instance.getClass()); } + + private static DisplayAs getDisplayNameAnnotation(final Class clazz) { + if (clazz == null) { + return null; + } + final var annot = clazz.getAnnotation(DisplayAs.class); + return annot != null ? annot : getDisplayNameAnnotation(clazz.getSuperclass()); + } } String value() default ""; diff --git a/src/main/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemController.java b/src/main/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemController.java index 6afd5219..84f35054 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemController.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemController.java @@ -5,17 +5,18 @@ import net.hostsharing.hsadminng.hs.booking.generated.api.v1.api.HsBookingItemsA import net.hostsharing.hsadminng.hs.booking.generated.api.v1.model.HsBookingItemInsertResource; import net.hostsharing.hsadminng.hs.booking.generated.api.v1.model.HsBookingItemPatchResource; import net.hostsharing.hsadminng.hs.booking.generated.api.v1.model.HsBookingItemResource; +import net.hostsharing.hsadminng.hs.booking.item.validators.BookingItemEntitySaveProcessor; import net.hostsharing.hsadminng.hs.booking.item.validators.HsBookingItemEntityValidatorRegistry; +import net.hostsharing.hsadminng.hs.booking.project.HsBookingProjectRealEntity; import net.hostsharing.hsadminng.mapper.KeyValueMap; -import net.hostsharing.hsadminng.mapper.Mapper; +import net.hostsharing.hsadminng.mapper.StrictMapper; +import net.hostsharing.hsadminng.persistence.EntityManagerWrapper; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.ResponseEntity; import org.springframework.transaction.annotation.Transactional; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.servlet.mvc.method.annotation.MvcUriComponentsBuilder; -import jakarta.persistence.EntityManager; -import jakarta.persistence.PersistenceContext; import java.time.LocalDate; import java.util.List; import java.util.UUID; @@ -30,13 +31,13 @@ public class HsBookingItemController implements HsBookingItemsApi { private Context context; @Autowired - private Mapper mapper; + private StrictMapper mapper; @Autowired private HsBookingItemRbacRepository bookingItemRepo; - @PersistenceContext - private EntityManager em; + @Autowired + private EntityManagerWrapper em; @Override @Transactional(readOnly = true) @@ -48,7 +49,7 @@ public class HsBookingItemController implements HsBookingItemsApi { final var entities = bookingItemRepo.findAllByProjectUuid(projectUuid); - final var resources = mapper.mapList(entities, HsBookingItemResource.class, ENTITY_TO_RESOURCE_POSTMAPPER); + final var resources = mapper.mapList(entities, HsBookingItemResource.class, RBAC_ENTITY_TO_RESOURCE_POSTMAPPER); return ResponseEntity.ok(resources); } @@ -62,15 +63,20 @@ public class HsBookingItemController implements HsBookingItemsApi { context.define(currentSubject, assumedRoles); final var entityToSave = mapper.map(body, HsBookingItemRbacEntity.class, RESOURCE_TO_ENTITY_POSTMAPPER); - - final var saved = HsBookingItemEntityValidatorRegistry.validated(em, bookingItemRepo.save(entityToSave)); + final var mapped = new BookingItemEntitySaveProcessor(em, entityToSave) + .preprocessEntity() + .validateEntity() + .prepareForSave() + .save() + .validateContext() + .mapUsing(e -> mapper.map(e, HsBookingItemResource.class, ITEM_TO_RESOURCE_POSTMAPPER)) + .revampProperties(); final var uri = MvcUriComponentsBuilder.fromController(getClass()) .path("/api/hs/booking/items/{id}") - .buildAndExpand(saved.getUuid()) + .buildAndExpand(mapped.getUuid()) .toUri(); - final var mapped = mapper.map(saved, HsBookingItemResource.class, ENTITY_TO_RESOURCE_POSTMAPPER); return ResponseEntity.created(uri).body(mapped); } @@ -87,7 +93,7 @@ public class HsBookingItemController implements HsBookingItemsApi { result.ifPresent(entity -> em.detach(entity)); // prevent further LAZY-loading return result .map(bookingItemEntity -> ResponseEntity.ok( - mapper.map(bookingItemEntity, HsBookingItemResource.class, ENTITY_TO_RESOURCE_POSTMAPPER))) + mapper.map(bookingItemEntity, HsBookingItemResource.class, RBAC_ENTITY_TO_RESOURCE_POSTMAPPER))) .orElseGet(() -> ResponseEntity.notFound().build()); } @@ -120,18 +126,21 @@ public class HsBookingItemController implements HsBookingItemsApi { new HsBookingItemEntityPatcher(current).apply(body); final var saved = bookingItemRepo.save(HsBookingItemEntityValidatorRegistry.validated(em, current)); - final var mapped = mapper.map(saved, HsBookingItemResource.class, ENTITY_TO_RESOURCE_POSTMAPPER); + final var mapped = mapper.map(saved, HsBookingItemResource.class, RBAC_ENTITY_TO_RESOURCE_POSTMAPPER); return ResponseEntity.ok(mapped); } - final BiConsumer ENTITY_TO_RESOURCE_POSTMAPPER = (entity, resource) -> { + final BiConsumer ITEM_TO_RESOURCE_POSTMAPPER = (entity, resource) -> { resource.setValidFrom(entity.getValidity().lower()); if (entity.getValidity().hasUpperBound()) { resource.setValidTo(entity.getValidity().upper().minusDays(1)); } }; + final BiConsumer RBAC_ENTITY_TO_RESOURCE_POSTMAPPER = ITEM_TO_RESOURCE_POSTMAPPER::accept; + final BiConsumer RESOURCE_TO_ENTITY_POSTMAPPER = (resource, entity) -> { + entity.setProject(em.find(HsBookingProjectRealEntity.class, resource.getProjectUuid())); entity.setValidity(toPostgresDateRange(LocalDate.now(), resource.getValidTo())); entity.putResources(KeyValueMap.from(resource.getResources())); }; diff --git a/src/main/java/net/hostsharing/hsadminng/hs/booking/item/validators/BookingItemEntitySaveProcessor.java b/src/main/java/net/hostsharing/hsadminng/hs/booking/item/validators/BookingItemEntitySaveProcessor.java new file mode 100644 index 00000000..77ce40ae --- /dev/null +++ b/src/main/java/net/hostsharing/hsadminng/hs/booking/item/validators/BookingItemEntitySaveProcessor.java @@ -0,0 +1,131 @@ +package net.hostsharing.hsadminng.hs.booking.item.validators; + +import net.hostsharing.hsadminng.errors.MultiValidationException; +import net.hostsharing.hsadminng.hs.booking.generated.api.v1.model.HsBookingItemResource; +import net.hostsharing.hsadminng.hs.booking.item.HsBookingItem; +import net.hostsharing.hsadminng.hs.validation.HsEntityValidator; + +import jakarta.persistence.EntityManager; +import java.util.Arrays; +import java.util.Map; +import java.util.function.Function; +import java.util.regex.Pattern; + +// TODO.refa: introduce common base class with HsHostingAssetEntitySaveProcessor +/** + * Wraps the steps of the pararation, validation, mapping and revamp around saving of a HsBookingItem into a readable API. + */ +public class BookingItemEntitySaveProcessor { + + private final HsEntityValidator validator; + private String expectedStep = "preprocessEntity"; + private final EntityManager em; + private HsBookingItem entity; + private HsBookingItemResource resource; + + public BookingItemEntitySaveProcessor(final EntityManager em, final HsBookingItem entity) { + this.em = em; + this.entity = entity; + this.validator = HsBookingItemEntityValidatorRegistry.forType(entity.getType()); + } + + /// initial step allowing to set default values before any validations + public BookingItemEntitySaveProcessor preprocessEntity() { + step("preprocessEntity", "validateEntity"); + validator.preprocessEntity(entity); + return this; + } + + /// validates the entity itself including its properties + public BookingItemEntitySaveProcessor validateEntity() { + step("validateEntity", "prepareForSave"); + MultiValidationException.throwIfNotEmpty(validator.validateEntity(entity)); + return this; + } + + // TODO.impl: remove once the migration of legacy data is done + /// validates the entity itself including its properties, but ignoring some error messages for import of legacy data + public BookingItemEntitySaveProcessor validateEntityIgnoring(final String... ignoreRegExp) { + step("validateEntity", "prepareForSave"); + final var ignoreRegExpPatterns = Arrays.stream(ignoreRegExp).map(Pattern::compile).toList(); + MultiValidationException.throwIfNotEmpty( + validator.validateEntity(entity).stream() + .filter(error -> ignoreRegExpPatterns.stream().noneMatch(p -> p.matcher(error).matches() )) + .toList() + ); + return this; + } + + /// hashing passwords etc. + public BookingItemEntitySaveProcessor prepareForSave() { + step("prepareForSave", "save"); + validator.prepareProperties(em, entity); + return this; + } + + /** + * Saves the entity using the given `saveFunction`. + * + *

`validator.postPersist(em, entity)` is NOT called. + * If any postprocessing is necessary, the saveFunction has to implement this.

+ * @param saveFunction + * @return this + */ + public BookingItemEntitySaveProcessor saveUsing(final Function saveFunction) { + step("save", "validateContext"); + entity = saveFunction.apply(entity); + return this; + } + + /** + * Saves the using the `EntityManager`, but does NOT ever merge the entity. + * + *

`validator.postPersist(em, entity)` is called afterwards with the entity guaranteed to be flushed to the database.

+ * @return this + */ + public BookingItemEntitySaveProcessor save() { + return saveUsing(e -> { + if (!em.contains(entity)) { + em.persist(entity); + } + em.flush(); // makes RbacEntity available as RealEntity if needed + validator.postPersist(em, entity); + return entity; + }); + } + + /// validates the entity within it's parent and child hierarchy (e.g. totals validators and other limits) + public BookingItemEntitySaveProcessor validateContext() { + step("validateContext", "mapUsing"); + return HsEntityValidator.doWithEntityManager(em, () -> { + MultiValidationException.throwIfNotEmpty(validator.validateContext(entity)); + return this; + }); + } + + /// maps entity to JSON resource representation + public BookingItemEntitySaveProcessor mapUsing( + final Function mapFunction) { + step("mapUsing", "revampProperties"); + resource = mapFunction.apply(entity); + return this; + } + + /// removes write-only-properties and ads computed-properties + @SuppressWarnings("unchecked") + public HsBookingItemResource revampProperties() { + step("revampProperties", null); + final var revampedProps = validator.revampProperties(em, entity, (Map) resource.getResources()); + resource.setResources(revampedProps); + return resource; + } + + // Makes sure that the steps are called in the correct order. + // Could also be implemented using an interface per method, but that seems exaggerated. + private void step(final String current, final String next) { + if (!expectedStep.equals(current)) { + throw new IllegalStateException("expected " + expectedStep + " but got " + current); + } + expectedStep = next; + } +} diff --git a/src/main/java/net/hostsharing/hsadminng/hs/booking/item/validators/HsBookingItemEntityValidatorRegistry.java b/src/main/java/net/hostsharing/hsadminng/hs/booking/item/validators/HsBookingItemEntityValidatorRegistry.java index 8bfe12fd..6567ae83 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/booking/item/validators/HsBookingItemEntityValidatorRegistry.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/booking/item/validators/HsBookingItemEntityValidatorRegistry.java @@ -48,10 +48,11 @@ public class HsBookingItemEntityValidatorRegistry { } public static List doValidate(final EntityManager em, final HsBookingItem bookingItem) { + final var bookingItemValidator = HsBookingItemEntityValidatorRegistry.forType(bookingItem.getType()); return HsEntityValidator.doWithEntityManager(em, () -> HsEntityValidator.sequentiallyValidate( - () -> HsBookingItemEntityValidatorRegistry.forType(bookingItem.getType()).validateEntity(bookingItem), - () -> HsBookingItemEntityValidatorRegistry.forType(bookingItem.getType()).validateContext(bookingItem)) + () -> bookingItemValidator.validateEntity(bookingItem), + () -> bookingItemValidator.validateContext(bookingItem)) ); } diff --git a/src/main/java/net/hostsharing/hsadminng/hs/booking/item/validators/HsDomainSetupBookingItemValidator.java b/src/main/java/net/hostsharing/hsadminng/hs/booking/item/validators/HsDomainSetupBookingItemValidator.java index c9fd731a..f42ea4e0 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/booking/item/validators/HsDomainSetupBookingItemValidator.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/booking/item/validators/HsDomainSetupBookingItemValidator.java @@ -1,3 +1,4 @@ + package net.hostsharing.hsadminng.hs.booking.item.validators; import net.hostsharing.hsadminng.hs.booking.item.HsBookingItem; @@ -15,6 +16,9 @@ class HsDomainSetupBookingItemValidator extends HsBookingItemEntityValidator { public static final String FQDN_REGEX = "^((?!-)[A-Za-z0-9-]{1,63}(?> T validateEntityExists(final String property, final T entitySkeleton) { - final var foundEntity = em.find(entitySkeleton.getClass(), entitySkeleton.getUuid()); - if ( foundEntity == null) { - throw new ValidationException("Unable to find " + DisplayName.of(entitySkeleton) + " by " + property + ": " + entitySkeleton.getUuid()); - } - - //noinspection unchecked - return (T) foundEntity; - } } diff --git a/src/main/java/net/hostsharing/hsadminng/hs/office/membership/HsOfficeMembershipController.java b/src/main/java/net/hostsharing/hsadminng/hs/office/membership/HsOfficeMembershipController.java index 8c87e5fa..d63f9e6a 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/office/membership/HsOfficeMembershipController.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/office/membership/HsOfficeMembershipController.java @@ -5,7 +5,7 @@ import net.hostsharing.hsadminng.hs.office.generated.api.v1.api.HsOfficeMembersh import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficeMembershipInsertResource; import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficeMembershipPatchResource; import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficeMembershipResource; -import net.hostsharing.hsadminng.mapper.Mapper; +import net.hostsharing.hsadminng.mapper.StandardMapper; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.ResponseEntity; import org.springframework.transaction.annotation.Transactional; @@ -24,7 +24,7 @@ public class HsOfficeMembershipController implements HsOfficeMembershipsApi { private Context context; @Autowired - private Mapper mapper; + private StandardMapper mapper; @Autowired private HsOfficeMembershipRepository membershipRepo; diff --git a/src/main/java/net/hostsharing/hsadminng/hs/office/membership/HsOfficeMembershipEntityPatcher.java b/src/main/java/net/hostsharing/hsadminng/hs/office/membership/HsOfficeMembershipEntityPatcher.java index cbecb800..33bf363b 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/office/membership/HsOfficeMembershipEntityPatcher.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/office/membership/HsOfficeMembershipEntityPatcher.java @@ -2,18 +2,18 @@ package net.hostsharing.hsadminng.hs.office.membership; import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficeMembershipPatchResource; import net.hostsharing.hsadminng.mapper.EntityPatcher; -import net.hostsharing.hsadminng.mapper.Mapper; +import net.hostsharing.hsadminng.mapper.StandardMapper; import net.hostsharing.hsadminng.mapper.OptionalFromJson; import java.util.Optional; public class HsOfficeMembershipEntityPatcher implements EntityPatcher { - private final Mapper mapper; + private final StandardMapper mapper; private final HsOfficeMembershipEntity entity; public HsOfficeMembershipEntityPatcher( - final Mapper mapper, + final StandardMapper mapper, final HsOfficeMembershipEntity entity) { this.mapper = mapper; this.entity = entity; 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 b4b8bd75..55c280f3 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 @@ -12,7 +12,7 @@ import net.hostsharing.hsadminng.hs.office.person.HsOfficePersonEntity; import net.hostsharing.hsadminng.hs.office.relation.HsOfficeRelationRealEntity; import net.hostsharing.hsadminng.hs.office.relation.HsOfficeRelationRealRepository; import net.hostsharing.hsadminng.hs.office.relation.HsOfficeRelationType; -import net.hostsharing.hsadminng.mapper.Mapper; +import net.hostsharing.hsadminng.mapper.StandardMapper; import net.hostsharing.hsadminng.rbac.object.BaseEntity; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; @@ -36,7 +36,7 @@ public class HsOfficePartnerController implements HsOfficePartnersApi { private Context context; @Autowired - private Mapper mapper; + private StandardMapper mapper; @Autowired private HsOfficePartnerRepository partnerRepo; diff --git a/src/main/java/net/hostsharing/hsadminng/hs/office/person/HsOfficePersonController.java b/src/main/java/net/hostsharing/hsadminng/hs/office/person/HsOfficePersonController.java index 41d9d441..ac746aab 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/office/person/HsOfficePersonController.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/office/person/HsOfficePersonController.java @@ -1,6 +1,6 @@ package net.hostsharing.hsadminng.hs.office.person; -import net.hostsharing.hsadminng.mapper.Mapper; +import net.hostsharing.hsadminng.mapper.StandardMapper; import net.hostsharing.hsadminng.context.Context; import net.hostsharing.hsadminng.hs.office.generated.api.v1.api.HsOfficePersonsApi; import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficePersonInsertResource; @@ -23,7 +23,7 @@ public class HsOfficePersonController implements HsOfficePersonsApi { private Context context; @Autowired - private Mapper mapper; + private StandardMapper mapper; @Autowired private HsOfficePersonRepository personRepo; diff --git a/src/main/java/net/hostsharing/hsadminng/hs/office/relation/HsOfficeRelationController.java b/src/main/java/net/hostsharing/hsadminng/hs/office/relation/HsOfficeRelationController.java index f054e563..b93537d9 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/office/relation/HsOfficeRelationController.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/office/relation/HsOfficeRelationController.java @@ -5,7 +5,7 @@ import net.hostsharing.hsadminng.hs.office.contact.HsOfficeContactRealRepository import net.hostsharing.hsadminng.hs.office.generated.api.v1.api.HsOfficeRelationsApi; import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.*; import net.hostsharing.hsadminng.hs.office.person.HsOfficePersonRepository; -import net.hostsharing.hsadminng.mapper.Mapper; +import net.hostsharing.hsadminng.mapper.StandardMapper; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.ResponseEntity; import org.springframework.transaction.annotation.Transactional; @@ -28,7 +28,7 @@ public class HsOfficeRelationController implements HsOfficeRelationsApi { private Context context; @Autowired - private Mapper mapper; + private StandardMapper mapper; @Autowired private HsOfficeRelationRbacRepository relationRbacRepo; diff --git a/src/main/java/net/hostsharing/hsadminng/hs/office/sepamandate/HsOfficeSepaMandateController.java b/src/main/java/net/hostsharing/hsadminng/hs/office/sepamandate/HsOfficeSepaMandateController.java index 9511bdd6..52ef2aad 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/office/sepamandate/HsOfficeSepaMandateController.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/office/sepamandate/HsOfficeSepaMandateController.java @@ -5,7 +5,7 @@ import net.hostsharing.hsadminng.hs.office.generated.api.v1.api.HsOfficeSepaMand import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficeSepaMandateInsertResource; import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficeSepaMandatePatchResource; import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficeSepaMandateResource; -import net.hostsharing.hsadminng.mapper.Mapper; +import net.hostsharing.hsadminng.mapper.StandardMapper; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.ResponseEntity; import org.springframework.transaction.annotation.Transactional; @@ -28,7 +28,7 @@ public class HsOfficeSepaMandateController implements HsOfficeSepaMandatesApi { private Context context; @Autowired - private Mapper mapper; + private StandardMapper mapper; @Autowired private HsOfficeSepaMandateRepository sepaMandateRepo; diff --git a/src/main/java/net/hostsharing/hsadminng/hs/validation/HsEntityValidator.java b/src/main/java/net/hostsharing/hsadminng/hs/validation/HsEntityValidator.java index b2fa8a02..68f2779f 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/validation/HsEntityValidator.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/validation/HsEntityValidator.java @@ -130,7 +130,7 @@ public abstract class HsEntityValidator { } public Map revampProperties(final EntityManager em, final E entity, final Map config) { - final var copy = new HashMap<>(config); + final var copy = config != null ? new HashMap<>(config) : new HashMap(); stream(propertyValidators).forEach(p -> { if (p.isWriteOnly()) { copy.remove(p.propertyName); diff --git a/src/main/java/net/hostsharing/hsadminng/mapper/Mapper.java b/src/main/java/net/hostsharing/hsadminng/mapper/Mapper.java index 9fda5165..21779a5c 100644 --- a/src/main/java/net/hostsharing/hsadminng/mapper/Mapper.java +++ b/src/main/java/net/hostsharing/hsadminng/mapper/Mapper.java @@ -11,18 +11,20 @@ import java.lang.reflect.Field; import java.util.List; import java.util.function.BiConsumer; import java.util.stream.Collectors; +import java.util.stream.Stream; +import static java.util.Arrays.stream; import static net.hostsharing.hsadminng.errors.DisplayAs.DisplayName; /** * A nicer API for ModelMapper. */ -public class Mapper extends ModelMapper { +abstract class Mapper extends ModelMapper { @PersistenceContext EntityManager em; - public Mapper() { + Mapper() { getConfiguration().setAmbiguityIgnored(true); } @@ -45,8 +47,12 @@ public class Mapper extends ModelMapper { @Override public D map(final Object source, final Class destinationType) { + return map("", source, destinationType); + } + + public D map(final String namePrefix, final Object source, final Class destinationType) { final var target = super.map(source, destinationType); - for (Field f : destinationType.getDeclaredFields()) { + for (Field f : getDeclaredFieldsIncludingSuperClasses(destinationType)) { if (f.getAnnotation(ManyToOne.class) == null) { continue; } @@ -64,18 +70,30 @@ public class Mapper extends ModelMapper { if (subEntityUuid == null) { continue; } - ReflectionUtils.setField(f, target, findEntityById(f.getType(), subEntityUuid)); + ReflectionUtils.setField(f, target, fetchEntity(namePrefix + f.getName() + ".uuid", f.getType(), subEntityUuid)); } return target; } - private Object findEntityById(final Class entityClass, final Object subEntityUuid) { - // using getReference would be more efficent, but results in very technical error messages - final var entity = em.find(entityClass, subEntityUuid); + private static Field[] getDeclaredFieldsIncludingSuperClasses(final Class destinationType) { + if (destinationType == null) { + return new Field[0]; + } + + return Stream.concat( + stream(destinationType.getDeclaredFields()), + stream(getDeclaredFieldsIncludingSuperClasses(destinationType.getSuperclass()))) + .toArray(Field[]::new); + } + + public E fetchEntity(final String propertyName, final Class entityClass, final Object subEntityUuid) { + final var entity = em.getReference(entityClass, subEntityUuid); if (entity != null) { return entity; } - throw new ValidationException("Unable to find " + DisplayName.of(entityClass) + " by uuid: " + subEntityUuid); + throw new ValidationException( + "Unable to find " + DisplayName.of(entityClass) + + " by " + propertyName + ": " + subEntityUuid); } public T map(final S source, final Class targetClass, final BiConsumer postMapper) { @@ -86,4 +104,13 @@ public class Mapper extends ModelMapper { postMapper.accept(source, target); return target; } + + public T map(final String namePrefix, final S source, final Class targetClass, final BiConsumer postMapper) { + if (source == null) { + return null; + } + final var target = map(source, targetClass); + postMapper.accept(source, target); + return target; + } } diff --git a/src/main/java/net/hostsharing/hsadminng/mapper/MapperConfiguration.java b/src/main/java/net/hostsharing/hsadminng/mapper/MapperConfiguration.java deleted file mode 100644 index a77b953a..00000000 --- a/src/main/java/net/hostsharing/hsadminng/mapper/MapperConfiguration.java +++ /dev/null @@ -1,13 +0,0 @@ -package net.hostsharing.hsadminng.mapper; - -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; - -@Configuration -public class MapperConfiguration { - - @Bean - public Mapper modelMapper() { - return new Mapper(); - } -} diff --git a/src/main/java/net/hostsharing/hsadminng/mapper/StandardMapper.java b/src/main/java/net/hostsharing/hsadminng/mapper/StandardMapper.java new file mode 100644 index 00000000..42725d3d --- /dev/null +++ b/src/main/java/net/hostsharing/hsadminng/mapper/StandardMapper.java @@ -0,0 +1,14 @@ +package net.hostsharing.hsadminng.mapper; + +import org.springframework.stereotype.Component; + +/** + * A nicer API for ModelMapper in standard mode. + */ +@Component +public class StandardMapper extends Mapper { + + public StandardMapper() { + getConfiguration().setAmbiguityIgnored(true); + } +} diff --git a/src/main/java/net/hostsharing/hsadminng/mapper/StrictMapper.java b/src/main/java/net/hostsharing/hsadminng/mapper/StrictMapper.java new file mode 100644 index 00000000..a6d3c3fc --- /dev/null +++ b/src/main/java/net/hostsharing/hsadminng/mapper/StrictMapper.java @@ -0,0 +1,19 @@ +package net.hostsharing.hsadminng.mapper; + +import org.springframework.stereotype.Component; + +import static org.modelmapper.convention.MatchingStrategies.STRICT; + +/** + * A nicer API for ModelMapper in strict mode. + * + *

This makes sure that resource.whateverUuid does not accidentally get mapped to entity.uuid, + * if resource.uuid does not exist.

+ */ +@Component +public class StrictMapper extends Mapper { + + public StrictMapper() { + getConfiguration().setMatchingStrategy(STRICT); + } +} diff --git a/src/main/java/net/hostsharing/hsadminng/persistence/EntityExistsValidator.java b/src/main/java/net/hostsharing/hsadminng/persistence/EntityExistsValidator.java new file mode 100644 index 00000000..fac98d33 --- /dev/null +++ b/src/main/java/net/hostsharing/hsadminng/persistence/EntityExistsValidator.java @@ -0,0 +1,39 @@ +package net.hostsharing.hsadminng.persistence; + +import net.hostsharing.hsadminng.errors.DisplayAs.DisplayName; +import net.hostsharing.hsadminng.rbac.object.BaseEntity; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import jakarta.persistence.Entity; +import jakarta.validation.ValidationException; + +@Service +public class EntityExistsValidator { + + @Autowired + private EntityManagerWrapper em; + + public > void validateEntityExists(final String property, final T entitySkeleton) { + final var foundEntity = em.find(entityClass(entitySkeleton), entitySkeleton.getUuid()); + if ( foundEntity == null) { + throw new ValidationException("Unable to find " + DisplayName.of(entitySkeleton) + " by " + property + ": " + entitySkeleton.getUuid()); + } + } + + private static > Class entityClass(final T entityOrProxy) { + final var entityClass = entityClass(entityOrProxy.getClass()); + if (entityClass == null) { + throw new IllegalArgumentException("@Entity not found in superclass hierarchy of " + entityOrProxy.getClass()); + } + return entityClass; + } + + private static Class entityClass(final Class entityOrProxyClass) { + return entityOrProxyClass.isAnnotationPresent(Entity.class) + ? entityOrProxyClass + : entityOrProxyClass.getSuperclass() == null + ? null + : entityClass(entityOrProxyClass.getSuperclass()); + } +} diff --git a/src/main/java/net/hostsharing/hsadminng/rbac/grant/RbacGrantController.java b/src/main/java/net/hostsharing/hsadminng/rbac/grant/RbacGrantController.java index 4ca538b9..6af53104 100644 --- a/src/main/java/net/hostsharing/hsadminng/rbac/grant/RbacGrantController.java +++ b/src/main/java/net/hostsharing/hsadminng/rbac/grant/RbacGrantController.java @@ -1,7 +1,7 @@ package net.hostsharing.hsadminng.rbac.grant; import net.hostsharing.hsadminng.context.Context; -import net.hostsharing.hsadminng.mapper.Mapper; +import net.hostsharing.hsadminng.mapper.StandardMapper; import net.hostsharing.hsadminng.rbac.generated.api.v1.api.RbacGrantsApi; import net.hostsharing.hsadminng.rbac.generated.api.v1.model.RbacGrantResource; import org.springframework.beans.factory.annotation.Autowired; @@ -22,7 +22,7 @@ public class RbacGrantController implements RbacGrantsApi { private Context context; @Autowired - private Mapper mapper; + private StandardMapper mapper; @Autowired private RbacGrantRepository rbacGrantRepository; diff --git a/src/main/java/net/hostsharing/hsadminng/rbac/role/RbacRoleController.java b/src/main/java/net/hostsharing/hsadminng/rbac/role/RbacRoleController.java index 5da97292..cffff888 100644 --- a/src/main/java/net/hostsharing/hsadminng/rbac/role/RbacRoleController.java +++ b/src/main/java/net/hostsharing/hsadminng/rbac/role/RbacRoleController.java @@ -1,7 +1,7 @@ package net.hostsharing.hsadminng.rbac.role; import net.hostsharing.hsadminng.context.Context; -import net.hostsharing.hsadminng.mapper.Mapper; +import net.hostsharing.hsadminng.mapper.StandardMapper; import net.hostsharing.hsadminng.rbac.generated.api.v1.api.RbacRolesApi; import net.hostsharing.hsadminng.rbac.generated.api.v1.model.RbacRoleResource; import org.springframework.beans.factory.annotation.Autowired; @@ -18,7 +18,7 @@ public class RbacRoleController implements RbacRolesApi { private Context context; @Autowired - private Mapper mapper; + private StandardMapper mapper; @Autowired private RbacRoleRepository rbacRoleRepository; diff --git a/src/main/java/net/hostsharing/hsadminng/rbac/subject/RbacSubjectController.java b/src/main/java/net/hostsharing/hsadminng/rbac/subject/RbacSubjectController.java index 52c0649b..1676cc7c 100644 --- a/src/main/java/net/hostsharing/hsadminng/rbac/subject/RbacSubjectController.java +++ b/src/main/java/net/hostsharing/hsadminng/rbac/subject/RbacSubjectController.java @@ -1,7 +1,7 @@ package net.hostsharing.hsadminng.rbac.subject; import net.hostsharing.hsadminng.context.Context; -import net.hostsharing.hsadminng.mapper.Mapper; +import net.hostsharing.hsadminng.mapper.StandardMapper; import net.hostsharing.hsadminng.rbac.generated.api.v1.api.RbacSubjectsApi; import net.hostsharing.hsadminng.rbac.generated.api.v1.model.RbacSubjectPermissionResource; import net.hostsharing.hsadminng.rbac.generated.api.v1.model.RbacSubjectResource; @@ -21,7 +21,7 @@ public class RbacSubjectController implements RbacSubjectsApi { private Context context; @Autowired - private Mapper mapper; + private StandardMapper mapper; @Autowired private RbacSubjectRepository rbacSubjectRepository; diff --git a/src/main/java/net/hostsharing/hsadminng/rbac/test/cust/TestCustomerController.java b/src/main/java/net/hostsharing/hsadminng/rbac/test/cust/TestCustomerController.java index c6bbc115..26628545 100644 --- a/src/main/java/net/hostsharing/hsadminng/rbac/test/cust/TestCustomerController.java +++ b/src/main/java/net/hostsharing/hsadminng/rbac/test/cust/TestCustomerController.java @@ -1,7 +1,7 @@ package net.hostsharing.hsadminng.rbac.test.cust; import net.hostsharing.hsadminng.context.Context; -import net.hostsharing.hsadminng.mapper.Mapper; +import net.hostsharing.hsadminng.mapper.StandardMapper; import net.hostsharing.hsadminng.test.generated.api.v1.api.TestCustomersApi; import net.hostsharing.hsadminng.test.generated.api.v1.model.TestCustomerResource; import org.springframework.beans.factory.annotation.Autowired; @@ -21,7 +21,7 @@ public class TestCustomerController implements TestCustomersApi { private Context context; @Autowired - private Mapper mapper; + private StandardMapper mapper; @Autowired private TestCustomerRepository testCustomerRepository; diff --git a/src/main/java/net/hostsharing/hsadminng/rbac/test/pac/TestPackageController.java b/src/main/java/net/hostsharing/hsadminng/rbac/test/pac/TestPackageController.java index c6ecc7e0..d503bf58 100644 --- a/src/main/java/net/hostsharing/hsadminng/rbac/test/pac/TestPackageController.java +++ b/src/main/java/net/hostsharing/hsadminng/rbac/test/pac/TestPackageController.java @@ -1,6 +1,6 @@ package net.hostsharing.hsadminng.rbac.test.pac; -import net.hostsharing.hsadminng.mapper.Mapper; +import net.hostsharing.hsadminng.mapper.StandardMapper; import net.hostsharing.hsadminng.mapper.OptionalFromJson; import net.hostsharing.hsadminng.context.Context; import net.hostsharing.hsadminng.test.generated.api.v1.api.TestPackagesApi; @@ -21,7 +21,7 @@ public class TestPackageController implements TestPackagesApi { private Context context; @Autowired - private Mapper mapper; + private StandardMapper mapper; @Autowired private TestPackageRepository testPackageRepository; 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 e28f4d38..11ecf5bc 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,17 +1,20 @@ package net.hostsharing.hsadminng.hs.booking.item; +import net.hostsharing.hsadminng.config.JsonObjectMapperConfiguration; import net.hostsharing.hsadminng.context.Context; import net.hostsharing.hsadminng.hs.booking.project.HsBookingProjectRealEntity; import net.hostsharing.hsadminng.hs.booking.project.HsBookingProjectRealRepository; -import net.hostsharing.hsadminng.mapper.Mapper; +import net.hostsharing.hsadminng.mapper.StrictMapper; +import net.hostsharing.hsadminng.persistence.EntityManagerWrapper; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.junit.runner.RunWith; -import org.mockito.Mock; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.boot.test.context.TestConfiguration; import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Import; import org.springframework.http.MediaType; import org.springframework.test.context.junit4.SpringRunner; @@ -28,13 +31,14 @@ import java.util.UUID; import static net.hostsharing.hsadminng.rbac.test.JsonMatcher.lenientlyEquals; import static org.hamcrest.Matchers.matchesRegex; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @WebMvcTest(HsBookingItemController.class) -@Import(Mapper.class) +@Import({StrictMapper.class, JsonObjectMapperConfiguration.class}) @RunWith(SpringRunner.class) class HsBookingItemControllerRestTest { @@ -44,8 +48,12 @@ class HsBookingItemControllerRestTest { @MockBean Context contextMock; - @Mock - EntityManager em; + @Autowired + @SuppressWarnings("unused") // not used in test, but in controller class + StrictMapper mapper; + + @MockBean + EntityManagerWrapper em; @MockBean EntityManagerFactory emf; @@ -56,6 +64,16 @@ class HsBookingItemControllerRestTest { @MockBean HsBookingItemRbacRepository rbacBookingItemRepo; + @TestConfiguration + public static class TestConfig { + + @Bean + public EntityManager entityManager() { + return mock(EntityManager.class); + } + + } + @BeforeEach void init() { when(emf.createEntityManager()).thenReturn(em); diff --git a/src/test/java/net/hostsharing/hsadminng/hs/booking/item/validators/HsDomainSetupBookingItemValidatorUnitTest.java b/src/test/java/net/hostsharing/hsadminng/hs/booking/item/validators/HsDomainSetupBookingItemValidatorUnitTest.java index 60356401..52a63509 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/booking/item/validators/HsDomainSetupBookingItemValidatorUnitTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/booking/item/validators/HsDomainSetupBookingItemValidatorUnitTest.java @@ -35,7 +35,8 @@ class HsDomainSetupBookingItemValidatorUnitTest { .project(project) .caption("Test-Domain") .resources(Map.ofEntries( - entry("domainName", "example.org") + entry("domainName", "example.org"), + entry("targetUnixUser", "xyz00") )) .build(); @@ -55,6 +56,7 @@ class HsDomainSetupBookingItemValidatorUnitTest { .caption("Test-Domain") .resources(Map.ofEntries( entry("domainName", "example.org"), + entry("targetUnixUser", "xyz00"), entry("verificationCode", "1234-5678-9100") )) .build(); @@ -73,7 +75,8 @@ class HsDomainSetupBookingItemValidatorUnitTest { .project(project) .caption("Test-Domain") .resources(Map.ofEntries( - entry("domainName", right(TOO_LONG_DOMAIN_NAME, 253)) + entry("domainName", right(TOO_LONG_DOMAIN_NAME, 253)), + entry("targetUnixUser", "xyz00") )) .build(); @@ -91,7 +94,8 @@ class HsDomainSetupBookingItemValidatorUnitTest { .project(project) .caption("Test-Domain") .resources(Map.ofEntries( - entry("domainName", right(TOO_LONG_DOMAIN_NAME, 254)) + entry("domainName", right(TOO_LONG_DOMAIN_NAME, 254)), + entry("targetUnixUser", "xyz00") )) .build(); @@ -102,6 +106,44 @@ class HsDomainSetupBookingItemValidatorUnitTest { assertThat(result).contains("'D-12345:Test-Project:Test-Domain.resources.domainName' length is expected to be at max 253 but length of 'dfghijklmnopqrstuvwxyz0123456789.asdfghijklmnopqrstuvwxyz0123456789.asdfghijklmnopqrstuvwxyz0123456789.asdfghijklmnopqrstuvwxyz0123456789.asdfghijklmnopqrstuvwxyz0123456789.asdfghijklmnopqrstuvwxyz0123456789.asdfghijklmnopqrstuvwxyz0123456789.example.org' is 254"); } + @Test + void acceptsValidUnixUser() { + final var domainSetupBookingItemEntity = HsBookingItemRealEntity.builder() + .type(DOMAIN_SETUP) + .project(project) + .caption("Test-Domain") + .resources(Map.ofEntries( + entry("domainName", "example.com"), + entry("targetUnixUser", "xyz00-test") + )) + .build(); + + // when + final var result = HsBookingItemEntityValidatorRegistry.doValidate(em, domainSetupBookingItemEntity); + + // then + assertThat(result).isEmpty(); + } + + @Test + void rejectsInvalidUnixUser() { + final var domainSetupBookingItemEntity = HsBookingItemRealEntity.builder() + .type(DOMAIN_SETUP) + .project(project) + .caption("Test-Domain") + .resources(Map.ofEntries( + entry("domainName", "example.com"), + entry("targetUnixUser", "xyz00test") + )) + .build(); + + // when + final var result = HsBookingItemEntityValidatorRegistry.doValidate(em, domainSetupBookingItemEntity); + + // then + assertThat(result).contains("'D-12345:Test-Project:Test-Domain.resources.targetUnixUser' = 'xyz00test' is not a valid unix-user name"); + } + @ParameterizedTest @ValueSource(strings = { "de", "com", "net", "org", "actually-any-top-level-domain", @@ -123,7 +165,8 @@ class HsDomainSetupBookingItemValidatorUnitTest { .project(project) .caption("Test-Domain") .resources(Map.ofEntries( - entry("domainName", secondLevelRegistrarDomain) + entry("domainName", secondLevelRegistrarDomain), + entry("targetUnixUser", "xyz00") )) .build(); @@ -148,7 +191,8 @@ class HsDomainSetupBookingItemValidatorUnitTest { .project(project) .caption("Test-Domain") .resources(Map.ofEntries( - entry("domainName", secondLevelRegistrarDomain) + entry("domainName", secondLevelRegistrarDomain), + entry("targetUnixUser", "xyz00") )) .build(); @@ -170,6 +214,7 @@ class HsDomainSetupBookingItemValidatorUnitTest { // then assertThat(validator.properties()).map(Map::toString).containsExactlyInAnyOrder( "{type=string, propertyName=domainName, matchesRegEx=[^((?!-)[A-Za-z0-9-]{1,63}(?(ofEntries( - entry("domainName", domainName) + entry("domainName", domainName), + entry("targetUnixUser", "xyz00") )))); HsBookingItemEntityValidatorRegistry.forType(HsBookingItemType.DOMAIN_SETUP).prepareProperties(null, bookingItem); return HsHostingAssetRbacEntity.builder() diff --git a/src/test/java/net/hostsharing/hsadminng/hs/migration/ImportHostingAssets.java b/src/test/java/net/hostsharing/hsadminng/hs/migration/ImportHostingAssets.java index 9d73ac89..634ba207 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/migration/ImportHostingAssets.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/migration/ImportHostingAssets.java @@ -1680,13 +1680,19 @@ public class ImportHostingAssets extends BaseOfficeDataImport { final var relatedProject = domainSetup.getSubHostingAssets().stream() .map(ha -> ha.getAssignedToAsset() != null ? ha.getAssignedToAsset().getRelatedProject() : null) .findAny().orElseThrow(); + final var targetUnixUser = domainSetup.getSubHostingAssets().stream() + .filter(subAsset -> subAsset.getType() == DOMAIN_HTTP_SETUP) + .map(domainHttpSetup -> domainHttpSetup.getAssignedToAsset().getIdentifier()) + .findAny().orElse(null); final var bookingItem = HsBookingItemRealEntity.builder() .type(HsBookingItemType.DOMAIN_SETUP) .caption("BI " + domainSetup.getIdentifier()) .project((HsBookingProjectRealEntity) relatedProject) //.validity(toPostgresDateRange(created, cancelled)) .resources(Map.ofEntries( - entry("domainName", domainSetup.getIdentifier()))) + entry("domainName", domainSetup.getIdentifier()), + entry("targetUnixUser", targetUnixUser) + )) .build(); domainSetup.setBookingItem(bookingItem); bookingItems.put(nextAvailableBookingItemId(), bookingItem); 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 6dcd1cb5..624e9994 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,7 +1,7 @@ package net.hostsharing.hsadminng.hs.office.bankaccount; import net.hostsharing.hsadminng.context.Context; -import net.hostsharing.hsadminng.mapper.Mapper; +import net.hostsharing.hsadminng.mapper.StandardMapper; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.EnumSource; import org.springframework.beans.factory.annotation.Autowired; @@ -25,7 +25,7 @@ class HsOfficeBankAccountControllerRestTest { Context contextMock; @MockBean - Mapper mapper; + StandardMapper mapper; @MockBean HsOfficeBankAccountRepository bankAccountRepo; 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 0e4716d4..03a1c71b 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,7 +1,7 @@ package net.hostsharing.hsadminng.hs.office.coopassets; import net.hostsharing.hsadminng.context.Context; -import net.hostsharing.hsadminng.mapper.Mapper; +import net.hostsharing.hsadminng.mapper.StandardMapper; import net.hostsharing.hsadminng.rbac.test.JsonBuilder; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.EnumSource; @@ -30,7 +30,7 @@ class HsOfficeCoopAssetsTransactionControllerRestTest { Context contextMock; @MockBean - Mapper mapper; + StandardMapper mapper; @MockBean HsOfficeCoopAssetsTransactionRepository coopAssetsTransactionRepo; 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 4d44c0fb..1e33fde9 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,7 +1,7 @@ package net.hostsharing.hsadminng.hs.office.coopshares; import net.hostsharing.hsadminng.context.Context; -import net.hostsharing.hsadminng.mapper.Mapper; +import net.hostsharing.hsadminng.mapper.StandardMapper; import net.hostsharing.hsadminng.rbac.test.JsonBuilder; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.EnumSource; @@ -30,7 +30,7 @@ class HsOfficeCoopSharesTransactionControllerRestTest { Context contextMock; @MockBean - Mapper mapper; + StandardMapper mapper; @MockBean HsOfficeCoopSharesTransactionRepository coopSharesTransactionRepo; diff --git a/src/test/java/net/hostsharing/hsadminng/hs/office/debitor/HsOfficeDebitorControllerAcceptanceTest.java b/src/test/java/net/hostsharing/hsadminng/hs/office/debitor/HsOfficeDebitorControllerAcceptanceTest.java index 98ba650c..d0954b6a 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/office/debitor/HsOfficeDebitorControllerAcceptanceTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/office/debitor/HsOfficeDebitorControllerAcceptanceTest.java @@ -433,7 +433,7 @@ class HsOfficeDebitorControllerAcceptanceTest extends ContextBasedTestWithCleanu .post("http://localhost/api/hs/office/debitors") .then().log().all().assertThat() .statusCode(400) - .body("message", is("ERROR: [400] Unable to find RealRelation by uuid: 00000000-0000-0000-0000-000000000000")); + .body("message", is("ERROR: [400] Unable to find RealRelation by debitorRelUuid: 00000000-0000-0000-0000-000000000000")); // @formatter:on } } 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 2a5005e6..64de089c 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 @@ -3,7 +3,7 @@ package net.hostsharing.hsadminng.hs.office.membership; import net.hostsharing.hsadminng.context.Context; import net.hostsharing.hsadminng.hs.office.coopassets.HsOfficeCoopAssetsTransactionRepository; import net.hostsharing.hsadminng.hs.office.partner.HsOfficePartnerEntity; -import net.hostsharing.hsadminng.mapper.Mapper; +import net.hostsharing.hsadminng.mapper.StandardMapper; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; @@ -32,7 +32,7 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers. import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @WebMvcTest(HsOfficeMembershipController.class) -@Import(Mapper.class) +@Import(StandardMapper.class) public class HsOfficeMembershipControllerRestTest { @Autowired @@ -115,7 +115,7 @@ public class HsOfficeMembershipControllerRestTest { .andExpect(status().is4xxClientError()) .andExpect(jsonPath("statusCode", is(400))) .andExpect(jsonPath("statusPhrase", is("Bad Request"))) - .andExpect(jsonPath("message", is("ERROR: [400] Unable to find Partner by uuid: " + givenPartnerUuid))); + .andExpect(jsonPath("message", is("ERROR: [400] Unable to find Partner by partner.uuid: " + givenPartnerUuid))); } @ParameterizedTest diff --git a/src/test/java/net/hostsharing/hsadminng/hs/office/membership/HsOfficeMembershipEntityPatcherUnitTest.java b/src/test/java/net/hostsharing/hsadminng/hs/office/membership/HsOfficeMembershipEntityPatcherUnitTest.java index 2e739e7f..841e7e12 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/office/membership/HsOfficeMembershipEntityPatcherUnitTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/office/membership/HsOfficeMembershipEntityPatcherUnitTest.java @@ -4,7 +4,7 @@ import io.hypersistence.utils.hibernate.type.range.Range; import net.hostsharing.hsadminng.hs.office.debitor.HsOfficeDebitorEntity; import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficeMembershipPatchResource; import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficeMembershipStatusResource; -import net.hostsharing.hsadminng.mapper.Mapper; +import net.hostsharing.hsadminng.mapper.StandardMapper; import net.hostsharing.hsadminng.rbac.test.PatchUnitTestBase; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.TestInstance; @@ -40,7 +40,7 @@ class HsOfficeMembershipEntityPatcherUnitTest extends PatchUnitTestBase< @Mock private EntityManager em; - private Mapper mapper = new Mapper(); + private StandardMapper mapper = new StandardMapper(); @BeforeEach void initMocks() { 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 a42a4780..2af222dc 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 @@ -5,7 +5,7 @@ import net.hostsharing.hsadminng.hs.office.contact.HsOfficeContactRbacEntity; import net.hostsharing.hsadminng.hs.office.person.HsOfficePersonEntity; import net.hostsharing.hsadminng.hs.office.relation.HsOfficeRelationRealEntity; import net.hostsharing.hsadminng.hs.office.relation.HsOfficeRelationRealRepository; -import net.hostsharing.hsadminng.mapper.Mapper; +import net.hostsharing.hsadminng.mapper.StandardMapper; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; @@ -36,7 +36,7 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers. import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @WebMvcTest(HsOfficePartnerController.class) -@Import(Mapper.class) +@Import(StandardMapper.class) class HsOfficePartnerControllerRestTest { static final UUID GIVEN_MANDANTE_UUID = UUID.randomUUID(); diff --git a/src/test/java/net/hostsharing/hsadminng/hs/office/sepamandate/HsOfficeSepaMandateControllerAcceptanceTest.java b/src/test/java/net/hostsharing/hsadminng/hs/office/sepamandate/HsOfficeSepaMandateControllerAcceptanceTest.java index 026ce95c..89b25f35 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/office/sepamandate/HsOfficeSepaMandateControllerAcceptanceTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/office/sepamandate/HsOfficeSepaMandateControllerAcceptanceTest.java @@ -195,7 +195,7 @@ class HsOfficeSepaMandateControllerAcceptanceTest extends ContextBasedTestWithCl .post("http://localhost/api/hs/office/sepamandates") .then().log().all().assertThat() .statusCode(400) - .body("message", is("ERROR: [400] Unable to find BankAccount by uuid: 00000000-0000-0000-0000-000000000000")); + .body("message", is("ERROR: [400] Unable to find BankAccount with uuid 00000000-0000-0000-0000-000000000000")); // @formatter:on } @@ -225,7 +225,7 @@ class HsOfficeSepaMandateControllerAcceptanceTest extends ContextBasedTestWithCl .post("http://localhost/api/hs/office/sepamandates") .then().log().all().assertThat() .statusCode(400) - .body("message", is("ERROR: [400] Unable to find Debitor by uuid: 00000000-0000-0000-0000-000000000000")); + .body("message", is("ERROR: [400] Unable to find Debitor with uuid 00000000-0000-0000-0000-000000000000")); // @formatter:on } } diff --git a/src/test/java/net/hostsharing/hsadminng/rbac/context/ContextIntegrationTests.java b/src/test/java/net/hostsharing/hsadminng/rbac/context/ContextIntegrationTests.java index c48f782e..cf5f387e 100644 --- a/src/test/java/net/hostsharing/hsadminng/rbac/context/ContextIntegrationTests.java +++ b/src/test/java/net/hostsharing/hsadminng/rbac/context/ContextIntegrationTests.java @@ -1,7 +1,7 @@ package net.hostsharing.hsadminng.rbac.context; import net.hostsharing.hsadminng.context.Context; -import net.hostsharing.hsadminng.mapper.Mapper; +import net.hostsharing.hsadminng.mapper.StandardMapper; import net.hostsharing.hsadminng.mapper.Array; import net.hostsharing.hsadminng.rbac.test.JpaAttempt; import org.junit.jupiter.api.Test; @@ -17,7 +17,7 @@ import jakarta.servlet.http.HttpServletRequest; import static org.assertj.core.api.Assertions.assertThat; @DataJpaTest -@ComponentScan(basePackageClasses = { Context.class, JpaAttempt.class, Mapper.class }) +@ComponentScan(basePackageClasses = { Context.class, JpaAttempt.class, StandardMapper.class }) @DirtiesContext class ContextIntegrationTests { 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 2d3d74c7..7d38b0e9 100644 --- a/src/test/java/net/hostsharing/hsadminng/rbac/role/RbacRoleControllerRestTest.java +++ b/src/test/java/net/hostsharing/hsadminng/rbac/role/RbacRoleControllerRestTest.java @@ -1,7 +1,7 @@ package net.hostsharing.hsadminng.rbac.role; import net.hostsharing.hsadminng.context.Context; -import net.hostsharing.hsadminng.mapper.Mapper; +import net.hostsharing.hsadminng.mapper.StandardMapper; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.runner.RunWith; @@ -30,7 +30,7 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers. import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @WebMvcTest(RbacRoleController.class) -@Import(Mapper.class) +@Import(StandardMapper.class) @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 d23a8394..2131c7d9 100644 --- a/src/test/java/net/hostsharing/hsadminng/rbac/subject/RbacSubjectControllerRestTest.java +++ b/src/test/java/net/hostsharing/hsadminng/rbac/subject/RbacSubjectControllerRestTest.java @@ -1,7 +1,7 @@ package net.hostsharing.hsadminng.rbac.subject; import net.hostsharing.hsadminng.context.Context; -import net.hostsharing.hsadminng.mapper.Mapper; +import net.hostsharing.hsadminng.mapper.StandardMapper; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.runner.RunWith; @@ -31,7 +31,7 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers. import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @WebMvcTest(RbacSubjectController.class) -@Import(Mapper.class) +@Import(StandardMapper.class) @RunWith(SpringRunner.class) class RbacSubjectControllerRestTest { diff --git a/src/test/java/net/hostsharing/hsadminng/rbac/test/MapperUnitTest.java b/src/test/java/net/hostsharing/hsadminng/rbac/test/MapperUnitTest.java index 0e01cd05..b90c7cb1 100644 --- a/src/test/java/net/hostsharing/hsadminng/rbac/test/MapperUnitTest.java +++ b/src/test/java/net/hostsharing/hsadminng/rbac/test/MapperUnitTest.java @@ -2,7 +2,7 @@ package net.hostsharing.hsadminng.rbac.test; import lombok.*; import net.hostsharing.hsadminng.errors.DisplayAs; -import net.hostsharing.hsadminng.mapper.Mapper; +import net.hostsharing.hsadminng.mapper.StandardMapper; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.InjectMocks; @@ -27,7 +27,7 @@ class MapperUnitTest { EntityManager em; @InjectMocks - Mapper mapper; + StandardMapper mapper; final UUID GIVEN_UUID = UUID.randomUUID(); @@ -50,7 +50,7 @@ class MapperUnitTest { @Test void mapsBeanWithExistingSubEntity() { final SourceBean givenSource = SourceBean.builder().a("1234").b("Text").s1(new SubSourceBean1(GIVEN_UUID)).build(); - when(em.find(SubTargetBean1.class, GIVEN_UUID)).thenReturn(new SubTargetBean1(GIVEN_UUID, "xxx")); + when(em.getReference(SubTargetBean1.class, GIVEN_UUID)).thenReturn(new SubTargetBean1(GIVEN_UUID, "xxx")); final var result = mapper.map(givenSource, TargetBean.class); assertThat(result).usingRecursiveComparison().isEqualTo( @@ -81,27 +81,27 @@ class MapperUnitTest { @Test void mapsBeanWithSubEntityNotFound() { final SourceBean givenSource = SourceBean.builder().a("1234").b("Text").s1(new SubSourceBean1(GIVEN_UUID)).build(); - when(em.find(SubTargetBean1.class, GIVEN_UUID)).thenReturn(null); + when(em.getReference(SubTargetBean1.class, GIVEN_UUID)).thenReturn(null); final var exception = catchThrowable(() -> mapper.map(givenSource, TargetBean.class) ); assertThat(exception).isInstanceOf(ValidationException.class) - .hasMessage("Unable to find SubTargetBean1 by uuid: " + GIVEN_UUID); + .hasMessage("Unable to find SubTargetBean1 by s1.uuid: " + GIVEN_UUID); } @Test void mapsBeanWithSubEntityNotFoundAndDisplayName() { final SourceBean givenSource = SourceBean.builder().a("1234").b("Text").s2(new SubSourceBean2(GIVEN_UUID)).build(); - when(em.find(SubTargetBean2.class, GIVEN_UUID)).thenReturn(null); + when(em.getReference(SubTargetBean2.class, GIVEN_UUID)).thenReturn(null); final var exception = catchThrowable(() -> mapper.map(givenSource, TargetBean.class) ); assertThat(exception).isInstanceOf(ValidationException.class) - .hasMessage("Unable to find SomeDisplayName by uuid: " + GIVEN_UUID); + .hasMessage("Unable to find SomeDisplayName by s2.uuid: " + GIVEN_UUID); } @Test