refactoring for implicit creation of dependend hosting-assets (#108)
Co-authored-by: Michael Hoennig <michael@hoennig.de> Reviewed-on: https://dev.hostsharing.net/hostsharing/hs.hsadmin.ng/pulls/108 Reviewed-by: Timotheus Pokorra <timotheus.pokorra@hostsharing.net>
This commit is contained in:
		
							
								
								
									
										27
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										27
									
								
								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? | ||||
|   | ||||
| @@ -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 ""; | ||||
|   | ||||
| @@ -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<HsBookingItemRbacEntity, HsBookingItemResource> ENTITY_TO_RESOURCE_POSTMAPPER = (entity, resource) -> { | ||||
|     final BiConsumer<HsBookingItem, HsBookingItemResource> ITEM_TO_RESOURCE_POSTMAPPER = (entity, resource) -> { | ||||
|         resource.setValidFrom(entity.getValidity().lower()); | ||||
|         if (entity.getValidity().hasUpperBound()) { | ||||
|             resource.setValidTo(entity.getValidity().upper().minusDays(1)); | ||||
|         } | ||||
|     }; | ||||
|  | ||||
|     final BiConsumer<HsBookingItemRbacEntity, HsBookingItemResource> RBAC_ENTITY_TO_RESOURCE_POSTMAPPER = ITEM_TO_RESOURCE_POSTMAPPER::accept; | ||||
|  | ||||
|     final BiConsumer<HsBookingItemInsertResource, HsBookingItemRbacEntity> 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())); | ||||
|     }; | ||||
|   | ||||
| @@ -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<HsBookingItem> 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`. | ||||
|      * | ||||
|      * <p>`validator.postPersist(em, entity)` is NOT called. | ||||
|      * If any postprocessing is necessary, the saveFunction has to implement this.</p> | ||||
|      * @param saveFunction | ||||
|      * @return this | ||||
|      */ | ||||
|     public BookingItemEntitySaveProcessor saveUsing(final Function<HsBookingItem, HsBookingItem> saveFunction) { | ||||
|         step("save", "validateContext"); | ||||
|         entity = saveFunction.apply(entity); | ||||
|         return this; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Saves the using the `EntityManager`, but does NOT ever merge the entity. | ||||
|      * | ||||
|      * <p>`validator.postPersist(em, entity)` is called afterwards with the entity guaranteed to be flushed to the database.</p> | ||||
|      * @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<HsBookingItem, HsBookingItemResource> 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<String, Object>) 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; | ||||
|     } | ||||
| } | ||||
| @@ -48,10 +48,11 @@ public class HsBookingItemEntityValidatorRegistry { | ||||
|     } | ||||
|  | ||||
|     public static List<String> 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)) | ||||
|         ); | ||||
|     } | ||||
|  | ||||
|   | ||||
| @@ -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}(?<!-)\\.)+[A-Za-z]{2,12}"; | ||||
|     public static final String DOMAIN_NAME_PROPERTY_NAME = "domainName"; | ||||
|     public static final String TARGET_UNIX_USER_PROPERTY_NAME = "targetUnixUser"; | ||||
|     public static final String WEBSPACE_NAME_REGEX = "[a-z][a-z0-9]{2}[0-9]{2}"; | ||||
|     public static final String TARGET_UNIX_USER_NAME_REGEX = "^"+WEBSPACE_NAME_REGEX+"$|^"+WEBSPACE_NAME_REGEX+"-[a-z0-9\\._-]+$"; | ||||
|     public static final String VERIFICATION_CODE_PROPERTY_NAME = "verificationCode"; | ||||
|  | ||||
|     HsDomainSetupBookingItemValidator() { | ||||
| @@ -24,6 +28,12 @@ class HsDomainSetupBookingItemValidator extends HsBookingItemEntityValidator { | ||||
|                         .matchesRegEx(FQDN_REGEX).describedAs("is not a (non-top-level) fully qualified domain name") | ||||
|                         .notMatchesRegEx(REGISTRAR_LEVEL_DOMAINS).describedAs("is a forbidden registrar-level domain name") | ||||
|                         .required(), | ||||
|                 // TODO.legacy: remove the following property once we give up legacy compatibility | ||||
|                 stringProperty(TARGET_UNIX_USER_PROPERTY_NAME).writeOnce() | ||||
|                         .maxLength(253) | ||||
|                         .matchesRegEx(TARGET_UNIX_USER_NAME_REGEX).describedAs("is not a valid unix-user name") | ||||
|                         .writeOnce() | ||||
|                         .required(), | ||||
|                 stringProperty(VERIFICATION_CODE_PROPERTY_NAME) | ||||
|                         .minLength(12) | ||||
|                         .maxLength(64) | ||||
|   | ||||
| @@ -6,7 +6,7 @@ import net.hostsharing.hsadminng.hs.booking.generated.api.v1.api.HsBookingProjec | ||||
| import net.hostsharing.hsadminng.hs.booking.generated.api.v1.model.HsBookingProjectInsertResource; | ||||
| import net.hostsharing.hsadminng.hs.booking.generated.api.v1.model.HsBookingProjectPatchResource; | ||||
| import net.hostsharing.hsadminng.hs.booking.generated.api.v1.model.HsBookingProjectResource; | ||||
| 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; | ||||
| @@ -25,7 +25,7 @@ public class HsBookingProjectController implements HsBookingProjectsApi { | ||||
|     private Context context; | ||||
|  | ||||
|     @Autowired | ||||
|     private Mapper mapper; | ||||
|     private StandardMapper mapper; | ||||
|  | ||||
|     @Autowired | ||||
|     private HsBookingProjectRbacRepository bookingProjectRepo; | ||||
|   | ||||
| @@ -11,7 +11,7 @@ import net.hostsharing.hsadminng.hs.hosting.generated.api.v1.model.HsHostingAsse | ||||
| import net.hostsharing.hsadminng.hs.hosting.generated.api.v1.model.HsHostingAssetResource; | ||||
| import net.hostsharing.hsadminng.hs.hosting.generated.api.v1.model.HsHostingAssetTypeResource; | ||||
| import net.hostsharing.hsadminng.mapper.KeyValueMap; | ||||
| import net.hostsharing.hsadminng.mapper.Mapper; | ||||
| import net.hostsharing.hsadminng.mapper.StandardMapper; | ||||
| import net.hostsharing.hsadminng.persistence.EntityManagerWrapper; | ||||
| import org.springframework.beans.factory.annotation.Autowired; | ||||
| import org.springframework.http.ResponseEntity; | ||||
| @@ -35,7 +35,7 @@ public class HsHostingAssetController implements HsHostingAssetsApi { | ||||
|     private Context context; | ||||
|  | ||||
|     @Autowired | ||||
|     private Mapper mapper; | ||||
|     private StandardMapper mapper; | ||||
|  | ||||
|     @Autowired | ||||
|     private HsHostingAssetRbacRepository rbacAssetRepo; | ||||
|   | ||||
| @@ -4,7 +4,7 @@ import net.hostsharing.hsadminng.context.Context; | ||||
| import net.hostsharing.hsadminng.hs.office.generated.api.v1.api.HsOfficeBankAccountsApi; | ||||
| import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficeBankAccountInsertResource; | ||||
| import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficeBankAccountResource; | ||||
| import net.hostsharing.hsadminng.mapper.Mapper; | ||||
| import net.hostsharing.hsadminng.mapper.StandardMapper; | ||||
| import org.iban4j.BicUtil; | ||||
| import org.iban4j.IbanUtil; | ||||
| import org.springframework.beans.factory.annotation.Autowired; | ||||
| @@ -24,7 +24,7 @@ public class HsOfficeBankAccountController implements HsOfficeBankAccountsApi { | ||||
|     private Context context; | ||||
|  | ||||
|     @Autowired | ||||
|     private Mapper mapper; | ||||
|     private StandardMapper mapper; | ||||
|  | ||||
|     @Autowired | ||||
|     private HsOfficeBankAccountRepository bankAccountRepo; | ||||
|   | ||||
| @@ -1,6 +1,6 @@ | ||||
| package net.hostsharing.hsadminng.hs.office.contact; | ||||
|  | ||||
| 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.HsOfficeContactsApi; | ||||
| import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficeContactInsertResource; | ||||
| @@ -26,7 +26,7 @@ public class HsOfficeContactController implements HsOfficeContactsApi { | ||||
|     private Context context; | ||||
|  | ||||
|     @Autowired | ||||
|     private Mapper mapper; | ||||
|     private StandardMapper mapper; | ||||
|  | ||||
|     @Autowired | ||||
|     private HsOfficeContactRbacRepository contactRepo; | ||||
|   | ||||
| @@ -4,7 +4,7 @@ import net.hostsharing.hsadminng.context.Context; | ||||
| import net.hostsharing.hsadminng.hs.office.generated.api.v1.api.HsOfficeCoopAssetsApi; | ||||
| import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.*; | ||||
| import net.hostsharing.hsadminng.errors.MultiValidationException; | ||||
| import net.hostsharing.hsadminng.mapper.Mapper; | ||||
| import net.hostsharing.hsadminng.mapper.StandardMapper; | ||||
| import org.springframework.beans.factory.annotation.Autowired; | ||||
| import org.springframework.format.annotation.DateTimeFormat; | ||||
| import org.springframework.format.annotation.DateTimeFormat.ISO; | ||||
| @@ -29,7 +29,7 @@ public class HsOfficeCoopAssetsTransactionController implements HsOfficeCoopAsse | ||||
|     private Context context; | ||||
|  | ||||
|     @Autowired | ||||
|     private Mapper mapper; | ||||
|     private StandardMapper mapper; | ||||
|  | ||||
|     @Autowired | ||||
|     private HsOfficeCoopAssetsTransactionRepository coopAssetsTransactionRepo; | ||||
|   | ||||
| @@ -6,7 +6,7 @@ import net.hostsharing.hsadminng.hs.office.generated.api.v1.api.HsOfficeCoopShar | ||||
| import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficeCoopSharesTransactionInsertResource; | ||||
| import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficeCoopSharesTransactionResource; | ||||
| import net.hostsharing.hsadminng.errors.MultiValidationException; | ||||
| import net.hostsharing.hsadminng.mapper.Mapper; | ||||
| import net.hostsharing.hsadminng.mapper.StandardMapper; | ||||
| import org.springframework.beans.factory.annotation.Autowired; | ||||
| import org.springframework.format.annotation.DateTimeFormat; | ||||
| import org.springframework.format.annotation.DateTimeFormat.ISO; | ||||
| @@ -31,7 +31,7 @@ public class HsOfficeCoopSharesTransactionController implements HsOfficeCoopShar | ||||
|     private Context context; | ||||
|  | ||||
|     @Autowired | ||||
|     private Mapper mapper; | ||||
|     private StandardMapper mapper; | ||||
|  | ||||
|     @Autowired | ||||
|     private HsOfficeCoopSharesTransactionRepository coopSharesTransactionRepo; | ||||
|   | ||||
| @@ -7,8 +7,8 @@ import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficeDebito | ||||
| import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficeDebitorResource; | ||||
| 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.rbac.object.BaseEntity; | ||||
| import net.hostsharing.hsadminng.mapper.StandardMapper; | ||||
| import net.hostsharing.hsadminng.persistence.EntityExistsValidator; | ||||
| import org.apache.commons.lang3.Validate; | ||||
| import org.hibernate.Hibernate; | ||||
| import org.springframework.beans.factory.annotation.Autowired; | ||||
| @@ -23,7 +23,6 @@ import jakarta.validation.ValidationException; | ||||
| import java.util.List; | ||||
| import java.util.UUID; | ||||
|  | ||||
| import static net.hostsharing.hsadminng.errors.DisplayAs.DisplayName; | ||||
| import static net.hostsharing.hsadminng.hs.office.relation.HsOfficeRelationType.DEBITOR; | ||||
|  | ||||
| @RestController | ||||
| @@ -34,7 +33,7 @@ public class HsOfficeDebitorController implements HsOfficeDebitorsApi { | ||||
|     private Context context; | ||||
|  | ||||
|     @Autowired | ||||
|     private Mapper mapper; | ||||
|     private StandardMapper mapper; | ||||
|  | ||||
|     @Autowired | ||||
|     private HsOfficeDebitorRepository debitorRepo; | ||||
| @@ -42,6 +41,9 @@ public class HsOfficeDebitorController implements HsOfficeDebitorsApi { | ||||
|     @Autowired | ||||
|     private HsOfficeRelationRealRepository relrealRepo; | ||||
|  | ||||
|     @Autowired | ||||
|     private EntityExistsValidator entityValidator; | ||||
|  | ||||
|     @PersistenceContext | ||||
|     private EntityManager em; | ||||
|  | ||||
| @@ -84,10 +86,10 @@ public class HsOfficeDebitorController implements HsOfficeDebitorsApi { | ||||
|         final var entityToSave = mapper.map(body, HsOfficeDebitorEntity.class); | ||||
|         if ( body.getDebitorRel() != null ) { | ||||
|             body.getDebitorRel().setType(DEBITOR.name()); | ||||
|             final var debitorRel = mapper.map(body.getDebitorRel(), HsOfficeRelationRealEntity.class); | ||||
|             validateEntityExists("debitorRel.anchorUuid", debitorRel.getAnchor()); | ||||
|             validateEntityExists("debitorRel.holderUuid", debitorRel.getHolder()); | ||||
|             validateEntityExists("debitorRel.contactUuid", debitorRel.getContact()); | ||||
|             final var debitorRel = mapper.map("debitorRel.", body.getDebitorRel(), HsOfficeRelationRealEntity.class); | ||||
|             entityValidator.validateEntityExists("debitorRel.anchorUuid", debitorRel.getAnchor()); | ||||
|             entityValidator.validateEntityExists("debitorRel.holderUuid", debitorRel.getHolder()); | ||||
|             entityValidator.validateEntityExists("debitorRel.contactUuid", debitorRel.getContact()); | ||||
|             entityToSave.setDebitorRel(relrealRepo.save(debitorRel)); | ||||
|         } else { | ||||
|             final var debitorRelOptional = relrealRepo.findByUuid(body.getDebitorRelUuid()); | ||||
| @@ -160,15 +162,4 @@ public class HsOfficeDebitorController implements HsOfficeDebitorsApi { | ||||
|         final var mapped = mapper.map(saved, HsOfficeDebitorResource.class); | ||||
|         return ResponseEntity.ok(mapped); | ||||
|     } | ||||
|  | ||||
|     // TODO.impl: extract this to some generally usable class? | ||||
|     private <T extends BaseEntity<T>> 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; | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -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; | ||||
|   | ||||
| @@ -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<HsOfficeMembershipPatchResource> { | ||||
|  | ||||
|     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; | ||||
|   | ||||
| @@ -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; | ||||
|   | ||||
| @@ -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; | ||||
|   | ||||
| @@ -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; | ||||
|   | ||||
| @@ -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; | ||||
|   | ||||
| @@ -130,7 +130,7 @@ public abstract class HsEntityValidator<E extends PropertiesProvider> { | ||||
|     } | ||||
|  | ||||
|     public Map<String, Object> revampProperties(final EntityManager em, final E entity, final Map<String, Object> 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); | ||||
|   | ||||
| @@ -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> D map(final Object source, final Class<D> destinationType) { | ||||
|         return map("", source, destinationType); | ||||
|     } | ||||
|  | ||||
|     public <D> D map(final String namePrefix, final Object source, final Class<D> 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 <D> Field[] getDeclaredFieldsIncludingSuperClasses(final Class<D> destinationType) { | ||||
|         if (destinationType == null) { | ||||
|             return new Field[0]; | ||||
|         } | ||||
|  | ||||
|         return Stream.concat( | ||||
|                 stream(destinationType.getDeclaredFields()), | ||||
|                 stream(getDeclaredFieldsIncludingSuperClasses(destinationType.getSuperclass()))) | ||||
|             .toArray(Field[]::new); | ||||
|     } | ||||
|  | ||||
|     public <E> E fetchEntity(final String propertyName, final Class<E> 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 <S, T> T map(final S source, final Class<T> targetClass, final BiConsumer<S, T> postMapper) { | ||||
| @@ -86,4 +104,13 @@ public class Mapper extends ModelMapper { | ||||
|         postMapper.accept(source, target); | ||||
|         return target; | ||||
|     } | ||||
|  | ||||
|     public <S, T> T map(final String namePrefix, final S source, final Class<T> targetClass, final BiConsumer<S, T> postMapper) { | ||||
|         if (source == null) { | ||||
|             return null; | ||||
|         } | ||||
|         final var target = map(source, targetClass); | ||||
|         postMapper.accept(source, target); | ||||
|         return target; | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -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(); | ||||
|     } | ||||
| } | ||||
| @@ -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); | ||||
|     } | ||||
| } | ||||
| @@ -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. | ||||
|  * | ||||
|  * <p>This makes sure that resource.whateverUuid does not accidentally get mapped to entity.uuid, | ||||
|  * if resource.uuid does not exist.</p> | ||||
|  */ | ||||
| @Component | ||||
| public class StrictMapper extends Mapper { | ||||
|  | ||||
|     public StrictMapper() { | ||||
|         getConfiguration().setMatchingStrategy(STRICT); | ||||
|     } | ||||
| } | ||||
| @@ -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 <T extends BaseEntity<T>> 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 <T extends BaseEntity<T>> 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()); | ||||
|     } | ||||
| } | ||||
| @@ -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; | ||||
|   | ||||
| @@ -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; | ||||
|   | ||||
| @@ -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; | ||||
|   | ||||
| @@ -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; | ||||
|   | ||||
| @@ -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; | ||||
|   | ||||
| @@ -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); | ||||
|   | ||||
| @@ -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}(?<!-)\\.)+[A-Za-z]{2,12}], matchesRegExDescription=is not a (non-top-level) fully qualified domain name, notMatchesRegEx=[[^.]+, (co|org|gov|ac|sch)\\.uk, (com|net|org|edu|gov|asn|id)\\.au, (co|ne|or|ac|go)\\.jp, (com|net|org|gov|edu|ac)\\.cn, (com|net|org|gov|edu|mil|art)\\.br, (co|net|org|gen|firm|ind)\\.in, (com|net|org|gob|edu)\\.mx, (gov|edu)\\.it, (co|net|org|govt|ac|school|geek|kiwi)\\.nz, (co|ne|or|go|re|pe)\\.kr], notMatchesRegExDescription=is a forbidden registrar-level domain name, maxLength=253, required=true, writeOnce=true}", | ||||
|                 "{type=string, propertyName=targetUnixUser, matchesRegEx=[^[a-z][a-z0-9]{2}[0-9]{2}$|^[a-z][a-z0-9]{2}[0-9]{2}-[a-z0-9\\._-]+$], matchesRegExDescription=is not a valid unix-user name, maxLength=253, required=true, writeOnce=true}", | ||||
|                 "{type=string, propertyName=verificationCode, minLength=12, maxLength=64, computed=IN_INIT}"); | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -9,7 +9,7 @@ import net.hostsharing.hsadminng.config.JsonObjectMapperConfiguration; | ||||
| import net.hostsharing.hsadminng.context.Context; | ||||
| import net.hostsharing.hsadminng.hs.booking.item.HsBookingItemRealRepository; | ||||
| import net.hostsharing.hsadminng.mapper.Array; | ||||
| import net.hostsharing.hsadminng.mapper.Mapper; | ||||
| import net.hostsharing.hsadminng.mapper.StandardMapper; | ||||
| import net.hostsharing.hsadminng.persistence.EntityManagerWrapper; | ||||
| import org.junit.jupiter.api.BeforeEach; | ||||
| import org.junit.jupiter.api.Test; | ||||
| @@ -52,7 +52,7 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers. | ||||
| import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; | ||||
|  | ||||
| @WebMvcTest(HsHostingAssetController.class) | ||||
| @Import({Mapper.class, JsonObjectMapperConfiguration.class}) | ||||
| @Import({ StandardMapper.class, JsonObjectMapperConfiguration.class}) | ||||
| @RunWith(SpringRunner.class) | ||||
| public class HsHostingAssetControllerRestTest { | ||||
|  | ||||
| @@ -63,10 +63,11 @@ public class HsHostingAssetControllerRestTest { | ||||
|     Context contextMock; | ||||
|  | ||||
|     @Autowired | ||||
|     Mapper mapper; | ||||
|     @SuppressWarnings("unused") // not used in test, but in controller class | ||||
|     StandardMapper mapper; | ||||
|  | ||||
|     @MockBean | ||||
|     private EntityManagerWrapper em; | ||||
|     EntityManagerWrapper em; | ||||
|  | ||||
|     @MockBean | ||||
|     EntityManagerFactory emf; | ||||
| @@ -90,6 +91,7 @@ public class HsHostingAssetControllerRestTest { | ||||
|         } | ||||
|  | ||||
|     } | ||||
|  | ||||
|     enum ListTestCases { | ||||
|         CLOUD_SERVER( | ||||
|                 List.of( | ||||
|   | ||||
| @@ -42,7 +42,8 @@ class HsDomainSetupHostingAssetValidatorUnitTest { | ||||
|                         .project(project) | ||||
|                         .type(HsBookingItemType.DOMAIN_SETUP) | ||||
|                         .resources(new HashMap<>(ofEntries( | ||||
|                                 entry("domainName", domainName) | ||||
|                                 entry("domainName", domainName), | ||||
|                                 entry("targetUnixUser", "xyz00") | ||||
|                         )))); | ||||
|         HsBookingItemEntityValidatorRegistry.forType(HsBookingItemType.DOMAIN_SETUP).prepareProperties(null, bookingItem); | ||||
|         return HsHostingAssetRbacEntity.builder() | ||||
|   | ||||
| @@ -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); | ||||
|   | ||||
| @@ -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; | ||||
|   | ||||
| @@ -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; | ||||
|   | ||||
| @@ -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; | ||||
|   | ||||
| @@ -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 | ||||
|         } | ||||
|     } | ||||
|   | ||||
| @@ -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 | ||||
|   | ||||
| @@ -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() { | ||||
|   | ||||
| @@ -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(); | ||||
|   | ||||
| @@ -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 | ||||
|         } | ||||
|     } | ||||
|   | ||||
| @@ -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 { | ||||
|  | ||||
|   | ||||
| @@ -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 { | ||||
|  | ||||
|   | ||||
| @@ -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 { | ||||
|  | ||||
|   | ||||
| @@ -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 | ||||
|   | ||||
		Reference in New Issue
	
	Block a user