From 7e004d3eedad7dda73f03cbe9aae176f698694af Mon Sep 17 00:00:00 2001 From: Michael Hoennig Date: Fri, 27 Sep 2024 13:13:08 +0200 Subject: [PATCH 01/31] add ADR about automatic hosting-asset-creation for new booking-items --- ...ng-asset-creation-for-new-booking-items.md | 119 ++++++++++++++++++ 1 file changed, 119 insertions(+) create mode 100644 doc/adr/2024-09-27-automatic-hosting-asset-creation-for-new-booking-items.md diff --git a/doc/adr/2024-09-27-automatic-hosting-asset-creation-for-new-booking-items.md b/doc/adr/2024-09-27-automatic-hosting-asset-creation-for-new-booking-items.md new file mode 100644 index 00000000..2df9e1c5 --- /dev/null +++ b/doc/adr/2024-09-27-automatic-hosting-asset-creation-for-new-booking-items.md @@ -0,0 +1,119 @@ +# Handling Automatic Creation of Hosting Assets for New Booking Items + +**Status:** +- [x] proposed by (Michael Hönnig) +- [ ] accepted by (Participants) +- [ ] rejected by (Participants) +- [ ] superseded by (superseding ADR) + + +## Context and Problem Statement + +When a customer creates a new booking item (e.g., `MANAGED_WEBSPACE`), the system must automatically create the related hosting asset. +This process can sometimes fail or require additional data from the user, e.g. installing a DNS verification key, or a hostmaster, e.g. the target server to use. + +The challenge is how to handle this automatic creation process while dealing with missing data, asynchronicity and failures while ensuring system consistency and proper user notification. + + +### Technical Background + +The creation of hosting assets can occur synchronously (in simple cases) or asynchronously (when additional steps like manual verification are needed). +For example, a `DOMAIN_SETUP` hosting asset may require DNS verification from the user, and until this is provided, the related domain cannot be fully set up. + +Additionally, not all data needed for creating the hosting asset is stored in the booking item. +It's part of the HTTP request and later stored in the hosting asset, but we also need to store it before the hosting asset can be created asynchronously. + +Current system behavior involves returning HTTP 201 upon booking item creation, but the automatic hosting asset creation might fail due to missing information. +The system needs to manage the creation process in a way that ensures valid hosting assets are created and informs the user of any actions required while still returning a 201 HTTP code, not an error code. + + +## Considered Options + +For storing the data needed for the hosting-asset creation: + +* STORAGE-1: Store temporary asset data in the `BookingItemEntity`, e.g. a JSON column. + And delete the value of that column, once the hosting assets got successfully created. +* STORAGE-2: Create hosting assets immediately, even if invalid, but mark them as "inactive" until completed and fully validated. +* STORAGE-3: Store the asset data in a kind of event- or job-queue, which get deleted once the hosting-asset got successfully created. + +For the user-notification status: + +* STATUS-1: Introduce a status field in the booking-items. +* STATUS-2: Store the status in the event-/job-queue entries. + +### STORAGE-1: Temporary Data Storage in `BookingItemEntity` + +Store asset-related data (e.g., domain name) in a temporary column or JSON field in the `BookingItemEntity` until the hosting assets are successfully created. +Once assets are created, the temporary data is deleted to avoid inconsistencies. + +#### Advantages +- Easy to implement. + +#### Disadvantages +- Needs either a separate map of properties in the booking-item. +- Or, if stored as a JSON field in the booking-item-resources, these are misused. +- Requires additional cleanup logic to remove stale data. + +### STORAGE-2: Inactive Hosting Assets Until Validation + +Create the hosting assets immediately upon booking item creation but mark them as "inactive" until all required information (e.g., verification code) is provided and validation is complete. + +#### Advantages +- Avoids temporary external data storage for the hosting-assets. + +#### Disadvantages +- Validation becomes more complex as some properties need to be validated, others not. + And some properties even need special treatment for new entities, which then becomes vague. +- Inactive assets have to be filtered from operational assets. +- Potential risk of incomplete or inconsistent assets being created, which may require correction. +- Difficult to write tests for all possible combinations of validations. + +### STORAGE-3: Event-Based Approach + +The hosting asset data required for creation us passed to the API and stored in a `BookingItemCreatedEvent`. +If hosting asset creation cannot happen synchronously, the event is stored and processed asynchronously in batches, retrying failed asset creation as needed. + +#### Advantages +- Clean-data-structure (separation of concerns). +- Clear separation between booking item creation and hosting asset creation. +- Only valid assets in the database. +- Can handle complex asynchronous processes (like waiting for external verification) in a clean and structured manner. +- Easier to manage retries and failures in asset creation without complicating the booking item structure. + +#### Disadvantages +- At the Spring controller level, the whole JSON is already converted into Java objects, + but for storing the asset data in the even, we need JSON again. + This could is not just a performance-overhead but could also lead to inconsistencies. + +### STATUS-1: Store hosting-asset-creation-status in the `BookingItemEntity` + +A status field would be added to booking-items to track the creation state of related hosting assets. +The users could check their booking-items for the status of the hosting-asset creation, error messages and further instructions. + +#### Advantages +- Easy to implement. + +#### Disadvantages +- Adds a field to the booking-item which is makes no sense anymore once the related hosting asset is created. + + +### Status-2: Store hosting-asset-creation-status in the `BookingItemCreateEvent` + +A status field would be added to the booking-item-created event and get updated with the latest messages any time we try to create the hosting-asset. + +#### Advantages +- Clean-data-structure (separation of concerns) + +#### Disadvantages +- Accessing the status requires querying the event queue. + + +## Decision Outcome + +**Chosen Option: STORAGE-3 with STATUS-2 (Event-Based Approach with `BookingItemCreatedEvent`)** + +The event-based approach was selected as the best solution for handling automatic hosting asset creation. This option provides a clear separation between booking item creation and hosting asset creation, ensuring that no invalid or incomplete assets are created. The asynchronous nature of the event system allows for retries and external validation steps (such as user-entered verification codes) without disrupting the overall flow. + +By using `BookingItemCreatedEvent` to store the hosting-asset data and the status, +we don't need to misuse other data structures for temporary data +and therefore hava a clean separation of concerns. -- 2.39.5 From 8ee04ecee3589dfbff08a3e57e96da79cac82be6 Mon Sep 17 00:00:00 2001 From: Michael Hoennig Date: Mon, 30 Sep 2024 15:21:02 +0200 Subject: [PATCH 02/31] HsBookingItemControllerAcceptanceTest passing --- .../config/JsonObjectMapperConfiguration.java | 2 +- .../hsadminng/hash/HashGenerator.java | 2 +- .../booking/item/BookingItemCreatedEvent.java | 50 ++++- .../booking/item/HsBookingItemController.java | 12 +- .../BookingItemEntitySaveProcessor.java | 2 +- .../HsDomainSetupBookingItemValidator.java | 26 +-- .../asset/HsBookingItemCreatedListener.java | 171 +++++++++++++++--- .../asset/HsHostingAssetRealRepository.java | 13 ++ .../HostingAssetEntitySaveProcessor.java | 2 +- ...HsDomainDnsSetupHostingAssetValidator.java | 4 +- .../hs/validation/PasswordProperty.java | 2 +- .../hs-booking/hs-booking-item-schemas.yaml | 13 ++ .../hs-hosting/hs-hosting-asset-schemas.yaml | 63 ++++++- .../5016-hs-office-contact-migration.sql | 2 +- .../5046-hs-office-partner-migration.sql | 2 +- .../5076-hs-office-sepamandate-migration.sql | 2 +- .../5116-hs-office-coopshares-migration.sql | 2 +- .../5126-hs-office-coopassets-migration.sql | 2 +- .../7010-hs-hosting-asset.sql | 2 +- .../7016-hs-hosting-asset-migration.sql | 2 +- ...HsBookingItemControllerAcceptanceTest.java | 125 ++++++++----- .../hs/migration/BaseOfficeDataImport.java | 2 +- 22 files changed, 386 insertions(+), 117 deletions(-) diff --git a/src/main/java/net/hostsharing/hsadminng/config/JsonObjectMapperConfiguration.java b/src/main/java/net/hostsharing/hsadminng/config/JsonObjectMapperConfiguration.java index 921d0c34..b7011f7f 100644 --- a/src/main/java/net/hostsharing/hsadminng/config/JsonObjectMapperConfiguration.java +++ b/src/main/java/net/hostsharing/hsadminng/config/JsonObjectMapperConfiguration.java @@ -17,7 +17,7 @@ public class JsonObjectMapperConfiguration { public Jackson2ObjectMapperBuilder customObjectMapper() { return new Jackson2ObjectMapperBuilder() .modules(new JsonNullableModule(), new JavaTimeModule()) - .featuresToEnable(JsonParser.Feature.ALLOW_COMMENTS) + .featuresToEnable(JsonParser.Feature.ALLOW_COMMENTS, JsonParser.Feature.ALLOW_COMMENTS) .featuresToDisable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS); } } diff --git a/src/main/java/net/hostsharing/hsadminng/hash/HashGenerator.java b/src/main/java/net/hostsharing/hsadminng/hash/HashGenerator.java index cd16b697..268375eb 100644 --- a/src/main/java/net/hostsharing/hsadminng/hash/HashGenerator.java +++ b/src/main/java/net/hostsharing/hsadminng/hash/HashGenerator.java @@ -27,7 +27,7 @@ public final class HashGenerator { "abcdefghijklmnopqrstuvwxyz" + "ABCDEFGHIJKLMNOPQRSTUVWXYZ" + "0123456789/."; - private static boolean couldBeHashEnabled; // TODO.impl: remove after legacy data is migrated + private static boolean couldBeHashEnabled; // TODO.legacy: remove after legacy data is migrated public enum Algorithm { LINUX_SHA512(LinuxEtcShadowHashGenerator::hash, "6"), diff --git a/src/main/java/net/hostsharing/hsadminng/hs/booking/item/BookingItemCreatedEvent.java b/src/main/java/net/hostsharing/hsadminng/hs/booking/item/BookingItemCreatedEvent.java index bea6c9ae..1b723353 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/booking/item/BookingItemCreatedEvent.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/booking/item/BookingItemCreatedEvent.java @@ -4,13 +4,57 @@ import lombok.Getter; import org.springframework.context.ApplicationEvent; import jakarta.validation.constraints.NotNull; +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; @Getter public class BookingItemCreatedEvent extends ApplicationEvent { - private final @NotNull HsBookingItem newBookingItem; - public BookingItemCreatedEvent(@NotNull HsBookingItemController source, @NotNull final HsBookingItem newBookingItem) { + private static final Map events = new HashMap<>(); // FIXME: use DB table + + static BookingItemCreatedEvent of(final HsBookingItemRealEntity bookingItem) { + return events.get(bookingItem.getUuid()); + } + + @Getter + public static class Status { + + private final String message; + + private Status(final String message) { + this.message = message; + } + + public static Status finished() { + return new Status(null); + } + + public static Status failed(final String errorMessage) { + return new Status(errorMessage); + } + + boolean isFinished() { + return message == null; + } + } + + private final @NotNull UUID bookingItemUuid; + private final @NotNull String assetJson; + + private Status status; + + public BookingItemCreatedEvent( + @NotNull final HsBookingItemController source, + @NotNull final HsBookingItem newBookingItem, + final String assetJson) { super(source); - this.newBookingItem = newBookingItem; + this.bookingItemUuid = newBookingItem.getUuid(); + this.assetJson = assetJson; + } + + public void setStatus(final Status status) { + this.status = status; + events.put(bookingItemUuid, this); } } 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 b3e3250e..801fff0f 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 @@ -1,5 +1,7 @@ package net.hostsharing.hsadminng.hs.booking.item; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; import net.hostsharing.hsadminng.context.Context; import net.hostsharing.hsadminng.hs.booking.generated.api.v1.api.HsBookingItemsApi; import net.hostsharing.hsadminng.hs.booking.generated.api.v1.model.HsBookingItemInsertResource; @@ -40,6 +42,9 @@ public class HsBookingItemController implements HsBookingItemsApi { @Autowired private HsBookingItemRbacRepository bookingItemRepo; + @Autowired + private ObjectMapper jsonMapper; + @Autowired private EntityManagerWrapper em; @@ -77,7 +82,12 @@ public class HsBookingItemController implements HsBookingItemsApi { .mapUsing(e -> mapper.map(e, HsBookingItemResource.class, ITEM_TO_RESOURCE_POSTMAPPER)) .revampProperties(); - applicationEventPublisher.publishEvent(new BookingItemCreatedEvent(this, saveProcessor.getEntity())); + try { + applicationEventPublisher.publishEvent(new BookingItemCreatedEvent( + this, saveProcessor.getEntity(), jsonMapper.writeValueAsString(body.getAsset()))); + } catch (JsonProcessingException e) { + throw new RuntimeException(e); + } final var uri = MvcUriComponentsBuilder.fromController(getClass()) 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 index 1e712ad3..b1680084 100644 --- 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 @@ -48,7 +48,7 @@ public class BookingItemEntitySaveProcessor { return this; } - // TODO.impl: remove once the migration of legacy data is done + // TODO.legacy: 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"); 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 266ff641..5c12d21a 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,39 +1,30 @@ package net.hostsharing.hsadminng.hs.booking.item.validators; -import net.hostsharing.hsadminng.hs.booking.item.HsBookingItem; import net.hostsharing.hsadminng.hs.validation.PropertiesProvider; import jakarta.persistence.EntityManager; import java.security.SecureRandom; -import java.util.ArrayList; -import java.util.List; import static net.hostsharing.hsadminng.hs.hosting.asset.validators.Dns.REGISTRAR_LEVEL_DOMAINS; import static net.hostsharing.hsadminng.hs.validation.StringProperty.stringProperty; class HsDomainSetupBookingItemValidator extends HsBookingItemEntityValidator { + public static final String DOMAIN_NAME_PROPERTY_NAME = "domainName"; public static final String FQDN_REGEX = "^((?!-)[A-Za-z0-9-]{1,63}(? validateEntity(final HsBookingItem bookingItem) { - final var violations = new ArrayList(); - final var domainName = bookingItem.getDirectValue(DOMAIN_NAME_PROPERTY_NAME, String.class); - if (!bookingItem.isLoaded() && - domainName.matches("hostsharing.(com|net|org|coop|de)")) { - violations.add("'" + bookingItem.toShortString() + ".resources." + DOMAIN_NAME_PROPERTY_NAME + "' = '" + domainName - + "' is a forbidden Hostsharing domain name"); - } - violations.addAll(super.validateEntity(bookingItem)); - return violations; - } - private static String generateVerificationCode(final EntityManager em, final PropertiesProvider propertiesProvider) { final var userDefinedVerificationCode = propertiesProvider.getDirectValue(VERIFICATION_CODE_PROPERTY_NAME, String.class); if (userDefinedVerificationCode != null) { diff --git a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/HsBookingItemCreatedListener.java b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/HsBookingItemCreatedListener.java index c625076a..4312f5d3 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/HsBookingItemCreatedListener.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/HsBookingItemCreatedListener.java @@ -1,14 +1,31 @@ package net.hostsharing.hsadminng.hs.hosting.asset; +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.RequiredArgsConstructor; +import lombok.SneakyThrows; +import net.hostsharing.hsadminng.hs.booking.generated.api.v1.model.HsHostingAssetAutoInsertResource; +import net.hostsharing.hsadminng.hs.booking.generated.api.v1.model.HsHostingAssetTypeResource; import net.hostsharing.hsadminng.hs.booking.item.BookingItemCreatedEvent; +import net.hostsharing.hsadminng.hs.booking.item.BookingItemCreatedEvent.Status; import net.hostsharing.hsadminng.hs.booking.item.HsBookingItemRealEntity; import net.hostsharing.hsadminng.hs.hosting.asset.validators.HostingAssetEntitySaveProcessor; +import net.hostsharing.hsadminng.hs.office.contact.HsOfficeContactRealEntity; +import net.hostsharing.hsadminng.mapper.StandardMapper; import net.hostsharing.hsadminng.persistence.EntityManagerWrapper; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.ApplicationListener; import org.springframework.stereotype.Component; -import java.util.List; +import jakarta.validation.ValidationException; +import java.net.IDN; +import java.util.Map; +import java.util.UUID; + +import static java.util.Map.entry; +import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.DOMAIN_DNS_SETUP; +import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.DOMAIN_HTTP_SETUP; +import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.DOMAIN_MBOX_SETUP; +import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.DOMAIN_SMTP_SETUP; @Component public class HsBookingItemCreatedListener implements ApplicationListener { @@ -16,41 +33,143 @@ public class HsBookingItemCreatedListener implements ApplicationListener null; case CLOUD_SERVER -> null; case MANAGED_SERVER -> null; case MANAGED_WEBSPACE -> null; - case DOMAIN_SETUP -> createDomainSetupHostingAsset(newBookingItemRealEntity); + case DOMAIN_SETUP -> new DomainSetupHostingAssetFactory(newBookingItemRealEntity, asset); }; - if (newHostingAsset != null) { - try { - new HostingAssetEntitySaveProcessor(emw, newHostingAsset) - .preprocessEntity() - .validateEntity() - .prepareForSave() - .save() - .validateContext(); - } catch (final Exception e) { - // TODO.impl: store status in a separate field, maybe enum+message - newBookingItemRealEntity.getResources().put("status", e.getMessage()); - } + if (factory != null) { + event.setStatus(factory.performSaveProcess()); } } - private HsHostingAsset createDomainSetupHostingAsset(final HsBookingItemRealEntity fromBookingItem) { - return HsHostingAssetRbacEntity.builder() - .bookingItem(fromBookingItem) - .type(HsHostingAssetType.DOMAIN_SETUP) - .identifier(fromBookingItem.getDirectValue("domainName", String.class)) - .subHostingAssets(List.of( - // TARGET_UNIX_USER_PROPERTY_NAME - )) - .build(); + private T ref(final Class entityClass, final UUID uuid) { + return uuid != null ? emw.getReference(entityClass, uuid) : null; + } + + @RequiredArgsConstructor + abstract class HostingAssetFactory { + + final HsBookingItemRealEntity fromBookingItem; + final HsHostingAssetAutoInsertResource asset; + + protected HsHostingAsset create() { + return null; + } + + Status performSaveProcess() { + final var newHostingAsset = create(); + try { + return persist(newHostingAsset); + } catch (final Exception e) { + return Status.failed(e.getMessage()); + } + } + + protected Status persist(final HsHostingAsset newHostingAsset) { + new HostingAssetEntitySaveProcessor(emw, newHostingAsset) + .preprocessEntity() + .validateEntity() + .prepareForSave() + .save() + .validateContext(); + return Status.finished(); + } + } + + class DomainSetupHostingAssetFactory extends HostingAssetFactory { + + public DomainSetupHostingAssetFactory( + final HsBookingItemRealEntity newBookingItemRealEntity, + final HsHostingAssetAutoInsertResource asset) { + super(newBookingItemRealEntity, asset); + } + + @Override + protected HsHostingAsset create() { + final String domainName = asset.getIdentifier(); + final var domainSetupAsset = createDomainSetupAsset(domainName); + + // TODO.legacy: as long as we need to be compatible, we always do all technical domain-setups + final var subHostingAssetResources = asset.getSubHostingAssets(); + final var domainHttpSetupAssetResource = subHostingAssetResources.stream() + .filter(ha -> ha.getType() == HsHostingAssetTypeResource.DOMAIN_HTTP_SETUP) + .findFirst().orElseThrow(() -> new ValidationException( + domainName + ": missing target unix user (assignedToHostingAssetUuid) for DOMAIN_HTTP_SETUP ")); + final var domainHttpSetupAsset = domainSetupAsset.getSubHostingAssets().stream().filter(sha -> sha.getType() == DOMAIN_HTTP_SETUP).findFirst().orElseThrow(); + domainHttpSetupAsset.setParentAsset(domainSetupAsset); + final HsHostingAssetRealEntity assignedToUnixUserAsset = + emw.find(HsHostingAssetRealEntity.class, domainHttpSetupAssetResource.getAssignedToAssetUuid()); + domainHttpSetupAsset.setAssignedToAsset(assignedToUnixUserAsset); + + if (subHostingAssetResources.stream().noneMatch(ha -> ha.getType() == HsHostingAssetTypeResource.DOMAIN_DNS_SETUP)) { + domainSetupAsset.getSubHostingAssets().add(HsHostingAssetRealEntity.builder() + .type(DOMAIN_DNS_SETUP) + .parentAsset(domainSetupAsset) + .assignedToAsset(assignedToUnixUserAsset.getParentAsset()) // FIXME: why is that needed? + .identifier(domainName + "|DNS") + .config(Map.ofEntries( + // FIXME: + entry("TTL", 21600), + entry("auto-SOA", true) + )) + .build()); + } + if (subHostingAssetResources.stream().noneMatch(ha -> ha.getType() == HsHostingAssetTypeResource.DOMAIN_MBOX_SETUP)) { + domainSetupAsset.getSubHostingAssets().add(HsHostingAssetRealEntity.builder() + .type(DOMAIN_MBOX_SETUP) + .parentAsset(domainSetupAsset) + .assignedToAsset(assignedToUnixUserAsset.getParentAsset()) + .identifier(domainName + "|MBOX") + .caption("HTTP-Setup für " + IDN.toUnicode(domainName)) + .build()); + } + if (subHostingAssetResources.stream().noneMatch(ha -> ha.getType() == HsHostingAssetTypeResource.DOMAIN_SMTP_SETUP)) { + domainSetupAsset.getSubHostingAssets().add(HsHostingAssetRealEntity.builder() + .type(DOMAIN_SMTP_SETUP) + .parentAsset(domainSetupAsset) + .assignedToAsset(assignedToUnixUserAsset.getParentAsset()) + .identifier(domainName + "|SMTP") + .caption("HTTP-Setup für " + IDN.toUnicode(domainName)) + .build()); + } + return domainSetupAsset; + } + + private HsHostingAssetRealEntity createDomainSetupAsset(final String domainName) { + return HsHostingAssetRealEntity.builder() + .bookingItem(fromBookingItem) + .type(HsHostingAssetType.DOMAIN_SETUP) + .identifier(domainName) + .caption(asset.getCaption() != null ? asset.getCaption() : domainName) + .parentAsset(ref(HsHostingAssetRealEntity.class, asset.getParentAssetUuid())) + .alarmContact(ref(HsOfficeContactRealEntity.class, asset.getAlarmContactUuid())) + // FIXME .config(asset.getConfig()) + .subHostingAssets( + standardMapper.mapList(asset.getSubHostingAssets(), HsHostingAssetRealEntity.class) + ) + .build(); + } + + @Override + protected Status persist(final HsHostingAsset newHostingAsset) { + final var status = super.persist(newHostingAsset); + newHostingAsset.getSubHostingAssets().forEach(super::persist); + return status; + } } } diff --git a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetRealRepository.java b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetRealRepository.java index 1e177524..c3709039 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetRealRepository.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetRealRepository.java @@ -3,6 +3,7 @@ package net.hostsharing.hsadminng.hs.hosting.asset; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.Repository; +import jakarta.validation.constraints.NotNull; import java.util.List; import java.util.Optional; import java.util.UUID; @@ -13,6 +14,18 @@ public interface HsHostingAssetRealRepository extends HsHostingAssetRepository findByIdentifier(String assetIdentifier); + default List findByTypeAndIdentifier(@NotNull HsHostingAssetType type, @NotNull String identifier) { + return findByTypeAndIdentifierImpl(type.name(), identifier); + } + + @Query(""" + select ha + from HsHostingAssetRealEntity ha + where cast(ha.type as String) = :type + and ha.identifier = :identifier + """) + List findByTypeAndIdentifierImpl(@NotNull String type, @NotNull String identifier); + @Query(value = """ select ha.uuid, ha.alarmcontactuuid, diff --git a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HostingAssetEntitySaveProcessor.java b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HostingAssetEntitySaveProcessor.java index 3e5850e5..e6c43be2 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HostingAssetEntitySaveProcessor.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HostingAssetEntitySaveProcessor.java @@ -42,7 +42,7 @@ public class HostingAssetEntitySaveProcessor { return this; } - // TODO.impl: remove once the migration of legacy data is done + // TODO.legacy: 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 HostingAssetEntitySaveProcessor validateEntityIgnoring(final String... ignoreRegExp) { step("validateEntity", "prepareForSave"); diff --git a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsDomainDnsSetupHostingAssetValidator.java b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsDomainDnsSetupHostingAssetValidator.java index 57ffc279..72738416 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsDomainDnsSetupHostingAssetValidator.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsDomainDnsSetupHostingAssetValidator.java @@ -15,7 +15,7 @@ import static net.hostsharing.hsadminng.hs.validation.BooleanProperty.booleanPro import static net.hostsharing.hsadminng.hs.validation.IntegerProperty.integerProperty; import static net.hostsharing.hsadminng.hs.validation.StringProperty.stringProperty; -// TODO.impl: make package private once we've migrated the legacy data +// TODO.legacy: make package private once we've migrated the legacy data public class HsDomainDnsSetupHostingAssetValidator extends HostingAssetEntityValidator { // according to RFC 1035 (section 5) and RFC 1034 @@ -33,7 +33,7 @@ public class HsDomainDnsSetupHostingAssetValidator extends HostingAssetEntityVal RR_REGEX_NAME + RR_REGEX_IN + RR_REGEX_TTL + RR_RECORD_TYPE + RR_RECORD_DATA + RR_COMMENT; public static final String IDENTIFIER_SUFFIX = "|DNS"; - private static List zoneFileErrors = null; // TODO.impl: remove once legacy data is migrated + private static List zoneFileErrors = null; // TODO.legacy: remove once legacy data is migrated HsDomainDnsSetupHostingAssetValidator() { super( diff --git a/src/main/java/net/hostsharing/hsadminng/hs/validation/PasswordProperty.java b/src/main/java/net/hostsharing/hsadminng/hs/validation/PasswordProperty.java index ceaf2603..52cc3931 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/validation/PasswordProperty.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/validation/PasswordProperty.java @@ -31,7 +31,7 @@ public class PasswordProperty extends StringProperty { @Override protected void validate(final List result, final String propValue, final PropertiesProvider propProvider) { - // TODO.impl: remove after legacy data is migrated + // TODO.legacy: remove after legacy data is migrated if (HashGenerator.using(hashedUsing).couldBeHash(propValue) && propValue.length() > this.maxLength()) { // already hashed => do not validate return; diff --git a/src/main/resources/api-definition/hs-booking/hs-booking-item-schemas.yaml b/src/main/resources/api-definition/hs-booking/hs-booking-item-schemas.yaml index 92875b90..22d17ce3 100644 --- a/src/main/resources/api-definition/hs-booking/hs-booking-item-schemas.yaml +++ b/src/main/resources/api-definition/hs-booking/hs-booking-item-schemas.yaml @@ -58,6 +58,17 @@ components: nullable: false type: $ref: '#/components/schemas/HsBookingItemType' + identifier: + type: string + minLength: 3 + maxLength: 80 + nullable: false + description: only used as a default value for automatically created hosting assets, not part of the booking item + assignedToHostingAssetUuid: + type: string + format: uuid + nullable: false + description: only used as a default value for automatically created hosting assets, not part of the booking item caption: type: string minLength: 3 @@ -69,6 +80,8 @@ components: nullable: true resources: $ref: '#/components/schemas/BookingResources' + asset: + $ref: '../hs-hosting/hs-hosting-asset-schemas.yaml#/components/schemas/HsHostingAssetAutoInsert' required: - caption - projectUuid diff --git a/src/main/resources/api-definition/hs-hosting/hs-hosting-asset-schemas.yaml b/src/main/resources/api-definition/hs-hosting/hs-hosting-asset-schemas.yaml index b65a8a51..9ab6cf07 100644 --- a/src/main/resources/api-definition/hs-hosting/hs-hosting-asset-schemas.yaml +++ b/src/main/resources/api-definition/hs-hosting/hs-hosting-asset-schemas.yaml @@ -94,7 +94,68 @@ components: - type - identifier - caption - - config + additionalProperties: false + + HsHostingAssetAutoInsert: + type: object + properties: + parentAssetUuid: + type: string + format: uuid + nullable: true + assignedToAssetUuid: + type: string + format: uuid + type: + $ref: '#/components/schemas/HsHostingAssetType' + identifier: + type: string + minLength: 3 + maxLength: 80 + nullable: false + caption: + type: string + minLength: 3 + maxLength: 80 + nullable: false + alarmContactUuid: + type: string + format: uuid + nullable: true + config: + $ref: '#/components/schemas/HsHostingAssetConfiguration' + subHostingAssets: + type: array + items: + $ref: '#/components/schemas/HsHostingAssetSubInsert' + required: + - identifier + additionalProperties: false + + HsHostingAssetSubInsert: + type: object + properties: + assignedToAssetUuid: + type: string + format: uuid + type: + $ref: '#/components/schemas/HsHostingAssetType' + identifier: + type: string + minLength: 3 + maxLength: 80 + nullable: false + caption: + type: string + minLength: 3 + maxLength: 80 + nullable: false + alarmContactUuid: + type: string + format: uuid + nullable: true + config: + $ref: '#/components/schemas/HsHostingAssetConfiguration' additionalProperties: false HsHostingAssetConfiguration: diff --git a/src/main/resources/db/changelog/5-hs-office/501-contact/5016-hs-office-contact-migration.sql b/src/main/resources/db/changelog/5-hs-office/501-contact/5016-hs-office-contact-migration.sql index 4e0683a8..c9cb699f 100644 --- a/src/main/resources/db/changelog/5-hs-office/501-contact/5016-hs-office-contact-migration.sql +++ b/src/main/resources/db/changelog/5-hs-office/501-contact/5016-hs-office-contact-migration.sql @@ -1,6 +1,6 @@ --liquibase formatted sql --- TODO: These changesets are just for the external remote views to simulate the legacy tables. +-- TODO.legacy: These changesets are just for the external remote views to simulate the legacy tables. -- Once we don't need the external remote views anymore, create revert changesets. -- ============================================================================ diff --git a/src/main/resources/db/changelog/5-hs-office/504-partner/5046-hs-office-partner-migration.sql b/src/main/resources/db/changelog/5-hs-office/504-partner/5046-hs-office-partner-migration.sql index 0a4da2cd..8118102e 100644 --- a/src/main/resources/db/changelog/5-hs-office/504-partner/5046-hs-office-partner-migration.sql +++ b/src/main/resources/db/changelog/5-hs-office/504-partner/5046-hs-office-partner-migration.sql @@ -1,6 +1,6 @@ --liquibase formatted sql --- TODO: These changesets are just for the external remote views to simulate the legacy tables. +-- TODO.legacy: These changesets are just for the external remote views to simulate the legacy tables. -- Once we don't need the external remote views anymore, create revert changesets. -- ============================================================================ diff --git a/src/main/resources/db/changelog/5-hs-office/507-sepamandate/5076-hs-office-sepamandate-migration.sql b/src/main/resources/db/changelog/5-hs-office/507-sepamandate/5076-hs-office-sepamandate-migration.sql index 977bd8d0..c92b0ea1 100644 --- a/src/main/resources/db/changelog/5-hs-office/507-sepamandate/5076-hs-office-sepamandate-migration.sql +++ b/src/main/resources/db/changelog/5-hs-office/507-sepamandate/5076-hs-office-sepamandate-migration.sql @@ -1,6 +1,6 @@ --liquibase formatted sql --- TODO: These changesets are just for the external remote views to simulate the legacy tables. +-- TODO.legacy: These changesets are just for the external remote views to simulate the legacy tables. -- Once we don't need the external remote views anymore, create revert changesets. -- ============================================================================ diff --git a/src/main/resources/db/changelog/5-hs-office/511-coopshares/5116-hs-office-coopshares-migration.sql b/src/main/resources/db/changelog/5-hs-office/511-coopshares/5116-hs-office-coopshares-migration.sql index 1e4efb42..8e620bcd 100644 --- a/src/main/resources/db/changelog/5-hs-office/511-coopshares/5116-hs-office-coopshares-migration.sql +++ b/src/main/resources/db/changelog/5-hs-office/511-coopshares/5116-hs-office-coopshares-migration.sql @@ -1,6 +1,6 @@ --liquibase formatted sql --- TODO: These changesets are just for the external remote views to simulate the legacy tables. +-- TODO.legacy: These changesets are just for the external remote views to simulate the legacy tables. -- Once we don't need the external remote views anymore, create revert changesets. -- ============================================================================ diff --git a/src/main/resources/db/changelog/5-hs-office/512-coopassets/5126-hs-office-coopassets-migration.sql b/src/main/resources/db/changelog/5-hs-office/512-coopassets/5126-hs-office-coopassets-migration.sql index 1bb0d500..f8d5f610 100644 --- a/src/main/resources/db/changelog/5-hs-office/512-coopassets/5126-hs-office-coopassets-migration.sql +++ b/src/main/resources/db/changelog/5-hs-office/512-coopassets/5126-hs-office-coopassets-migration.sql @@ -1,6 +1,6 @@ --liquibase formatted sql --- TODO: These changesets are just for the external remote views to simulate the legacy tables. +-- TODO.legacy: These changesets are just for the external remote views to simulate the legacy tables. -- Once we don't need the external remote views anymore, create revert changesets. -- ============================================================================ diff --git a/src/main/resources/db/changelog/7-hs-hosting/701-hosting-asset/7010-hs-hosting-asset.sql b/src/main/resources/db/changelog/7-hs-hosting/701-hosting-asset/7010-hs-hosting-asset.sql index d8d1393e..ed2fdd30 100644 --- a/src/main/resources/db/changelog/7-hs-hosting/701-hosting-asset/7010-hs-hosting-asset.sql +++ b/src/main/resources/db/changelog/7-hs-hosting/701-hosting-asset/7010-hs-hosting-asset.sql @@ -41,7 +41,7 @@ create table if not exists hs_hosting.asset config jsonb not null, alarmContactUuid uuid null references hs_office.contact(uuid) initially deferred, - unique (type, identifier), -- at least as long as we need to be compatible to the legacy system + unique (type, identifier), -- TODO.legacy: at least as long as we need to be compatible to the legacy system constraint hosting_asset_has_booking_item_or_parent_asset check (bookingItemUuid is not null or parentAssetUuid is not null or type in ('DOMAIN_SETUP', 'IPV4_NUMBER', 'IPV6_NUMBER')) diff --git a/src/main/resources/db/changelog/7-hs-hosting/701-hosting-asset/7016-hs-hosting-asset-migration.sql b/src/main/resources/db/changelog/7-hs-hosting/701-hosting-asset/7016-hs-hosting-asset-migration.sql index b6edea34..a0305e8d 100644 --- a/src/main/resources/db/changelog/7-hs-hosting/701-hosting-asset/7016-hs-hosting-asset-migration.sql +++ b/src/main/resources/db/changelog/7-hs-hosting/701-hosting-asset/7016-hs-hosting-asset-migration.sql @@ -1,6 +1,6 @@ --liquibase formatted sql --- TODO: These changesets are just for the external remote views to simulate the legacy tables. +-- TODO.legacy: These changesets are just for the external remote views to simulate the legacy tables. -- Once we don't need the external remote views anymore, create revert changesets. -- ============================================================================ diff --git a/src/test/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemControllerAcceptanceTest.java b/src/test/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemControllerAcceptanceTest.java index cf43f8cb..ddce12f8 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemControllerAcceptanceTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemControllerAcceptanceTest.java @@ -5,12 +5,14 @@ import io.restassured.RestAssured; import io.restassured.http.ContentType; import net.hostsharing.hsadminng.HsadminNgApplication; import net.hostsharing.hsadminng.hs.booking.debitor.HsBookingDebitorRepository; +import net.hostsharing.hsadminng.hs.booking.project.HsBookingProjectRealEntity; import net.hostsharing.hsadminng.hs.booking.project.HsBookingProjectRealRepository; import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAsset; import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetRealRepository; import net.hostsharing.hsadminng.hs.hosting.asset.validators.Dns; import net.hostsharing.hsadminng.rbac.test.ContextBasedTestWithCleanup; import net.hostsharing.hsadminng.rbac.test.JpaAttempt; +import org.jetbrains.annotations.NotNull; import org.junit.jupiter.api.ClassOrderer; import org.junit.jupiter.api.MethodOrderer; import org.junit.jupiter.api.Nested; @@ -31,6 +33,7 @@ import java.util.UUID; import static java.util.Map.entry; import static java.util.Optional.ofNullable; import static net.hostsharing.hsadminng.hs.booking.item.HsBookingItemType.MANAGED_WEBSPACE; +import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.UNIX_USER; import static net.hostsharing.hsadminng.rbac.test.JsonMatcher.lenientlyEquals; import static org.assertj.core.api.Assertions.assertThat; import static org.hamcrest.Matchers.matchesRegex; @@ -70,11 +73,7 @@ class HsBookingItemControllerAcceptanceTest extends ContextBasedTestWithCleanup // given context("superuser-alex@hostsharing.net"); - final var givenProject = debitorRepo.findByDebitorNumber(1000111).stream() - .map(d -> realProjectRepo.findAllByDebitorUuid(d.getUuid())) - .flatMap(List::stream) - .findFirst() - .orElseThrow(); + final var givenProject = findDefaultProjectOfDebitorNumber(1000111); RestAssured // @formatter:off .given() @@ -138,11 +137,7 @@ class HsBookingItemControllerAcceptanceTest extends ContextBasedTestWithCleanup void globalAdmin_canAddBookingItem() { context.define("superuser-alex@hostsharing.net"); - final var givenProject = debitorRepo.findByDebitorNumber(1000111).stream() - .map(d -> realProjectRepo.findAllByDebitorUuid(d.getUuid())) - .flatMap(List::stream) - .findFirst() - .orElseThrow(); + final var givenProject = findDefaultProjectOfDebitorNumber(1000111); final var location = RestAssured // @formatter:off .given() @@ -189,34 +184,41 @@ class HsBookingItemControllerAcceptanceTest extends ContextBasedTestWithCleanup void projectAgent_canAddBookingItemWithHostingAsset() { context.define("superuser-alex@hostsharing.net", "hs_booking.project#D-1000111-D-1000111defaultproject:AGENT"); - final var givenProject = debitorRepo.findByDebitorNumber(1000111).stream() - .map(d -> realProjectRepo.findAllByDebitorUuid(d.getUuid())) - .flatMap(List::stream) - .findFirst() - .orElseThrow(); + final var givenProject = findDefaultProjectOfDebitorNumber(1000111); + final var givenUnixUser = realHostingAssetRepo.findByTypeAndIdentifier(UNIX_USER, "fir01-web").stream() + .findFirst().orElseThrow(); Dns.fakeResultForDomain("example.org", Dns.Result.fromRecords("Hostsharing-domain-setup-verification-code=just-a-fake-verification-code")); final var location = RestAssured // @formatter:off .given() - .header("current-subject", "superuser-alex@hostsharing.net") - .contentType(ContentType.JSON) - .body(""" - { - "projectUuid": "{projectUuid}", - "type": "DOMAIN_SETUP", - "caption": "some new domain-setup booking", - "resources": { - "domainName": "example.org", - "targetUnixUser": "fir01-web", - "verificationCode": "just-a-fake-verification-code" + .header("current-subject", "superuser-alex@hostsharing.net") + .contentType(ContentType.JSON) + .body(""" + { + "projectUuid": "{projectUuid}", + "type": "DOMAIN_SETUP", + "caption": "Domain-Setup for example.org", + "resources": { + "domainName": "example.org", + "verificationCode": "just-a-fake-verification-code" + }, + "asset": { // FIXME: rename to hostingAsset + "identifier": "example.org", // also as default for all subAssets + "subHostingAssets": [ + { + "type": "DOMAIN_HTTP_SETUP", + "assignedToAssetUuid": "{unixUserUuid}" + } + ] + } } - } - """ - .replace("{projectUuid}", givenProject.getUuid().toString()) - ) - .port(port) + """ + .replace("{projectUuid}", givenProject.getUuid().toString()) + .replace("{unixUserUuid}", givenUnixUser.getUuid().toString()) + ) + .port(port) .when() .post("http://localhost/api/hs/booking/items") .then().log().all().assertThat() @@ -225,10 +227,9 @@ class HsBookingItemControllerAcceptanceTest extends ContextBasedTestWithCleanup .body("", lenientlyEquals(""" { "type": "DOMAIN_SETUP", - "caption": "some new domain-setup booking", + "caption": "Domain-Setup for example.org", "validFrom": "{today}", - "validTo": null, - "resources": { "domainName": "example.org", "targetUnixUser": "fir01-web" } + "validTo": null } """ .replace("{today}", LocalDate.now().toString()) @@ -240,24 +241,37 @@ class HsBookingItemControllerAcceptanceTest extends ContextBasedTestWithCleanup // then, the new BookingItem can be accessed under the generated UUID final var newBookingItem = fetchRealBookingItemFromURI(location); assertThat(newBookingItem) - .extracting(bi -> bi.getDirectValue("domainName", String.class)) - .isEqualTo("example.org"); + .extracting(HsBookingItem::getCaption) + .isEqualTo("Domain-Setup for example.org"); - // and the related HostingAsset also got created - assertThat(realHostingAssetRepo.findByIdentifier("example.org")).isNotEmpty() + // and the related HostingAssets are also got created + final var domainSetupHostingAsset = realHostingAssetRepo.findByIdentifier("example.org"); + assertThat(domainSetupHostingAsset).isNotEmpty() .map(HsHostingAsset::getBookingItem) .contains(newBookingItem); + assertThat(realHostingAssetRepo.findByIdentifier("example.org|DNS")).isNotEmpty() + .map(HsHostingAsset::getParentAsset) + .isEqualTo(domainSetupHostingAsset); + assertThat(realHostingAssetRepo.findByIdentifier("example.org|HTTP")).isNotEmpty() + .map(HsHostingAsset::getParentAsset) + .isEqualTo(domainSetupHostingAsset); + assertThat(realHostingAssetRepo.findByIdentifier("example.org|MBOX")).isNotEmpty() + .map(HsHostingAsset::getParentAsset) + .isEqualTo(domainSetupHostingAsset); + assertThat(realHostingAssetRepo.findByIdentifier("example.org|SMTP")).isNotEmpty() + .map(HsHostingAsset::getParentAsset) + .isEqualTo(domainSetupHostingAsset); + final var status = BookingItemCreatedEvent.of(newBookingItem); + assertThat(status.getStatus().isFinished()); + } @Test void projectAgent_canAddBookingItemEvenIfHostingAssetCreationFails() { context.define("superuser-alex@hostsharing.net", "hs_booking.project#D-1000111-D-1000111defaultproject:AGENT"); - final var givenProject = debitorRepo.findByDebitorNumber(1000111).stream() - .map(d -> realProjectRepo.findAllByDebitorUuid(d.getUuid())) - .flatMap(List::stream) - .findFirst() - .orElseThrow(); + final var givenProject = findDefaultProjectOfDebitorNumber(1000111); + final var givenUnixUser = realHostingAssetRepo.findByIdentifier("fir01-web").stream().findFirst().orElseThrow(); Dns.fakeResultForDomain("example.org", Dns.Result.fromRecords()); // without valid verificationCode @@ -272,12 +286,21 @@ class HsBookingItemControllerAcceptanceTest extends ContextBasedTestWithCleanup "caption": "some new domain-setup booking", "resources": { "domainName": "example.org", - "targetUnixUser": "fir01-web", "verificationCode": "just-a-fake-verification-code" + }, + "asset": { // FIXME: rename to hostingAsset + "identifier": "example.org", // also as default for all subAssets + "subHostingAssets": [ + { + "type": "DOMAIN_HTTP_SETUP", + "assignedToAssetUuid": "{unixUserUuid}" + } + ] } } """ .replace("{projectUuid}", givenProject.getUuid().toString()) + .replace("{unixUserUuid}", givenUnixUser.getUuid().toString()) ) .port(port) .when() @@ -291,7 +314,7 @@ class HsBookingItemControllerAcceptanceTest extends ContextBasedTestWithCleanup "caption": "some new domain-setup booking", "validFrom": "{today}", "validTo": null, - "resources": { "domainName": "example.org", "targetUnixUser": "fir01-web" } + "resources": { "domainName": "example.org" } } """ .replace("{today}", LocalDate.now().toString()) @@ -305,8 +328,8 @@ class HsBookingItemControllerAcceptanceTest extends ContextBasedTestWithCleanup assertThat(newBookingItem) .extracting(bi -> bi.getDirectValue("domainName", String.class)) .isEqualTo("example.org"); - assertThat(newBookingItem) - .extracting(bi -> bi.getDirectValue("status", String.class)) + final var status = BookingItemCreatedEvent.of(newBookingItem); + assertThat(status.getStatus().getMessage()) .isEqualTo("[[DNS] no TXT record 'Hostsharing-domain-setup-verification-code=just-a-fake-verification-code' found for domain name 'example.org' (nor in its super-domain)]"); // but the related HostingAsset did not get created @@ -314,6 +337,14 @@ class HsBookingItemControllerAcceptanceTest extends ContextBasedTestWithCleanup } } + private @NotNull HsBookingProjectRealEntity findDefaultProjectOfDebitorNumber(final int debitorNumber) { + return debitorRepo.findByDebitorNumber(debitorNumber).stream() + .map(d -> realProjectRepo.findAllByDebitorUuid(d.getUuid())) + .flatMap(List::stream) + .findFirst() + .orElseThrow(); + } + @Nested @Order(1) @TestMethodOrder(MethodOrderer.OrderAnnotation.class) diff --git a/src/test/java/net/hostsharing/hsadminng/hs/migration/BaseOfficeDataImport.java b/src/test/java/net/hostsharing/hsadminng/hs/migration/BaseOfficeDataImport.java index 511647aa..9aa92773 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/migration/BaseOfficeDataImport.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/migration/BaseOfficeDataImport.java @@ -502,7 +502,7 @@ public abstract class BaseOfficeDataImport extends CsvDataImport { // this happens if a natural person is marked as 'contractual' for itself final var idsToRemove = new HashSet(); relations.forEach((id, r) -> { - if (r.getHolder() == r.getAnchor()) { + if (r.getType() == HsOfficeRelationType.REPRESENTATIVE && r.getHolder() == r.getAnchor()) { idsToRemove.add(id); } }); -- 2.39.5 From 53259c357bc921235a7a07bc536b10a181ee4e5d Mon Sep 17 00:00:00 2001 From: Michael Hoennig Date: Mon, 30 Sep 2024 15:30:27 +0200 Subject: [PATCH 03/31] remove targetUnixUser property from unit tests --- ...mainSetupBookingItemValidatorUnitTest.java | 36 +++---------------- 1 file changed, 5 insertions(+), 31 deletions(-) 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 66d77899..716c7161 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 @@ -36,8 +36,7 @@ class HsDomainSetupBookingItemValidatorUnitTest { .project(project) .caption("Test-Domain") .resources(Map.ofEntries( - entry("domainName", "example.org"), - entry("targetUnixUser", "xyz00") + entry("domainName", "example.org") )) .build(); @@ -59,7 +58,6 @@ class HsDomainSetupBookingItemValidatorUnitTest { .caption("Test-Domain") .resources(Map.ofEntries( entry("domainName", "example.org"), - entry("targetUnixUser", "xyz00"), entry("verificationCode", "1234-5678-9100") )) .build(); @@ -80,8 +78,7 @@ class HsDomainSetupBookingItemValidatorUnitTest { .project(project) .caption("Test-Domain") .resources(Map.ofEntries( - entry("domainName", right(TOO_LONG_DOMAIN_NAME, 253)), - entry("targetUnixUser", "xyz00") + entry("domainName", right(TOO_LONG_DOMAIN_NAME, 253)) )) .build(); @@ -99,8 +96,7 @@ class HsDomainSetupBookingItemValidatorUnitTest { .project(project) .caption("Test-Domain") .resources(Map.ofEntries( - entry("domainName", right(TOO_LONG_DOMAIN_NAME, 254)), - entry("targetUnixUser", "xyz00") + entry("domainName", right(TOO_LONG_DOMAIN_NAME, 254)) )) .build(); @@ -118,8 +114,7 @@ class HsDomainSetupBookingItemValidatorUnitTest { .project(project) .caption("Test-Domain") .resources(Map.ofEntries( - entry("domainName", "example.com"), - entry("targetUnixUser", "xyz00-test") + entry("domainName", "example.com") )) .build(); @@ -130,25 +125,6 @@ class HsDomainSetupBookingItemValidatorUnitTest { 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", @@ -196,8 +172,7 @@ class HsDomainSetupBookingItemValidatorUnitTest { .project(project) .caption("Test-Domain") .resources(Map.ofEntries( - entry("domainName", secondLevelRegistrarDomain), - entry("targetUnixUser", "xyz00") + entry("domainName", secondLevelRegistrarDomain) )) .build(); @@ -219,7 +194,6 @@ class HsDomainSetupBookingItemValidatorUnitTest { // then assertThat(validator.properties()).map(Map::toString).containsExactlyInAnyOrder( "{type=string, propertyName=domainName, matchesRegEx=[^((?!-)[A-Za-z0-9-]{1,63}(? Date: Mon, 30 Sep 2024 15:35:54 +0200 Subject: [PATCH 04/31] add HsDomainSetupBookingItemValidator.validateEntity again --- .../HsDomainSetupBookingItemValidator.java | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) 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 5c12d21a..69b9bf17 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,10 +1,13 @@ package net.hostsharing.hsadminng.hs.booking.item.validators; +import net.hostsharing.hsadminng.hs.booking.item.HsBookingItem; import net.hostsharing.hsadminng.hs.validation.PropertiesProvider; import jakarta.persistence.EntityManager; import java.security.SecureRandom; +import java.util.ArrayList; +import java.util.List; import static net.hostsharing.hsadminng.hs.hosting.asset.validators.Dns.REGISTRAR_LEVEL_DOMAINS; import static net.hostsharing.hsadminng.hs.validation.StringProperty.stringProperty; @@ -32,6 +35,19 @@ class HsDomainSetupBookingItemValidator extends HsBookingItemEntityValidator { ); } + @Override + public List validateEntity(final HsBookingItem bookingItem) { + final var violations = new ArrayList(); + final var domainName = bookingItem.getDirectValue(DOMAIN_NAME_PROPERTY_NAME, String.class); + if (!bookingItem.isLoaded() && + domainName.matches("hostsharing.(com|net|org|coop|de)")) { + violations.add("'" + bookingItem.toShortString() + ".resources." + DOMAIN_NAME_PROPERTY_NAME + "' = '" + domainName + + "' is a forbidden Hostsharing domain name"); + } + violations.addAll(super.validateEntity(bookingItem)); + return violations; + } + private static String generateVerificationCode(final EntityManager em, final PropertiesProvider propertiesProvider) { final var userDefinedVerificationCode = propertiesProvider.getDirectValue(VERIFICATION_CODE_PROPERTY_NAME, String.class); if (userDefinedVerificationCode != null) { -- 2.39.5 From 05ffe313f84fe8fa74f83f238fe3065b9263d525 Mon Sep 17 00:00:00 2001 From: Michael Hoennig Date: Mon, 30 Sep 2024 15:41:56 +0200 Subject: [PATCH 05/31] remove targetUnixUser property from asset unit tests --- .../validators/HsDomainSetupHostingAssetValidatorUnitTest.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsDomainSetupHostingAssetValidatorUnitTest.java b/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsDomainSetupHostingAssetValidatorUnitTest.java index e5a119d5..8acca8d9 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsDomainSetupHostingAssetValidatorUnitTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsDomainSetupHostingAssetValidatorUnitTest.java @@ -42,8 +42,7 @@ class HsDomainSetupHostingAssetValidatorUnitTest { .project(project) .type(HsBookingItemType.DOMAIN_SETUP) .resources(new HashMap<>(ofEntries( - entry("domainName", domainName), - entry("targetUnixUser", "xyz00") + entry("domainName", domainName) )))); HsBookingItemEntityValidatorRegistry.forType(HsBookingItemType.DOMAIN_SETUP).prepareProperties(null, bookingItem); return HsHostingAssetRbacEntity.builder() -- 2.39.5 From 6868d1571838617c39552f76f6b8a326f0da8c3f Mon Sep 17 00:00:00 2001 From: Michael Hoennig Date: Mon, 30 Sep 2024 15:48:48 +0200 Subject: [PATCH 06/31] remove targetUnixUser property from ImportHostingAssets --- .../hsadminng/hs/migration/ImportHostingAssets.java | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) 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 634ba207..904149b4 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/migration/ImportHostingAssets.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/migration/ImportHostingAssets.java @@ -1680,18 +1680,13 @@ 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("targetUnixUser", targetUnixUser) + entry("domainName", domainSetup.getIdentifier()) )) .build(); domainSetup.setBookingItem(bookingItem); -- 2.39.5 From 0223cc192936b883c759f7f1f53e88469085b9d9 Mon Sep 17 00:00:00 2001 From: Michael Hoennig Date: Mon, 30 Sep 2024 17:42:42 +0200 Subject: [PATCH 07/31] introduce BookingItemCreatedEventEntity + DB-table --- .../item/BookingItemCreatedAppEvent.java | 20 +++++++ .../booking/item/BookingItemCreatedEvent.java | 60 ------------------- .../item/BookingItemCreatedEventEntity.java | 60 +++++++++++++++++++ .../BookingItemCreatedEventRepository.java | 12 ++++ .../booking/item/HsBookingItemController.java | 5 +- .../asset/HsBookingItemCreatedListener.java | 32 +++++----- .../630-booking-item/6300-hs-booking-item.sql | 15 +++++ ...HsBookingItemControllerAcceptanceTest.java | 19 ++++-- 8 files changed, 140 insertions(+), 83 deletions(-) create mode 100644 src/main/java/net/hostsharing/hsadminng/hs/booking/item/BookingItemCreatedAppEvent.java delete mode 100644 src/main/java/net/hostsharing/hsadminng/hs/booking/item/BookingItemCreatedEvent.java create mode 100644 src/main/java/net/hostsharing/hsadminng/hs/booking/item/BookingItemCreatedEventEntity.java create mode 100644 src/main/java/net/hostsharing/hsadminng/hs/booking/item/BookingItemCreatedEventRepository.java diff --git a/src/main/java/net/hostsharing/hsadminng/hs/booking/item/BookingItemCreatedAppEvent.java b/src/main/java/net/hostsharing/hsadminng/hs/booking/item/BookingItemCreatedAppEvent.java new file mode 100644 index 00000000..1e65d914 --- /dev/null +++ b/src/main/java/net/hostsharing/hsadminng/hs/booking/item/BookingItemCreatedAppEvent.java @@ -0,0 +1,20 @@ +package net.hostsharing.hsadminng.hs.booking.item; + +import lombok.Getter; +import org.springframework.context.ApplicationEvent; + +import jakarta.validation.constraints.NotNull; + +@Getter +public class BookingItemCreatedAppEvent extends ApplicationEvent { + + private BookingItemCreatedEventEntity entity; + + public BookingItemCreatedAppEvent( + @NotNull final HsBookingItemController source, + @NotNull final HsBookingItemRealEntity newBookingItem, + final String assetJson) { + super(source); + this.entity = new BookingItemCreatedEventEntity(newBookingItem, assetJson); + } +} diff --git a/src/main/java/net/hostsharing/hsadminng/hs/booking/item/BookingItemCreatedEvent.java b/src/main/java/net/hostsharing/hsadminng/hs/booking/item/BookingItemCreatedEvent.java deleted file mode 100644 index 1b723353..00000000 --- a/src/main/java/net/hostsharing/hsadminng/hs/booking/item/BookingItemCreatedEvent.java +++ /dev/null @@ -1,60 +0,0 @@ -package net.hostsharing.hsadminng.hs.booking.item; - -import lombok.Getter; -import org.springframework.context.ApplicationEvent; - -import jakarta.validation.constraints.NotNull; -import java.util.HashMap; -import java.util.Map; -import java.util.UUID; - -@Getter -public class BookingItemCreatedEvent extends ApplicationEvent { - - private static final Map events = new HashMap<>(); // FIXME: use DB table - - static BookingItemCreatedEvent of(final HsBookingItemRealEntity bookingItem) { - return events.get(bookingItem.getUuid()); - } - - @Getter - public static class Status { - - private final String message; - - private Status(final String message) { - this.message = message; - } - - public static Status finished() { - return new Status(null); - } - - public static Status failed(final String errorMessage) { - return new Status(errorMessage); - } - - boolean isFinished() { - return message == null; - } - } - - private final @NotNull UUID bookingItemUuid; - private final @NotNull String assetJson; - - private Status status; - - public BookingItemCreatedEvent( - @NotNull final HsBookingItemController source, - @NotNull final HsBookingItem newBookingItem, - final String assetJson) { - super(source); - this.bookingItemUuid = newBookingItem.getUuid(); - this.assetJson = assetJson; - } - - public void setStatus(final Status status) { - this.status = status; - events.put(bookingItemUuid, this); - } -} diff --git a/src/main/java/net/hostsharing/hsadminng/hs/booking/item/BookingItemCreatedEventEntity.java b/src/main/java/net/hostsharing/hsadminng/hs/booking/item/BookingItemCreatedEventEntity.java new file mode 100644 index 00000000..03b39d88 --- /dev/null +++ b/src/main/java/net/hostsharing/hsadminng/hs/booking/item/BookingItemCreatedEventEntity.java @@ -0,0 +1,60 @@ +package net.hostsharing.hsadminng.hs.booking.item; + +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import lombok.experimental.SuperBuilder; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.MapsId; +import jakarta.persistence.Table; +import jakarta.persistence.Version; +import jakarta.validation.constraints.NotNull; +import java.util.UUID; + + + +@Entity +@Table(schema = "hs_booking", name = "item_created_event") +@SuperBuilder(toBuilder = true) +@Getter +@NoArgsConstructor +public class BookingItemCreatedEventEntity { + @Id + @Column(name="bookingitemuuid") + private UUID id; + + @MapsId + @ManyToOne(optional = false) + @JoinColumn(name = "bookingitemuuid", nullable = false) + private HsBookingItemRealEntity bookingItem; + + @Version + private int version; + + @Column(name = "assetjson") + private String assetJson; + + @Setter + @Column(name = "statusmessage") + private String statusMessage; + + @Setter + @Column + private boolean completed; + + public void setBookingItem(HsBookingItemRealEntity bookingItem) { + this.bookingItem = bookingItem; + } + + public BookingItemCreatedEventEntity( + @NotNull final HsBookingItemRealEntity newBookingItem, + final String assetJson) { + this.bookingItem = newBookingItem; + this.assetJson = assetJson; + } +} diff --git a/src/main/java/net/hostsharing/hsadminng/hs/booking/item/BookingItemCreatedEventRepository.java b/src/main/java/net/hostsharing/hsadminng/hs/booking/item/BookingItemCreatedEventRepository.java new file mode 100644 index 00000000..e36bda61 --- /dev/null +++ b/src/main/java/net/hostsharing/hsadminng/hs/booking/item/BookingItemCreatedEventRepository.java @@ -0,0 +1,12 @@ +package net.hostsharing.hsadminng.hs.booking.item; + +import org.springframework.data.repository.Repository; + +import java.util.UUID; + +public interface BookingItemCreatedEventRepository extends Repository { + + BookingItemCreatedEventEntity save(HsBookingItemRealEntity current); + + BookingItemCreatedEventEntity findByBookingItem(HsBookingItemRealEntity newBookingItem); +} 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 801fff0f..8a78ba2a 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 @@ -83,8 +83,9 @@ public class HsBookingItemController implements HsBookingItemsApi { .revampProperties(); try { - applicationEventPublisher.publishEvent(new BookingItemCreatedEvent( - this, saveProcessor.getEntity(), jsonMapper.writeValueAsString(body.getAsset()))); + final var bookingItemRealEntity = em.getReference(HsBookingItemRealEntity.class, saveProcessor.getEntity().getUuid()); + applicationEventPublisher.publishEvent(new BookingItemCreatedAppEvent( + this, bookingItemRealEntity, jsonMapper.writeValueAsString(body.getAsset()))); } catch (JsonProcessingException e) { throw new RuntimeException(e); } diff --git a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/HsBookingItemCreatedListener.java b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/HsBookingItemCreatedListener.java index 4312f5d3..b4eb59df 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/HsBookingItemCreatedListener.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/HsBookingItemCreatedListener.java @@ -5,8 +5,7 @@ import lombok.RequiredArgsConstructor; import lombok.SneakyThrows; import net.hostsharing.hsadminng.hs.booking.generated.api.v1.model.HsHostingAssetAutoInsertResource; import net.hostsharing.hsadminng.hs.booking.generated.api.v1.model.HsHostingAssetTypeResource; -import net.hostsharing.hsadminng.hs.booking.item.BookingItemCreatedEvent; -import net.hostsharing.hsadminng.hs.booking.item.BookingItemCreatedEvent.Status; +import net.hostsharing.hsadminng.hs.booking.item.BookingItemCreatedAppEvent; import net.hostsharing.hsadminng.hs.booking.item.HsBookingItemRealEntity; import net.hostsharing.hsadminng.hs.hosting.asset.validators.HostingAssetEntitySaveProcessor; import net.hostsharing.hsadminng.hs.office.contact.HsOfficeContactRealEntity; @@ -28,7 +27,7 @@ import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.DOMA import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.DOMAIN_SMTP_SETUP; @Component -public class HsBookingItemCreatedListener implements ApplicationListener { +public class HsBookingItemCreatedListener implements ApplicationListener { @Autowired private EntityManagerWrapper emw; @@ -41,19 +40,19 @@ public class HsBookingItemCreatedListener implements ApplicationListener null; case CLOUD_SERVER -> null; case MANAGED_SERVER -> null; - case MANAGED_WEBSPACE -> null; + case MANAGED_WEBSPACE -> null; // TODO.impl: implement ManagedWebspace HostingAsset creation, where possible case DOMAIN_SETUP -> new DomainSetupHostingAssetFactory(newBookingItemRealEntity, asset); }; if (factory != null) { - event.setStatus(factory.performSaveProcess()); + event.getEntity().setStatusMessage(factory.performSaveProcess()); + emw.persist(event.getEntity()); // TODO.impl: once we implement retry, we might need merge } } @@ -71,23 +70,23 @@ public class HsBookingItemCreatedListener implements ApplicationListener bi.getDirectValue("domainName", String.class)) .isEqualTo("example.org"); - final var status = BookingItemCreatedEvent.of(newBookingItem); - assertThat(status.getStatus().getMessage()) + final var event = bookingItemCreationEventRepo.findByBookingItem(newBookingItem); + assertThat(event.getStatusMessage()) .isEqualTo("[[DNS] no TXT record 'Hostsharing-domain-setup-verification-code=just-a-fake-verification-code' found for domain name 'example.org' (nor in its super-domain)]"); // but the related HostingAsset did not get created @@ -565,6 +569,13 @@ class HsBookingItemControllerAcceptanceTest extends ContextBasedTestWithCleanup }).assertSuccessful().returnedValue(); } + @AfterEach + void cleanupEventEntities() { + jpaAttempt.transacted(() -> { + em.createQuery("delete from BookingItemCreatedEventEntity").executeUpdate(); + }).assertSuccessful(); + } + private Map.Entry resource(final String key, final Object value) { return entry(key, value); } -- 2.39.5 From 63e026d4d53e2a1b3879c1d5d5c0b1202b4defd9 Mon Sep 17 00:00:00 2001 From: Michael Hoennig Date: Tue, 1 Oct 2024 09:34:40 +0200 Subject: [PATCH 08/31] refactor HsBookingItemCreatedListener --- .../asset/HsBookingItemCreatedListener.java | 125 +++++++++++------- 1 file changed, 74 insertions(+), 51 deletions(-) diff --git a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/HsBookingItemCreatedListener.java b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/HsBookingItemCreatedListener.java index b4eb59df..72720601 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/HsBookingItemCreatedListener.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/HsBookingItemCreatedListener.java @@ -4,9 +4,11 @@ import com.fasterxml.jackson.databind.ObjectMapper; import lombok.RequiredArgsConstructor; import lombok.SneakyThrows; import net.hostsharing.hsadminng.hs.booking.generated.api.v1.model.HsHostingAssetAutoInsertResource; +import net.hostsharing.hsadminng.hs.booking.generated.api.v1.model.HsHostingAssetSubInsertResource; import net.hostsharing.hsadminng.hs.booking.generated.api.v1.model.HsHostingAssetTypeResource; import net.hostsharing.hsadminng.hs.booking.item.BookingItemCreatedAppEvent; import net.hostsharing.hsadminng.hs.booking.item.HsBookingItemRealEntity; +import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetRealEntity.HsHostingAssetRealEntityBuilder; import net.hostsharing.hsadminng.hs.hosting.asset.validators.HostingAssetEntitySaveProcessor; import net.hostsharing.hsadminng.hs.office.contact.HsOfficeContactRealEntity; import net.hostsharing.hsadminng.mapper.StandardMapper; @@ -17,11 +19,10 @@ import org.springframework.stereotype.Component; import jakarta.validation.ValidationException; import java.net.IDN; -import java.util.Map; +import java.util.List; import java.util.UUID; +import java.util.function.Function; -import static java.util.Map.entry; -import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.DOMAIN_DNS_SETUP; import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.DOMAIN_HTTP_SETUP; import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.DOMAIN_MBOX_SETUP; import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.DOMAIN_SMTP_SETUP; @@ -44,9 +45,7 @@ public class HsBookingItemCreatedListener implements ApplicationListener null; - case CLOUD_SERVER -> null; - case MANAGED_SERVER -> null; + case PRIVATE_CLOUD, CLOUD_SERVER, MANAGED_SERVER -> null; // for now, no automatic HostingAsset possible case MANAGED_WEBSPACE -> null; // TODO.impl: implement ManagedWebspace HostingAsset creation, where possible case DOMAIN_SETUP -> new DomainSetupHostingAssetFactory(newBookingItemRealEntity, asset); }; @@ -100,52 +99,34 @@ public class HsBookingItemCreatedListener implements ApplicationListener ha.getType() == HsHostingAssetTypeResource.DOMAIN_HTTP_SETUP) - .findFirst().orElseThrow(() -> new ValidationException( - domainName + ": missing target unix user (assignedToHostingAssetUuid) for DOMAIN_HTTP_SETUP ")); - final var domainHttpSetupAsset = domainSetupAsset.getSubHostingAssets().stream().filter(sha -> sha.getType() == DOMAIN_HTTP_SETUP).findFirst().orElseThrow(); - domainHttpSetupAsset.setParentAsset(domainSetupAsset); - final HsHostingAssetRealEntity assignedToUnixUserAsset = - emw.find(HsHostingAssetRealEntity.class, domainHttpSetupAssetResource.getAssignedToAssetUuid()); - domainHttpSetupAsset.setAssignedToAsset(assignedToUnixUserAsset); + final var subHostingAssetResources = getSubHostingAssetResources(); + final var domainHttpSetupAsset = createDomainHttpSetupAssetEntity( + subHostingAssetResources, + getDomainName(), + domainSetupAsset); + final var assignedToUnixUserAsset = domainHttpSetupAsset.getAssignedToAsset(); + + // TODO.legacy: don't create DNS setup as long as we need to remain support legacy web-ui etc. + + createDomainSubSetupAssetEntity( + domainSetupAsset, + DOMAIN_MBOX_SETUP, + builder -> builder + .assignedToAsset(assignedToUnixUserAsset.getParentAsset()) + .identifier(getDomainName() + "|MBOX") + .caption("HTTP-Setup für " + IDN.toUnicode(getDomainName()))); + + createDomainSubSetupAssetEntity( + domainSetupAsset, + DOMAIN_SMTP_SETUP, + builder -> builder + .assignedToAsset(assignedToUnixUserAsset.getParentAsset()) + .identifier(getDomainName() + "|SMTP") + .caption("HTTP-Setup für " + IDN.toUnicode(getDomainName()))); - if (subHostingAssetResources.stream().noneMatch(ha -> ha.getType() == HsHostingAssetTypeResource.DOMAIN_DNS_SETUP)) { - domainSetupAsset.getSubHostingAssets().add(HsHostingAssetRealEntity.builder() - .type(DOMAIN_DNS_SETUP) - .parentAsset(domainSetupAsset) - .assignedToAsset(assignedToUnixUserAsset.getParentAsset()) // FIXME: why is that needed? - .identifier(domainName + "|DNS") - .config(Map.ofEntries( - // FIXME: - entry("TTL", 21600), - entry("auto-SOA", true) - )) - .build()); - } - if (subHostingAssetResources.stream().noneMatch(ha -> ha.getType() == HsHostingAssetTypeResource.DOMAIN_MBOX_SETUP)) { - domainSetupAsset.getSubHostingAssets().add(HsHostingAssetRealEntity.builder() - .type(DOMAIN_MBOX_SETUP) - .parentAsset(domainSetupAsset) - .assignedToAsset(assignedToUnixUserAsset.getParentAsset()) - .identifier(domainName + "|MBOX") - .caption("HTTP-Setup für " + IDN.toUnicode(domainName)) - .build()); - } - if (subHostingAssetResources.stream().noneMatch(ha -> ha.getType() == HsHostingAssetTypeResource.DOMAIN_SMTP_SETUP)) { - domainSetupAsset.getSubHostingAssets().add(HsHostingAssetRealEntity.builder() - .type(DOMAIN_SMTP_SETUP) - .parentAsset(domainSetupAsset) - .assignedToAsset(assignedToUnixUserAsset.getParentAsset()) - .identifier(domainName + "|SMTP") - .caption("HTTP-Setup für " + IDN.toUnicode(domainName)) - .build()); - } return domainSetupAsset; } @@ -157,13 +138,55 @@ public class HsBookingItemCreatedListener implements ApplicationListener subHostingAssetResources, + final String domainName, + final HsHostingAssetRealEntity domainSetupAsset) { + final var domainHttpSetupAssetResource = subHostingAssetResources.stream() + .filter(ha -> ha.getType() == HsHostingAssetTypeResource.DOMAIN_HTTP_SETUP) + .findFirst().orElseThrow(() -> new ValidationException( + domainName + ": missing target unix user (assignedToHostingAssetUuid) for DOMAIN_HTTP_SETUP ")); + final var domainHttpSetupAsset = domainSetupAsset.getSubHostingAssets() + .stream() + .filter(sha -> sha.getType() == DOMAIN_HTTP_SETUP) + .findFirst() + .orElseThrow(); + domainHttpSetupAsset.setParentAsset(domainSetupAsset); + final var assignedToUnixUserAssetX = + emw.find(HsHostingAssetRealEntity.class, domainHttpSetupAssetResource.getAssignedToAssetUuid()); + domainHttpSetupAsset.setAssignedToAsset(assignedToUnixUserAssetX); + return domainHttpSetupAsset; + } + + private void createDomainSubSetupAssetEntity( + final HsHostingAssetRealEntity domainSetupAsset, + final HsHostingAssetType subAssetType, + final Function, HsHostingAssetRealEntityBuilder> builderTransformer) { + final var resourceType = HsHostingAssetTypeResource.valueOf(subAssetType.name()); + if (getSubHostingAssetResources().stream().noneMatch(ha -> ha.getType() == resourceType)) { + final var subAssetEntity = builderTransformer.apply( + HsHostingAssetRealEntity.builder() + .type(HsHostingAssetType.valueOf(subAssetType.name())) + .parentAsset(domainSetupAsset)) + .build(); + domainSetupAsset.getSubHostingAssets().add(subAssetEntity); + } + } + + private String getDomainName() { + return asset.getIdentifier(); + } + + private List getSubHostingAssetResources() { + return asset.getSubHostingAssets(); + } + @Override protected void persist(final HsHostingAsset newHostingAsset) { super.persist(newHostingAsset); -- 2.39.5 From 4d1d4cb9e94dc3a27d2bfd87114d8e2dce8dc3a4 Mon Sep 17 00:00:00 2001 From: Michael Hoennig Date: Tue, 1 Oct 2024 09:51:16 +0200 Subject: [PATCH 09/31] refactor HsBookingItemCreatedListener - extracting factories --- .../asset/HsBookingItemCreatedListener.java | 196 ------------------ .../DomainSetupHostingAssetFactory.java | 128 ++++++++++++ .../asset/factories/HostingAssetFactory.java | 45 ++++ .../HsBookingItemCreatedListener.java | 40 ++++ 4 files changed, 213 insertions(+), 196 deletions(-) delete mode 100644 src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/HsBookingItemCreatedListener.java create mode 100644 src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/factories/DomainSetupHostingAssetFactory.java create mode 100644 src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/factories/HostingAssetFactory.java create mode 100644 src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/factories/HsBookingItemCreatedListener.java diff --git a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/HsBookingItemCreatedListener.java b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/HsBookingItemCreatedListener.java deleted file mode 100644 index 72720601..00000000 --- a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/HsBookingItemCreatedListener.java +++ /dev/null @@ -1,196 +0,0 @@ -package net.hostsharing.hsadminng.hs.hosting.asset; - -import com.fasterxml.jackson.databind.ObjectMapper; -import lombok.RequiredArgsConstructor; -import lombok.SneakyThrows; -import net.hostsharing.hsadminng.hs.booking.generated.api.v1.model.HsHostingAssetAutoInsertResource; -import net.hostsharing.hsadminng.hs.booking.generated.api.v1.model.HsHostingAssetSubInsertResource; -import net.hostsharing.hsadminng.hs.booking.generated.api.v1.model.HsHostingAssetTypeResource; -import net.hostsharing.hsadminng.hs.booking.item.BookingItemCreatedAppEvent; -import net.hostsharing.hsadminng.hs.booking.item.HsBookingItemRealEntity; -import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetRealEntity.HsHostingAssetRealEntityBuilder; -import net.hostsharing.hsadminng.hs.hosting.asset.validators.HostingAssetEntitySaveProcessor; -import net.hostsharing.hsadminng.hs.office.contact.HsOfficeContactRealEntity; -import net.hostsharing.hsadminng.mapper.StandardMapper; -import net.hostsharing.hsadminng.persistence.EntityManagerWrapper; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.context.ApplicationListener; -import org.springframework.stereotype.Component; - -import jakarta.validation.ValidationException; -import java.net.IDN; -import java.util.List; -import java.util.UUID; -import java.util.function.Function; - -import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.DOMAIN_HTTP_SETUP; -import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.DOMAIN_MBOX_SETUP; -import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.DOMAIN_SMTP_SETUP; - -@Component -public class HsBookingItemCreatedListener implements ApplicationListener { - - @Autowired - private EntityManagerWrapper emw; - - @Autowired - private ObjectMapper jsonMapper; - - @Autowired - private StandardMapper standardMapper; - - @Override - @SneakyThrows - public void onApplicationEvent(final BookingItemCreatedAppEvent event) { - final var newBookingItemRealEntity = event.getEntity().getBookingItem(); - final var asset = jsonMapper.readValue(event.getEntity().getAssetJson(), HsHostingAssetAutoInsertResource.class); - final var factory = switch (newBookingItemRealEntity.getType()) { - case PRIVATE_CLOUD, CLOUD_SERVER, MANAGED_SERVER -> null; // for now, no automatic HostingAsset possible - case MANAGED_WEBSPACE -> null; // TODO.impl: implement ManagedWebspace HostingAsset creation, where possible - case DOMAIN_SETUP -> new DomainSetupHostingAssetFactory(newBookingItemRealEntity, asset); - }; - if (factory != null) { - event.getEntity().setStatusMessage(factory.performSaveProcess()); - emw.persist(event.getEntity()); // TODO.impl: once we implement retry, we might need merge - } - } - - private T ref(final Class entityClass, final UUID uuid) { - return uuid != null ? emw.getReference(entityClass, uuid) : null; - } - - @RequiredArgsConstructor - abstract class HostingAssetFactory { - - final HsBookingItemRealEntity fromBookingItem; - final HsHostingAssetAutoInsertResource asset; - - protected HsHostingAsset create() { - return null; - } - - String performSaveProcess() { - final var newHostingAsset = create(); - try { - persist(newHostingAsset); - return null; - } catch (final Exception e) { - return e.getMessage(); - } - } - - protected void persist(final HsHostingAsset newHostingAsset) { - new HostingAssetEntitySaveProcessor(emw, newHostingAsset) - .preprocessEntity() - .validateEntity() - .prepareForSave() - .save() - .validateContext(); - } - } - - class DomainSetupHostingAssetFactory extends HostingAssetFactory { - - public DomainSetupHostingAssetFactory( - final HsBookingItemRealEntity newBookingItemRealEntity, - final HsHostingAssetAutoInsertResource asset) { - super(newBookingItemRealEntity, asset); - } - - @Override - protected HsHostingAsset create() { - final var domainSetupAsset = createDomainSetupAsset(getDomainName()); - - // TODO.legacy: as long as we need to be compatible, we always do all technical domain-setups - final var subHostingAssetResources = getSubHostingAssetResources(); - final var domainHttpSetupAsset = createDomainHttpSetupAssetEntity( - subHostingAssetResources, - getDomainName(), - domainSetupAsset); - final var assignedToUnixUserAsset = domainHttpSetupAsset.getAssignedToAsset(); - - // TODO.legacy: don't create DNS setup as long as we need to remain support legacy web-ui etc. - - createDomainSubSetupAssetEntity( - domainSetupAsset, - DOMAIN_MBOX_SETUP, - builder -> builder - .assignedToAsset(assignedToUnixUserAsset.getParentAsset()) - .identifier(getDomainName() + "|MBOX") - .caption("HTTP-Setup für " + IDN.toUnicode(getDomainName()))); - - createDomainSubSetupAssetEntity( - domainSetupAsset, - DOMAIN_SMTP_SETUP, - builder -> builder - .assignedToAsset(assignedToUnixUserAsset.getParentAsset()) - .identifier(getDomainName() + "|SMTP") - .caption("HTTP-Setup für " + IDN.toUnicode(getDomainName()))); - - return domainSetupAsset; - } - - private HsHostingAssetRealEntity createDomainSetupAsset(final String domainName) { - return HsHostingAssetRealEntity.builder() - .bookingItem(fromBookingItem) - .type(HsHostingAssetType.DOMAIN_SETUP) - .identifier(domainName) - .caption(asset.getCaption() != null ? asset.getCaption() : domainName) - .parentAsset(ref(HsHostingAssetRealEntity.class, asset.getParentAssetUuid())) - .alarmContact(ref(HsOfficeContactRealEntity.class, asset.getAlarmContactUuid())) - .subHostingAssets( - standardMapper.mapList(getSubHostingAssetResources(), HsHostingAssetRealEntity.class) - ) - .build(); - } - - private HsHostingAssetRealEntity createDomainHttpSetupAssetEntity( - final List subHostingAssetResources, - final String domainName, - final HsHostingAssetRealEntity domainSetupAsset) { - final var domainHttpSetupAssetResource = subHostingAssetResources.stream() - .filter(ha -> ha.getType() == HsHostingAssetTypeResource.DOMAIN_HTTP_SETUP) - .findFirst().orElseThrow(() -> new ValidationException( - domainName + ": missing target unix user (assignedToHostingAssetUuid) for DOMAIN_HTTP_SETUP ")); - final var domainHttpSetupAsset = domainSetupAsset.getSubHostingAssets() - .stream() - .filter(sha -> sha.getType() == DOMAIN_HTTP_SETUP) - .findFirst() - .orElseThrow(); - domainHttpSetupAsset.setParentAsset(domainSetupAsset); - final var assignedToUnixUserAssetX = - emw.find(HsHostingAssetRealEntity.class, domainHttpSetupAssetResource.getAssignedToAssetUuid()); - domainHttpSetupAsset.setAssignedToAsset(assignedToUnixUserAssetX); - return domainHttpSetupAsset; - } - - private void createDomainSubSetupAssetEntity( - final HsHostingAssetRealEntity domainSetupAsset, - final HsHostingAssetType subAssetType, - final Function, HsHostingAssetRealEntityBuilder> builderTransformer) { - final var resourceType = HsHostingAssetTypeResource.valueOf(subAssetType.name()); - if (getSubHostingAssetResources().stream().noneMatch(ha -> ha.getType() == resourceType)) { - final var subAssetEntity = builderTransformer.apply( - HsHostingAssetRealEntity.builder() - .type(HsHostingAssetType.valueOf(subAssetType.name())) - .parentAsset(domainSetupAsset)) - .build(); - domainSetupAsset.getSubHostingAssets().add(subAssetEntity); - } - } - - private String getDomainName() { - return asset.getIdentifier(); - } - - private List getSubHostingAssetResources() { - return asset.getSubHostingAssets(); - } - - @Override - protected void persist(final HsHostingAsset newHostingAsset) { - super.persist(newHostingAsset); - newHostingAsset.getSubHostingAssets().forEach(super::persist); - } - } -} diff --git a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/factories/DomainSetupHostingAssetFactory.java b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/factories/DomainSetupHostingAssetFactory.java new file mode 100644 index 00000000..37045f93 --- /dev/null +++ b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/factories/DomainSetupHostingAssetFactory.java @@ -0,0 +1,128 @@ +package net.hostsharing.hsadminng.hs.hosting.asset.factories; + +import net.hostsharing.hsadminng.hs.booking.generated.api.v1.model.HsHostingAssetAutoInsertResource; +import net.hostsharing.hsadminng.hs.booking.generated.api.v1.model.HsHostingAssetSubInsertResource; +import net.hostsharing.hsadminng.hs.booking.generated.api.v1.model.HsHostingAssetTypeResource; +import net.hostsharing.hsadminng.hs.booking.item.HsBookingItemRealEntity; +import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAsset; +import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetRealEntity; +import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType; +import net.hostsharing.hsadminng.hs.office.contact.HsOfficeContactRealEntity; +import net.hostsharing.hsadminng.mapper.StandardMapper; +import net.hostsharing.hsadminng.persistence.EntityManagerWrapper; + +import jakarta.validation.ValidationException; +import java.net.IDN; +import java.util.List; +import java.util.function.Function; + +import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.DOMAIN_HTTP_SETUP; +import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.DOMAIN_MBOX_SETUP; +import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.DOMAIN_SMTP_SETUP; + +public class DomainSetupHostingAssetFactory extends HostingAssetFactory { + + public DomainSetupHostingAssetFactory( + final EntityManagerWrapper emw, + final HsBookingItemRealEntity newBookingItemRealEntity, + final HsHostingAssetAutoInsertResource asset, + final StandardMapper standardMapper) { + super(emw, newBookingItemRealEntity, asset, standardMapper); + } + + @Override + protected HsHostingAsset create() { + final var domainSetupAsset = createDomainSetupAsset(getDomainName()); + + // TODO.legacy: as long as we need to be compatible, we always do all technical domain-setups + final var subHostingAssetResources = getSubHostingAssetResources(); + final var domainHttpSetupAsset = createDomainHttpSetupAssetEntity( + subHostingAssetResources, + getDomainName(), + domainSetupAsset); + final var assignedToUnixUserAsset = domainHttpSetupAsset.getAssignedToAsset(); + + // TODO.legacy: don't create DNS setup as long as we need to remain support legacy web-ui etc. + + createDomainSubSetupAssetEntity( + domainSetupAsset, + DOMAIN_MBOX_SETUP, + builder -> builder + .assignedToAsset(assignedToUnixUserAsset.getParentAsset()) + .identifier(getDomainName() + "|MBOX") + .caption("HTTP-Setup für " + IDN.toUnicode(getDomainName()))); + + createDomainSubSetupAssetEntity( + domainSetupAsset, + DOMAIN_SMTP_SETUP, + builder -> builder + .assignedToAsset(assignedToUnixUserAsset.getParentAsset()) + .identifier(getDomainName() + "|SMTP") + .caption("HTTP-Setup für " + IDN.toUnicode(getDomainName()))); + + return domainSetupAsset; + } + + private HsHostingAssetRealEntity createDomainSetupAsset(final String domainName) { + return HsHostingAssetRealEntity.builder() + .bookingItem(fromBookingItem) + .type(HsHostingAssetType.DOMAIN_SETUP) + .identifier(domainName) + .caption(asset.getCaption() != null ? asset.getCaption() : domainName) + .parentAsset(ref(HsHostingAssetRealEntity.class, asset.getParentAssetUuid())) + .alarmContact(ref(HsOfficeContactRealEntity.class, asset.getAlarmContactUuid())) + .subHostingAssets( + standardMapper.mapList(getSubHostingAssetResources(), HsHostingAssetRealEntity.class) + ) + .build(); + } + + private HsHostingAssetRealEntity createDomainHttpSetupAssetEntity( + final List subHostingAssetResources, + final String domainName, + final HsHostingAssetRealEntity domainSetupAsset) { + final var domainHttpSetupAssetResource = subHostingAssetResources.stream() + .filter(ha -> ha.getType() == HsHostingAssetTypeResource.DOMAIN_HTTP_SETUP) + .findFirst().orElseThrow(() -> new ValidationException( + domainName + ": missing target unix user (assignedToHostingAssetUuid) for DOMAIN_HTTP_SETUP ")); + final var domainHttpSetupAsset = domainSetupAsset.getSubHostingAssets() + .stream() + .filter(sha -> sha.getType() == DOMAIN_HTTP_SETUP) + .findFirst() + .orElseThrow(); + domainHttpSetupAsset.setParentAsset(domainSetupAsset); + final var assignedToUnixUserAsset = + emw.find(HsHostingAssetRealEntity.class, domainHttpSetupAssetResource.getAssignedToAssetUuid()); + domainHttpSetupAsset.setAssignedToAsset(assignedToUnixUserAsset); + return domainHttpSetupAsset; + } + + private void createDomainSubSetupAssetEntity( + final HsHostingAssetRealEntity domainSetupAsset, + final HsHostingAssetType subAssetType, + final Function, HsHostingAssetRealEntity.HsHostingAssetRealEntityBuilder> builderTransformer) { + final var resourceType = HsHostingAssetTypeResource.valueOf(subAssetType.name()); + if (getSubHostingAssetResources().stream().noneMatch(ha -> ha.getType() == resourceType)) { + final var subAssetEntity = builderTransformer.apply( + HsHostingAssetRealEntity.builder() + .type(HsHostingAssetType.valueOf(subAssetType.name())) + .parentAsset(domainSetupAsset)) + .build(); + domainSetupAsset.getSubHostingAssets().add(subAssetEntity); + } + } + + private String getDomainName() { + return asset.getIdentifier(); + } + + private List getSubHostingAssetResources() { + return asset.getSubHostingAssets(); + } + + @Override + protected void persist(final HsHostingAsset newHostingAsset) { + super.persist(newHostingAsset); + newHostingAsset.getSubHostingAssets().forEach(super::persist); + } +} diff --git a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/factories/HostingAssetFactory.java b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/factories/HostingAssetFactory.java new file mode 100644 index 00000000..cf30a5b0 --- /dev/null +++ b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/factories/HostingAssetFactory.java @@ -0,0 +1,45 @@ +package net.hostsharing.hsadminng.hs.hosting.asset.factories; + +import lombok.RequiredArgsConstructor; +import net.hostsharing.hsadminng.hs.booking.generated.api.v1.model.HsHostingAssetAutoInsertResource; +import net.hostsharing.hsadminng.hs.booking.item.HsBookingItemRealEntity; +import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAsset; +import net.hostsharing.hsadminng.hs.hosting.asset.validators.HostingAssetEntitySaveProcessor; +import net.hostsharing.hsadminng.mapper.StandardMapper; +import net.hostsharing.hsadminng.persistence.EntityManagerWrapper; + +import java.util.UUID; + +@RequiredArgsConstructor +abstract class HostingAssetFactory { + + final EntityManagerWrapper emw; + final HsBookingItemRealEntity fromBookingItem; + final HsHostingAssetAutoInsertResource asset; + final StandardMapper standardMapper; + + protected abstract HsHostingAsset create(); + + public String performSaveProcess() { + final var newHostingAsset = create(); + try { + persist(newHostingAsset); + return null; + } catch (final Exception e) { + return e.getMessage(); + } + } + + protected void persist(final HsHostingAsset newHostingAsset) { + new HostingAssetEntitySaveProcessor(emw, newHostingAsset) + .preprocessEntity() + .validateEntity() + .prepareForSave() + .save() + .validateContext(); + } + + protected T ref(final Class entityClass, final UUID uuid) { + return uuid != null ? emw.getReference(entityClass, uuid) : null; + } +} diff --git a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/factories/HsBookingItemCreatedListener.java b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/factories/HsBookingItemCreatedListener.java new file mode 100644 index 00000000..3143c841 --- /dev/null +++ b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/factories/HsBookingItemCreatedListener.java @@ -0,0 +1,40 @@ +package net.hostsharing.hsadminng.hs.hosting.asset.factories; + +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.SneakyThrows; +import net.hostsharing.hsadminng.hs.booking.generated.api.v1.model.HsHostingAssetAutoInsertResource; +import net.hostsharing.hsadminng.hs.booking.item.BookingItemCreatedAppEvent; +import net.hostsharing.hsadminng.mapper.StandardMapper; +import net.hostsharing.hsadminng.persistence.EntityManagerWrapper; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationListener; +import org.springframework.stereotype.Component; + +@Component +public class HsBookingItemCreatedListener implements ApplicationListener { + + @Autowired + private EntityManagerWrapper emw; + + @Autowired + private ObjectMapper jsonMapper; + + @Autowired + private StandardMapper standardMapper; + + @Override + @SneakyThrows + public void onApplicationEvent(final BookingItemCreatedAppEvent event) { + final var newBookingItemRealEntity = event.getEntity().getBookingItem(); + final var asset = jsonMapper.readValue(event.getEntity().getAssetJson(), HsHostingAssetAutoInsertResource.class); + final var factory = switch (newBookingItemRealEntity.getType()) { + case PRIVATE_CLOUD, CLOUD_SERVER, MANAGED_SERVER -> null; // for now, no automatic HostingAsset possible + case MANAGED_WEBSPACE -> null; // TODO.impl: implement ManagedWebspace HostingAsset creation, where possible + case DOMAIN_SETUP -> new DomainSetupHostingAssetFactory(emw, newBookingItemRealEntity, asset, standardMapper); + }; + if (factory != null) { + event.getEntity().setStatusMessage(factory.performSaveProcess()); + emw.persist(event.getEntity()); // TODO.impl: once we implement retry, we might need merge + } + } +} -- 2.39.5 From 24225862f225e08893187471dae932f22b36f7ba Mon Sep 17 00:00:00 2001 From: Michael Hoennig Date: Tue, 1 Oct 2024 10:00:49 +0200 Subject: [PATCH 10/31] remove "example.org|DNS" from test --- .../booking/item/HsBookingItemControllerAcceptanceTest.java | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/test/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemControllerAcceptanceTest.java b/src/test/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemControllerAcceptanceTest.java index e2a06655..f7d616aa 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemControllerAcceptanceTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemControllerAcceptanceTest.java @@ -253,9 +253,7 @@ class HsBookingItemControllerAcceptanceTest extends ContextBasedTestWithCleanup assertThat(domainSetupHostingAsset).isNotEmpty() .map(HsHostingAsset::getBookingItem) .contains(newBookingItem); - assertThat(realHostingAssetRepo.findByIdentifier("example.org|DNS")).isNotEmpty() - .map(HsHostingAsset::getParentAsset) - .isEqualTo(domainSetupHostingAsset); + // TODO.legacy: add check for example.org|DNS assertThat(realHostingAssetRepo.findByIdentifier("example.org|HTTP")).isNotEmpty() .map(HsHostingAsset::getParentAsset) .isEqualTo(domainSetupHostingAsset); -- 2.39.5 From c08bddacda1aec6c113fc7f06fe28467621480d7 Mon Sep 17 00:00:00 2001 From: Michael Hoennig Date: Tue, 1 Oct 2024 10:30:41 +0200 Subject: [PATCH 11/31] fix architecture test --- .../java/net/hostsharing/hsadminng/arch/ArchitectureTest.java | 1 + 1 file changed, 1 insertion(+) diff --git a/src/test/java/net/hostsharing/hsadminng/arch/ArchitectureTest.java b/src/test/java/net/hostsharing/hsadminng/arch/ArchitectureTest.java index 2190d29f..aecfc7a8 100644 --- a/src/test/java/net/hostsharing/hsadminng/arch/ArchitectureTest.java +++ b/src/test/java/net/hostsharing/hsadminng/arch/ArchitectureTest.java @@ -64,6 +64,7 @@ public class ArchitectureTest { "..hs.booking.item.validators", "..hs.hosting.asset", "..hs.hosting.asset.validators", + "..hs.hosting.asset.factories", "..errors", "..mapper", "..ping", -- 2.39.5 From 924219a992ca9e57cd3db9adc24b46001584dd49 Mon Sep 17 00:00:00 2001 From: Michael Hoennig Date: Tue, 1 Oct 2024 10:57:49 +0200 Subject: [PATCH 12/31] remove completed flag from hs_booking.item_created_event --- .../hs/booking/item/BookingItemCreatedEventEntity.java | 8 -------- .../asset/factories/HsBookingItemCreatedListener.java | 8 ++++++-- .../630-booking-item/6300-hs-booking-item.sql | 3 +-- .../item/HsBookingItemControllerAcceptanceTest.java | 3 +-- 4 files changed, 8 insertions(+), 14 deletions(-) diff --git a/src/main/java/net/hostsharing/hsadminng/hs/booking/item/BookingItemCreatedEventEntity.java b/src/main/java/net/hostsharing/hsadminng/hs/booking/item/BookingItemCreatedEventEntity.java index 03b39d88..4a97dddf 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/booking/item/BookingItemCreatedEventEntity.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/booking/item/BookingItemCreatedEventEntity.java @@ -43,14 +43,6 @@ public class BookingItemCreatedEventEntity { @Column(name = "statusmessage") private String statusMessage; - @Setter - @Column - private boolean completed; - - public void setBookingItem(HsBookingItemRealEntity bookingItem) { - this.bookingItem = bookingItem; - } - public BookingItemCreatedEventEntity( @NotNull final HsBookingItemRealEntity newBookingItem, final String assetJson) { diff --git a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/factories/HsBookingItemCreatedListener.java b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/factories/HsBookingItemCreatedListener.java index 3143c841..3f63c50f 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/factories/HsBookingItemCreatedListener.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/factories/HsBookingItemCreatedListener.java @@ -33,8 +33,12 @@ public class HsBookingItemCreatedListener implements ApplicationListener new DomainSetupHostingAssetFactory(emw, newBookingItemRealEntity, asset, standardMapper); }; if (factory != null) { - event.getEntity().setStatusMessage(factory.performSaveProcess()); - emw.persist(event.getEntity()); // TODO.impl: once we implement retry, we might need merge + final var statusMessage = factory.performSaveProcess(); + // TODO.impl: once we implement retry, we need to amend this code (persist/merge/delete) + if (statusMessage != null) { + event.getEntity().setStatusMessage(statusMessage); + emw.persist(event.getEntity()); + } } } } diff --git a/src/main/resources/db/changelog/6-hs-booking/630-booking-item/6300-hs-booking-item.sql b/src/main/resources/db/changelog/6-hs-booking/630-booking-item/6300-hs-booking-item.sql index 5a3cc9d4..4c145652 100644 --- a/src/main/resources/db/changelog/6-hs-booking/630-booking-item/6300-hs-booking-item.sql +++ b/src/main/resources/db/changelog/6-hs-booking/630-booking-item/6300-hs-booking-item.sql @@ -40,8 +40,7 @@ create table if not exists hs_booking.item_created_event bookingItemUuid uuid unique references hs_booking.item (uuid), version int not null default 0, assetJson text, - statusMessage text, - completed boolean not null default false + statusMessage text ); --// diff --git a/src/test/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemControllerAcceptanceTest.java b/src/test/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemControllerAcceptanceTest.java index f7d616aa..db62357a 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemControllerAcceptanceTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemControllerAcceptanceTest.java @@ -264,8 +264,7 @@ class HsBookingItemControllerAcceptanceTest extends ContextBasedTestWithCleanup .map(HsHostingAsset::getParentAsset) .isEqualTo(domainSetupHostingAsset); final var event = bookingItemCreationEventRepo.findByBookingItem(newBookingItem); - assertThat(event.isCompleted()); - + assertThat(event).isNull(); } @Test -- 2.39.5 From 7e177adff36986e5ffafea909355fbb268d7b5f3 Mon Sep 17 00:00:00 2001 From: Michael Hoennig Date: Tue, 1 Oct 2024 17:46:58 +0200 Subject: [PATCH 13/31] add HsBookingItemCreatedListenerUnitTest with EntityManagerWrapperFake --- .../item/BookingItemCreatedAppEvent.java | 2 +- .../item/BookingItemCreatedEventEntity.java | 5 +- .../DomainSetupHostingAssetFactory.java | 3 +- .../HsBookingItemCreatedListener.java | 2 +- .../HsBookingItemCreatedListenerUnitTest.java | 137 ++++++++++++++++++ .../persistence/EntityManagerWrapperFake.java | 95 ++++++++++++ 6 files changed, 238 insertions(+), 6 deletions(-) create mode 100644 src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/factories/HsBookingItemCreatedListenerUnitTest.java create mode 100644 src/test/java/net/hostsharing/hsadminng/persistence/EntityManagerWrapperFake.java diff --git a/src/main/java/net/hostsharing/hsadminng/hs/booking/item/BookingItemCreatedAppEvent.java b/src/main/java/net/hostsharing/hsadminng/hs/booking/item/BookingItemCreatedAppEvent.java index 1e65d914..6960d626 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/booking/item/BookingItemCreatedAppEvent.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/booking/item/BookingItemCreatedAppEvent.java @@ -11,7 +11,7 @@ public class BookingItemCreatedAppEvent extends ApplicationEvent { private BookingItemCreatedEventEntity entity; public BookingItemCreatedAppEvent( - @NotNull final HsBookingItemController source, + @NotNull final Object source, @NotNull final HsBookingItemRealEntity newBookingItem, final String assetJson) { super(source); diff --git a/src/main/java/net/hostsharing/hsadminng/hs/booking/item/BookingItemCreatedEventEntity.java b/src/main/java/net/hostsharing/hsadminng/hs/booking/item/BookingItemCreatedEventEntity.java index 4a97dddf..245b056f 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/booking/item/BookingItemCreatedEventEntity.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/booking/item/BookingItemCreatedEventEntity.java @@ -4,6 +4,7 @@ import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; import lombok.experimental.SuperBuilder; +import net.hostsharing.hsadminng.rbac.object.BaseEntity; import jakarta.persistence.Column; import jakarta.persistence.Entity; @@ -23,10 +24,10 @@ import java.util.UUID; @SuperBuilder(toBuilder = true) @Getter @NoArgsConstructor -public class BookingItemCreatedEventEntity { +public class BookingItemCreatedEventEntity implements BaseEntity { @Id @Column(name="bookingitemuuid") - private UUID id; + private UUID uuid; @MapsId @ManyToOne(optional = false) diff --git a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/factories/DomainSetupHostingAssetFactory.java b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/factories/DomainSetupHostingAssetFactory.java index 37045f93..6421aa1d 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/factories/DomainSetupHostingAssetFactory.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/factories/DomainSetupHostingAssetFactory.java @@ -69,9 +69,8 @@ public class DomainSetupHostingAssetFactory extends HostingAssetFactory { .type(HsHostingAssetType.DOMAIN_SETUP) .identifier(domainName) .caption(asset.getCaption() != null ? asset.getCaption() : domainName) - .parentAsset(ref(HsHostingAssetRealEntity.class, asset.getParentAssetUuid())) .alarmContact(ref(HsOfficeContactRealEntity.class, asset.getAlarmContactUuid())) - .subHostingAssets( + .subHostingAssets( // FIXME: is this even used? standardMapper.mapList(getSubHostingAssetResources(), HsHostingAssetRealEntity.class) ) .build(); diff --git a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/factories/HsBookingItemCreatedListener.java b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/factories/HsBookingItemCreatedListener.java index 3f63c50f..1b031a05 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/factories/HsBookingItemCreatedListener.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/factories/HsBookingItemCreatedListener.java @@ -29,7 +29,7 @@ public class HsBookingItemCreatedListener implements ApplicationListener null; // for now, no automatic HostingAsset possible - case MANAGED_WEBSPACE -> null; // TODO.impl: implement ManagedWebspace HostingAsset creation, where possible + case MANAGED_WEBSPACE -> null; // FIXME: implement ManagedWebspace HostingAsset creation, where possible case DOMAIN_SETUP -> new DomainSetupHostingAssetFactory(emw, newBookingItemRealEntity, asset, standardMapper); }; if (factory != null) { diff --git a/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/factories/HsBookingItemCreatedListenerUnitTest.java b/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/factories/HsBookingItemCreatedListenerUnitTest.java new file mode 100644 index 00000000..8ae27c5a --- /dev/null +++ b/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/factories/HsBookingItemCreatedListenerUnitTest.java @@ -0,0 +1,137 @@ +package net.hostsharing.hsadminng.hs.hosting.asset.factories; + +import net.hostsharing.hsadminng.hs.booking.item.BookingItemCreatedAppEvent; +import net.hostsharing.hsadminng.hs.booking.item.BookingItemCreatedEventEntity; +import net.hostsharing.hsadminng.hs.booking.item.HsBookingItemRealEntity; +import net.hostsharing.hsadminng.hs.booking.item.HsBookingItemType; +import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetRealEntity; +import net.hostsharing.hsadminng.hs.hosting.asset.validators.Dns; +import net.hostsharing.hsadminng.persistence.EntityManagerWrapperFake; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import com.fasterxml.jackson.databind.ObjectMapper; +import net.hostsharing.hsadminng.config.JsonObjectMapperConfiguration; +import net.hostsharing.hsadminng.mapper.StandardMapper; +import net.hostsharing.hsadminng.persistence.EntityManagerWrapper; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Spy; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.util.Map; +import java.util.UUID; + +import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.MANAGED_WEBSPACE; +import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.UNIX_USER; +import static net.hostsharing.hsadminng.mapper.PatchableMapWrapper.entry; +import static org.assertj.core.api.Assertions.assertThat; + +@ExtendWith(MockitoExtension.class) +class HsBookingItemCreatedListenerUnitTest { + + private final HsHostingAssetRealEntity managedWebspaceHostingAsset = HsHostingAssetRealEntity.builder() + .uuid(UUID.randomUUID()) + .type(MANAGED_WEBSPACE) + .build(); + + private final HsHostingAssetRealEntity unixUserHostingAsset = HsHostingAssetRealEntity.builder() + .uuid(UUID.randomUUID()) + .type(UNIX_USER) + .parentAsset(managedWebspaceHostingAsset) + .build(); + + private EntityManagerWrapperFake emwFake = new EntityManagerWrapperFake(); + + @Spy + private EntityManagerWrapper emw = emwFake; + + @Spy + private ObjectMapper jsonMapper = new JsonObjectMapperConfiguration().customObjectMapper().build(); + + @Spy + private StandardMapper standardMapper = new StandardMapper(); + + @InjectMocks + private HsBookingItemCreatedListener listener; + + @BeforeEach + void initMocks() { + emwFake.persist(managedWebspaceHostingAsset); + emwFake.persist(unixUserHostingAsset); + } + + @Test + void doesNotPersistEventEntityWithoutValidationErrors() { + // given + final var givenBookingItem = HsBookingItemRealEntity.builder() + .type(HsBookingItemType.DOMAIN_SETUP) + .resources(Map.ofEntries( + entry("domainName", "example.org"), + entry("verificationCode", "just-a-fake-verification-code") + )) + .build(); + final var givenAssetJson = """ + { + "identifier": "example.org", // also as default for all subAssets + "subHostingAssets": [ + { + "type": "DOMAIN_HTTP_SETUP", + "assignedToAssetUuid": "{unixUserHostingAssetUuid}" + } + ] + } + """ + .replace("{unixUserHostingAssetUuid}", unixUserHostingAsset.getUuid().toString()); + Dns.fakeResultForDomain("example.org", + Dns.Result.fromRecords("Hostsharing-domain-setup-verification-code=just-a-fake-verification-code")); + + // when + listener.onApplicationEvent( + new BookingItemCreatedAppEvent(this, givenBookingItem, givenAssetJson) + ); + + // then + assertThat(emwFake.stream(BookingItemCreatedEventEntity.class).findAny().isEmpty()) + .as("the event should not have been persisted, but got persisted").isTrue(); + } + + @Test + void persistsEventEntityIfHostingAssetVerificationFails() { + // given + final var givenBookingItem = HsBookingItemRealEntity.builder() + .type(HsBookingItemType.DOMAIN_SETUP) + .resources(Map.ofEntries( + entry("domainName", "example.org") + )) + .build(); + final var givenAssetJson = """ + { + "identifier": "example.org", // also as default for all subAssets + "subHostingAssets": [ + { + "type": "DOMAIN_HTTP_SETUP", + "assignedToAssetUuid": "{unixUserHostingAssetUuid}" + } + ] + } + """ + .replace("{unixUserHostingAssetUuid}", unixUserHostingAsset.getUuid().toString()); + Dns.fakeResultForDomain("example.org", Dns.Result.fromRecords()); // without valid verificationCode + + // when + listener.onApplicationEvent( + new BookingItemCreatedAppEvent(this, givenBookingItem, givenAssetJson) + ); + + // then + emwFake.stream(BookingItemCreatedEventEntity.class) + .reduce(EntityManagerWrapperFake::toSingleElement) + .map(eventEntity -> { + assertThat(eventEntity.getBookingItem()).isSameAs(givenBookingItem); + assertThat(eventEntity.getAssetJson()).isEqualTo(givenAssetJson); + assertThat(eventEntity.getStatusMessage()).isEqualTo( + "[[DNS] no TXT record 'Hostsharing-domain-setup-verification-code=null' found for domain name 'example.org' (nor in its super-domain)]"); + return true; + }); + } +} diff --git a/src/test/java/net/hostsharing/hsadminng/persistence/EntityManagerWrapperFake.java b/src/test/java/net/hostsharing/hsadminng/persistence/EntityManagerWrapperFake.java new file mode 100644 index 00000000..fd05c371 --- /dev/null +++ b/src/test/java/net/hostsharing/hsadminng/persistence/EntityManagerWrapperFake.java @@ -0,0 +1,95 @@ +package net.hostsharing.hsadminng.persistence; + +import lombok.SneakyThrows; + +import jakarta.persistence.Id; +import java.lang.reflect.Field; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; +import java.util.UUID; +import java.util.stream.Stream; + + +public class EntityManagerWrapperFake extends EntityManagerWrapper { + + private Map, Map> entityClasses = new HashMap<>(); + + public static T toSingleElement(T last, T next) { + throw new AssertionError("only a single entity expected"); + } + + @Override + public boolean contains(final Object entity) { + final var id = getEntityId(entity); + return find(entity.getClass(), id) != null; + } + + @Override + public T find(final Class entityClass, final Object primaryKey) { + final var self = this; + if (entityClasses.containsKey(entityClass)) { + final var entities = entityClasses.get(entityClass); + //noinspection unchecked + return entities.keySet().stream() + .filter(key -> key.equals(primaryKey)) + .map(key -> (T) entities.get(key)) + .findAny() + .orElse(null); + } + return null; + } + + @Override + public void persist(final Object entity) { + if (!entityClasses.containsKey(entity.getClass())) { + entityClasses.put(entity.getClass(), new HashMap<>()); + } + final var id = getEntityId(entity).orElseGet(() -> setEntityId(entity, UUID.randomUUID())); + entityClasses.get(entity.getClass()).put(id, entity); + } + + @Override + public void flush() { + } + + public Stream stream() { + return entityClasses.values().stream().flatMap(entitiesPerClass -> entitiesPerClass.values().stream()); + } + + public Stream stream(final Class entityClass) { + if (entityClasses.containsKey(entityClass)) { + //noinspection unchecked + return (Stream) entityClasses.get(entityClass).values().stream(); + } + return Stream.empty(); + } + + @SneakyThrows + private static Optional getEntityId(final Object entity) { + for (Class currentClass = entity.getClass(); currentClass != null; currentClass = currentClass.getSuperclass()) { + for (Field field : currentClass.getDeclaredFields()) { + if (field.isAnnotationPresent(Id.class)) { + field.setAccessible(true); + return Optional.ofNullable(field.get(entity)); + } + } + } + throw new IllegalArgumentException("No @Id field found in entity class: " + entity.getClass().getName()); + } + + @SneakyThrows + private static Object setEntityId(final Object entity, final Object id) { + for (Class currentClass = entity.getClass(); currentClass != null; currentClass = currentClass.getSuperclass()) { + for (Field field : currentClass.getDeclaredFields()) { + if (field.isAnnotationPresent(Id.class)) { + field.setAccessible(true); + field.set(entity, id); + return id; + } + } + } + throw new IllegalArgumentException("No @Id field found in entity class: " + entity.getClass().getName()); + } + +} -- 2.39.5 From 3928c402587f7b4ece18ebb6fcd7ba493f4b3d44 Mon Sep 17 00:00:00 2001 From: Michael Hoennig Date: Wed, 2 Oct 2024 09:58:01 +0200 Subject: [PATCH 14/31] add persistsEventEntityIfDomainDnsSetupIsSupplied test --- .../DomainSetupHostingAssetFactory.java | 8 ++ .../asset/factories/HostingAssetFactory.java | 2 +- ...mainSetupHostingAssetFactoryUnitTest.java} | 80 +++++++++++++++---- 3 files changed, 73 insertions(+), 17 deletions(-) rename src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/factories/{HsBookingItemCreatedListenerUnitTest.java => DomainSetupHostingAssetFactoryUnitTest.java} (64%) diff --git a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/factories/DomainSetupHostingAssetFactory.java b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/factories/DomainSetupHostingAssetFactory.java index 6421aa1d..f1093311 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/factories/DomainSetupHostingAssetFactory.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/factories/DomainSetupHostingAssetFactory.java @@ -16,6 +16,7 @@ import java.net.IDN; import java.util.List; import java.util.function.Function; +import static net.hostsharing.hsadminng.hs.booking.generated.api.v1.model.HsHostingAssetTypeResource.DOMAIN_DNS_SETUP; import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.DOMAIN_HTTP_SETUP; import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.DOMAIN_MBOX_SETUP; import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.DOMAIN_SMTP_SETUP; @@ -43,6 +44,7 @@ public class DomainSetupHostingAssetFactory extends HostingAssetFactory { final var assignedToUnixUserAsset = domainHttpSetupAsset.getAssignedToAsset(); // TODO.legacy: don't create DNS setup as long as we need to remain support legacy web-ui etc. + assertDomainDnsSetupAssetNotSupplied(domainSetupAsset); createDomainSubSetupAssetEntity( domainSetupAsset, @@ -96,6 +98,12 @@ public class DomainSetupHostingAssetFactory extends HostingAssetFactory { return domainHttpSetupAsset; } + private void assertDomainDnsSetupAssetNotSupplied(final HsHostingAssetRealEntity domainSetupAsset) { + if (getSubHostingAssetResources().stream().anyMatch(ha -> ha.getType() == DOMAIN_DNS_SETUP)) { + throw new ValidationException("domain DNS setup not allowed for legacy compatibility"); + } + } + private void createDomainSubSetupAssetEntity( final HsHostingAssetRealEntity domainSetupAsset, final HsHostingAssetType subAssetType, diff --git a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/factories/HostingAssetFactory.java b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/factories/HostingAssetFactory.java index cf30a5b0..83984bb0 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/factories/HostingAssetFactory.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/factories/HostingAssetFactory.java @@ -21,8 +21,8 @@ abstract class HostingAssetFactory { protected abstract HsHostingAsset create(); public String performSaveProcess() { - final var newHostingAsset = create(); try { + final var newHostingAsset = create(); persist(newHostingAsset); return null; } catch (final Exception e) { diff --git a/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/factories/HsBookingItemCreatedListenerUnitTest.java b/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/factories/DomainSetupHostingAssetFactoryUnitTest.java similarity index 64% rename from src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/factories/HsBookingItemCreatedListenerUnitTest.java rename to src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/factories/DomainSetupHostingAssetFactoryUnitTest.java index 8ae27c5a..9220f509 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/factories/HsBookingItemCreatedListenerUnitTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/factories/DomainSetupHostingAssetFactoryUnitTest.java @@ -26,8 +26,9 @@ import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.UNIX import static net.hostsharing.hsadminng.mapper.PatchableMapWrapper.entry; import static org.assertj.core.api.Assertions.assertThat; +// Tests the DomainSetupHostingAssetFactory through a HsBookingItemCreatedListener instance. @ExtendWith(MockitoExtension.class) -class HsBookingItemCreatedListenerUnitTest { +class DomainSetupHostingAssetFactoryUnitTest { private final HsHostingAssetRealEntity managedWebspaceHostingAsset = HsHostingAssetRealEntity.builder() .uuid(UUID.randomUUID()) @@ -63,13 +64,10 @@ class HsBookingItemCreatedListenerUnitTest { @Test void doesNotPersistEventEntityWithoutValidationErrors() { // given - final var givenBookingItem = HsBookingItemRealEntity.builder() - .type(HsBookingItemType.DOMAIN_SETUP) - .resources(Map.ofEntries( - entry("domainName", "example.org"), - entry("verificationCode", "just-a-fake-verification-code") - )) - .build(); + final var givenBookingItem = createBookingItemFromResources( + entry("domainName", "example.org"), + entry("verificationCode", "just-a-fake-verification-code") + ); final var givenAssetJson = """ { "identifier": "example.org", // also as default for all subAssets @@ -96,14 +94,11 @@ class HsBookingItemCreatedListenerUnitTest { } @Test - void persistsEventEntityIfHostingAssetVerificationFails() { + void persistsEventEntityIfDomainSetupVerificationFails() { // given - final var givenBookingItem = HsBookingItemRealEntity.builder() - .type(HsBookingItemType.DOMAIN_SETUP) - .resources(Map.ofEntries( - entry("domainName", "example.org") - )) - .build(); + final var givenBookingItem = createBookingItemFromResources( + entry("domainName", "example.org") + ); final var givenAssetJson = """ { "identifier": "example.org", // also as default for all subAssets @@ -130,8 +125,61 @@ class HsBookingItemCreatedListenerUnitTest { assertThat(eventEntity.getBookingItem()).isSameAs(givenBookingItem); assertThat(eventEntity.getAssetJson()).isEqualTo(givenAssetJson); assertThat(eventEntity.getStatusMessage()).isEqualTo( - "[[DNS] no TXT record 'Hostsharing-domain-setup-verification-code=null' found for domain name 'example.org' (nor in its super-domain)]"); + "[[DNS] no TXT record 'Hostsharing-domain-setup-verification-code=null' found for domain name 'example.org' (nor in its super-domain)]" + ); return true; }); } + + @Test + void persistsEventEntityIfDomainDnsSetupIsSupplied() { + // given + final var givenBookingItem = createBookingItemFromResources( + entry("domainName", "example.org"), + entry("verificationCode", "just-a-fake-verification-code") + ); + final var givenAssetJson = """ + { + "identifier": "example.org", // also as default for all subAssets + "subHostingAssets": [ + { + "type": "DOMAIN_HTTP_SETUP", + "assignedToAssetUuid": "{unixUserHostingAssetUuid}" + }, + { + "type": "DOMAIN_DNS_SETUP", + "assignedToAssetUuid": "{managedWebspaceHostingAssetUuid}" + } + ] + } + """ + .replace("{unixUserHostingAssetUuid}", unixUserHostingAsset.getUuid().toString()) + .replace("{managedWebspaceHostingAssetUuid}", managedWebspaceHostingAsset.getUuid().toString()); + Dns.fakeResultForDomain("example.org", + Dns.Result.fromRecords("Hostsharing-domain-setup-verification-code=just-a-fake-verification-code")); + + // when + listener.onApplicationEvent( + new BookingItemCreatedAppEvent(this, givenBookingItem, givenAssetJson) + ); + + // then + emwFake.stream(BookingItemCreatedEventEntity.class) + .reduce(EntityManagerWrapperFake::toSingleElement) + .map(eventEntity -> { + assertThat(eventEntity.getBookingItem()).isSameAs(givenBookingItem); + assertThat(eventEntity.getAssetJson()).isEqualTo(givenAssetJson); + assertThat(eventEntity.getStatusMessage()).isEqualTo( + "domain DNS setup not allowed for legacy compatibility"); + return true; + }); + } + + @SafeVarargs + private static HsBookingItemRealEntity createBookingItemFromResources(final Map.Entry... givenResources) { + return HsBookingItemRealEntity.builder() + .type(HsBookingItemType.DOMAIN_SETUP) + .resources(Map.ofEntries(givenResources)) + .build(); + } } -- 2.39.5 From 28c56a4fd6e062109beb7d89e36e9c5fdceed5a3 Mon Sep 17 00:00:00 2001 From: Michael Hoennig Date: Wed, 2 Oct 2024 12:12:04 +0200 Subject: [PATCH 15/31] extract assertEventStatus --- ...omainSetupHostingAssetFactoryUnitTest.java | 39 ++++++++++--------- 1 file changed, 20 insertions(+), 19 deletions(-) diff --git a/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/factories/DomainSetupHostingAssetFactoryUnitTest.java b/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/factories/DomainSetupHostingAssetFactoryUnitTest.java index 9220f509..5a846260 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/factories/DomainSetupHostingAssetFactoryUnitTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/factories/DomainSetupHostingAssetFactoryUnitTest.java @@ -119,16 +119,8 @@ class DomainSetupHostingAssetFactoryUnitTest { ); // then - emwFake.stream(BookingItemCreatedEventEntity.class) - .reduce(EntityManagerWrapperFake::toSingleElement) - .map(eventEntity -> { - assertThat(eventEntity.getBookingItem()).isSameAs(givenBookingItem); - assertThat(eventEntity.getAssetJson()).isEqualTo(givenAssetJson); - assertThat(eventEntity.getStatusMessage()).isEqualTo( - "[[DNS] no TXT record 'Hostsharing-domain-setup-verification-code=null' found for domain name 'example.org' (nor in its super-domain)]" - ); - return true; - }); + assertEventStatus(givenBookingItem, givenAssetJson, + "[[DNS] no TXT record 'Hostsharing-domain-setup-verification-code=null' found for domain name 'example.org' (nor in its super-domain)]"); } @Test @@ -164,15 +156,8 @@ class DomainSetupHostingAssetFactoryUnitTest { ); // then - emwFake.stream(BookingItemCreatedEventEntity.class) - .reduce(EntityManagerWrapperFake::toSingleElement) - .map(eventEntity -> { - assertThat(eventEntity.getBookingItem()).isSameAs(givenBookingItem); - assertThat(eventEntity.getAssetJson()).isEqualTo(givenAssetJson); - assertThat(eventEntity.getStatusMessage()).isEqualTo( - "domain DNS setup not allowed for legacy compatibility"); - return true; - }); + assertEventStatus(givenBookingItem, givenAssetJson, + "domain DNS setup not allowed for legacy compatibility"); } @SafeVarargs @@ -182,4 +167,20 @@ class DomainSetupHostingAssetFactoryUnitTest { .resources(Map.ofEntries(givenResources)) .build(); } + + private void assertEventStatus( + final HsBookingItemRealEntity givenBookingItem, + final String givenAssetJson, + final String expected) { + emwFake.stream(BookingItemCreatedEventEntity.class) + .reduce(EntityManagerWrapperFake::toSingleElement) + .map(eventEntity -> { + assertThat(eventEntity.getBookingItem()).isSameAs(givenBookingItem); + assertThat(eventEntity.getAssetJson()).isEqualTo(givenAssetJson); + assertThat(eventEntity.getStatusMessage()).isEqualTo( + expected + ); + return true; + }); + } } -- 2.39.5 From 3dbcdc6b010b96d38ca8f170328cb7f2a4692da3 Mon Sep 17 00:00:00 2001 From: Michael Hoennig Date: Wed, 2 Oct 2024 12:12:40 +0200 Subject: [PATCH 16/31] add TODO.impl domain setup to inaccessible users should not be allowed --- .../hs/booking/item/HsBookingItemControllerAcceptanceTest.java | 1 + 1 file changed, 1 insertion(+) diff --git a/src/test/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemControllerAcceptanceTest.java b/src/test/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemControllerAcceptanceTest.java index db62357a..270e7917 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemControllerAcceptanceTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemControllerAcceptanceTest.java @@ -189,6 +189,7 @@ class HsBookingItemControllerAcceptanceTest extends ContextBasedTestWithCleanup context.define("superuser-alex@hostsharing.net", "hs_booking.project#D-1000111-D-1000111defaultproject:AGENT"); final var givenProject = findDefaultProjectOfDebitorNumber(1000111); + // TODO.impl: "sec01-web" should not work, but does final var givenUnixUser = realHostingAssetRepo.findByTypeAndIdentifier(UNIX_USER, "fir01-web").stream() .findFirst().orElseThrow(); -- 2.39.5 From 7f066f8b6a3ee1f4310f10008f4bcc78ca187fb4 Mon Sep 17 00:00:00 2001 From: Michael Hoennig Date: Wed, 2 Oct 2024 16:37:54 +0200 Subject: [PATCH 17/31] WIP --- .../DomainSetupHostingAssetFactory.java | 36 ++++++++++--- .../hostsharing/hsadminng/lambda/Reducer.java | 8 +++ .../hs-hosting/hs-hosting-asset-schemas.yaml | 6 +-- ...HsBookingItemControllerAcceptanceTest.java | 45 +++++++++------- ...omainSetupHostingAssetFactoryUnitTest.java | 54 +++++++++++++++++-- .../persistence/EntityManagerWrapperFake.java | 4 -- 6 files changed, 115 insertions(+), 38 deletions(-) create mode 100644 src/main/java/net/hostsharing/hsadminng/lambda/Reducer.java diff --git a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/factories/DomainSetupHostingAssetFactory.java b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/factories/DomainSetupHostingAssetFactory.java index f1093311..248d3bee 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/factories/DomainSetupHostingAssetFactory.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/factories/DomainSetupHostingAssetFactory.java @@ -8,14 +8,17 @@ import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAsset; import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetRealEntity; import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType; import net.hostsharing.hsadminng.hs.office.contact.HsOfficeContactRealEntity; +import net.hostsharing.hsadminng.lambda.Reducer; import net.hostsharing.hsadminng.mapper.StandardMapper; import net.hostsharing.hsadminng.persistence.EntityManagerWrapper; import jakarta.validation.ValidationException; import java.net.IDN; import java.util.List; +import java.util.Optional; import java.util.function.Function; +import static java.util.Optional.ofNullable; import static net.hostsharing.hsadminng.hs.booking.generated.api.v1.model.HsHostingAssetTypeResource.DOMAIN_DNS_SETUP; import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.DOMAIN_HTTP_SETUP; import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.DOMAIN_MBOX_SETUP; @@ -109,14 +112,31 @@ public class DomainSetupHostingAssetFactory extends HostingAssetFactory { final HsHostingAssetType subAssetType, final Function, HsHostingAssetRealEntity.HsHostingAssetRealEntityBuilder> builderTransformer) { final var resourceType = HsHostingAssetTypeResource.valueOf(subAssetType.name()); - if (getSubHostingAssetResources().stream().noneMatch(ha -> ha.getType() == resourceType)) { - final var subAssetEntity = builderTransformer.apply( - HsHostingAssetRealEntity.builder() - .type(HsHostingAssetType.valueOf(subAssetType.name())) - .parentAsset(domainSetupAsset)) - .build(); - domainSetupAsset.getSubHostingAssets().add(subAssetEntity); - } + final var subAssetResource = getSubHostingAssetResources().stream() + .filter(ha -> ha.getType() == resourceType) + .reduce(Reducer::toSingleElement); + final var subAssetEntity = builderTransformer.apply( + HsHostingAssetRealEntity.builder() + .type(subAssetType) + .parentAsset(domainSetupAsset)) + .build(); + domainSetupAsset.getSubHostingAssets().add(subAssetEntity); + subAssetResource.ifPresent( + res -> { + ofNullable(res.getAssignedToAssetUuid()) + .map(uuid -> emw.find(HsHostingAssetRealEntity.class, uuid)) + .ifPresent(subAssetEntity::setAssignedToAsset); + ofNullable(res.getAlarmContactUuid()) + .map(uuid -> emw.find(HsOfficeContactRealEntity.class, uuid)) + .ifPresent(subAssetEntity::setAlarmContact); + ofNullable(res.getIdentifier()).ifPresent(subAssetEntity::setIdentifier); + ofNullable(res.getCaption()).ifPresent(subAssetEntity::setCaption); + res.getConfig(); + res.get + + + } + ); } private String getDomainName() { diff --git a/src/main/java/net/hostsharing/hsadminng/lambda/Reducer.java b/src/main/java/net/hostsharing/hsadminng/lambda/Reducer.java new file mode 100644 index 00000000..52b4df79 --- /dev/null +++ b/src/main/java/net/hostsharing/hsadminng/lambda/Reducer.java @@ -0,0 +1,8 @@ +package net.hostsharing.hsadminng.lambda; + +public class Reducer { + public static T toSingleElement(T last, T next) { + throw new AssertionError("only a single entity expected"); + } + +} diff --git a/src/main/resources/api-definition/hs-hosting/hs-hosting-asset-schemas.yaml b/src/main/resources/api-definition/hs-hosting/hs-hosting-asset-schemas.yaml index 9ab6cf07..44813162 100644 --- a/src/main/resources/api-definition/hs-hosting/hs-hosting-asset-schemas.yaml +++ b/src/main/resources/api-definition/hs-hosting/hs-hosting-asset-schemas.yaml @@ -135,9 +135,6 @@ components: HsHostingAssetSubInsert: type: object properties: - assignedToAssetUuid: - type: string - format: uuid type: $ref: '#/components/schemas/HsHostingAssetType' identifier: @@ -150,6 +147,9 @@ components: minLength: 3 maxLength: 80 nullable: false + assignedToAssetUuid: + type: string + format: uuid alarmContactUuid: type: string format: uuid diff --git a/src/test/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemControllerAcceptanceTest.java b/src/test/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemControllerAcceptanceTest.java index 270e7917..15855fc9 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemControllerAcceptanceTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemControllerAcceptanceTest.java @@ -201,24 +201,33 @@ class HsBookingItemControllerAcceptanceTest extends ContextBasedTestWithCleanup .header("current-subject", "superuser-alex@hostsharing.net") .contentType(ContentType.JSON) .body(""" - { - "projectUuid": "{projectUuid}", - "type": "DOMAIN_SETUP", - "caption": "Domain-Setup for example.org", - "resources": { - "domainName": "example.org", - "verificationCode": "just-a-fake-verification-code" - }, - "asset": { // FIXME: rename to hostingAsset - "identifier": "example.org", // also as default for all subAssets - "subHostingAssets": [ - { - "type": "DOMAIN_HTTP_SETUP", - "assignedToAssetUuid": "{unixUserUuid}" - } - ] - } - } +{ + "projectUuid": "{projectUuid}", + "type": "DOMAIN_SETUP", + "caption": "Domain-Setup for example.org", + "resources": { + "domainName": "example.org", + "verificationCode": "just-a-fake-verification-code" + }, + "asset": { // FIXME: rename to hostingAsset + "identifier": "example.org", // also as default for all subAssets + "subHostingAssets": [ + { + "type": "DOMAIN_DNS_SETUP" + }, + { + "type": "DOMAIN_HTTP_SETUP", + "assignedToAssetUuid": "{unixUserUuid}" + }, + { + "type": "DOMAIN_MBOX_SETUP" + }, + { + "type": "DOMAIN_SMTP_SETUP" + } + ] + } +} """ .replace("{projectUuid}", givenProject.getUuid().toString()) .replace("{unixUserUuid}", givenUnixUser.getUuid().toString()) diff --git a/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/factories/DomainSetupHostingAssetFactoryUnitTest.java b/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/factories/DomainSetupHostingAssetFactoryUnitTest.java index 5a846260..97c70952 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/factories/DomainSetupHostingAssetFactoryUnitTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/factories/DomainSetupHostingAssetFactoryUnitTest.java @@ -6,6 +6,7 @@ import net.hostsharing.hsadminng.hs.booking.item.HsBookingItemRealEntity; import net.hostsharing.hsadminng.hs.booking.item.HsBookingItemType; import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetRealEntity; import net.hostsharing.hsadminng.hs.hosting.asset.validators.Dns; +import net.hostsharing.hsadminng.lambda.Reducer; import net.hostsharing.hsadminng.persistence.EntityManagerWrapperFake; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -33,14 +34,22 @@ class DomainSetupHostingAssetFactoryUnitTest { private final HsHostingAssetRealEntity managedWebspaceHostingAsset = HsHostingAssetRealEntity.builder() .uuid(UUID.randomUUID()) .type(MANAGED_WEBSPACE) + .identifier("one00") .build(); private final HsHostingAssetRealEntity unixUserHostingAsset = HsHostingAssetRealEntity.builder() .uuid(UUID.randomUUID()) .type(UNIX_USER) + .identifier("one00-web") .parentAsset(managedWebspaceHostingAsset) .build(); + private final HsHostingAssetRealEntity anotherManagedWebspaceHostingAsset = HsHostingAssetRealEntity.builder() + .uuid(UUID.randomUUID()) + .type(MANAGED_WEBSPACE) + .identifier("two00") + .build(); + private EntityManagerWrapperFake emwFake = new EntityManagerWrapperFake(); @Spy @@ -160,6 +169,43 @@ class DomainSetupHostingAssetFactoryUnitTest { "domain DNS setup not allowed for legacy compatibility"); } + @Test + void persistsEventEntityIfSuppliedDomainUnixUserAndSmtpSetupWebspaceDontMatch() { + // given + final var givenBookingItem = createBookingItemFromResources( + entry("domainName", "example.org"), + entry("verificationCode", "just-a-fake-verification-code") + ); + final var givenAssetJson = """ + { + "identifier": "example.org", // also as default for all subAssets + "subHostingAssets": [ + { + "type": "DOMAIN_HTTP_SETUP", + "assignedToAssetUuid": "{unixUserHostingAssetUuid}" + }, + { + "type": "DOMAIN_SMTP_SETUP", + "assignedToAssetUuid": "{managedWebspaceHostingAssetUuid}" + } + ] + } + """ + .replace("{unixUserHostingAssetUuid}", unixUserHostingAsset.getUuid().toString()) + .replace("{managedWebspaceHostingAssetUuid}", anotherManagedWebspaceHostingAsset.getUuid().toString()); + Dns.fakeResultForDomain("example.org", + Dns.Result.fromRecords("Hostsharing-domain-setup-verification-code=just-a-fake-verification-code")); + + // when + listener.onApplicationEvent( + new BookingItemCreatedAppEvent(this, givenBookingItem, givenAssetJson) + ); + + // then + assertEventStatus(givenBookingItem, givenAssetJson, + "domain DNS setup not allowed for legacy compatibility"); + } + @SafeVarargs private static HsBookingItemRealEntity createBookingItemFromResources(final Map.Entry... givenResources) { return HsBookingItemRealEntity.builder() @@ -171,15 +217,13 @@ class DomainSetupHostingAssetFactoryUnitTest { private void assertEventStatus( final HsBookingItemRealEntity givenBookingItem, final String givenAssetJson, - final String expected) { + final String expectedErrorMessage) { emwFake.stream(BookingItemCreatedEventEntity.class) - .reduce(EntityManagerWrapperFake::toSingleElement) + .reduce(Reducer::toSingleElement) .map(eventEntity -> { assertThat(eventEntity.getBookingItem()).isSameAs(givenBookingItem); assertThat(eventEntity.getAssetJson()).isEqualTo(givenAssetJson); - assertThat(eventEntity.getStatusMessage()).isEqualTo( - expected - ); + assertThat(eventEntity.getStatusMessage()).isEqualTo(expectedErrorMessage); return true; }); } diff --git a/src/test/java/net/hostsharing/hsadminng/persistence/EntityManagerWrapperFake.java b/src/test/java/net/hostsharing/hsadminng/persistence/EntityManagerWrapperFake.java index fd05c371..4644677a 100644 --- a/src/test/java/net/hostsharing/hsadminng/persistence/EntityManagerWrapperFake.java +++ b/src/test/java/net/hostsharing/hsadminng/persistence/EntityManagerWrapperFake.java @@ -15,10 +15,6 @@ public class EntityManagerWrapperFake extends EntityManagerWrapper { private Map, Map> entityClasses = new HashMap<>(); - public static T toSingleElement(T last, T next) { - throw new AssertionError("only a single entity expected"); - } - @Override public boolean contains(final Object entity) { final var id = getEntityId(entity); -- 2.39.5 From 4c4cd886bca7a0d0d82c77c21a6e8a0a34096bee Mon Sep 17 00:00:00 2001 From: Michael Hoennig Date: Fri, 4 Oct 2024 17:19:37 +0200 Subject: [PATCH 18/31] WIP --- .../DomainSetupHostingAssetFactory.java | 124 +++++++++--------- .../asset/factories/ToStringConverter.java | 39 ++++++ ...HsBookingItemControllerAcceptanceTest.java | 54 ++++---- 3 files changed, 128 insertions(+), 89 deletions(-) create mode 100644 src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/factories/ToStringConverter.java diff --git a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/factories/DomainSetupHostingAssetFactory.java b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/factories/DomainSetupHostingAssetFactory.java index 248d3bee..3babaf41 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/factories/DomainSetupHostingAssetFactory.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/factories/DomainSetupHostingAssetFactory.java @@ -15,11 +15,9 @@ import net.hostsharing.hsadminng.persistence.EntityManagerWrapper; import jakarta.validation.ValidationException; import java.net.IDN; import java.util.List; -import java.util.Optional; import java.util.function.Function; -import static java.util.Optional.ofNullable; -import static net.hostsharing.hsadminng.hs.booking.generated.api.v1.model.HsHostingAssetTypeResource.DOMAIN_DNS_SETUP; +import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.DOMAIN_DNS_SETUP; import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.DOMAIN_HTTP_SETUP; import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.DOMAIN_MBOX_SETUP; import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.DOMAIN_SMTP_SETUP; @@ -37,33 +35,62 @@ public class DomainSetupHostingAssetFactory extends HostingAssetFactory { @Override protected HsHostingAsset create() { final var domainSetupAsset = createDomainSetupAsset(getDomainName()); + final var subHostingAssets = domainSetupAsset.getSubHostingAssets(); // TODO.legacy: as long as we need to be compatible, we always do all technical domain-setups + final var subHostingAssetResources = getSubHostingAssetResources(); + + subHostingAssets.add( + createDomainSubSetupAssetEntity( + domainSetupAsset, + DOMAIN_HTTP_SETUP, + builder -> builder + .assignedToAsset(assignedToUnixUserAsset.getParentAsset()) + .identifier(getDomainName() + "|MBOX") + .caption("HTTP-Setup für " + IDN.toUnicode(getDomainName()))) + ); + + domainHttpSetupAsset.setParentAsset(domainSetupAsset); + final var assignedToUnixUserAsset = + emw.find(HsHostingAssetRealEntity.class, domainHttpSetupAssetResource.getAssignedToAssetUuid()); + domainHttpSetupAsset.setAssignedToAsset(assignedToUnixUserAsset); + + final var domainHttpSetupAsset = createDomainHttpSetupAssetEntity( subHostingAssetResources, getDomainName(), domainSetupAsset); final var assignedToUnixUserAsset = domainHttpSetupAsset.getAssignedToAsset(); - // TODO.legacy: don't create DNS setup as long as we need to remain support legacy web-ui etc. - assertDomainDnsSetupAssetNotSupplied(domainSetupAsset); - + // do not add to subHostingAssets, in compatibility mode, DNS setup works via file system createDomainSubSetupAssetEntity( domainSetupAsset, - DOMAIN_MBOX_SETUP, + DOMAIN_DNS_SETUP, builder -> builder .assignedToAsset(assignedToUnixUserAsset.getParentAsset()) - .identifier(getDomainName() + "|MBOX") + .identifier(getDomainName() + "|DNS") .caption("HTTP-Setup für " + IDN.toUnicode(getDomainName()))); - createDomainSubSetupAssetEntity( - domainSetupAsset, - DOMAIN_SMTP_SETUP, - builder -> builder - .assignedToAsset(assignedToUnixUserAsset.getParentAsset()) - .identifier(getDomainName() + "|SMTP") - .caption("HTTP-Setup für " + IDN.toUnicode(getDomainName()))); + subHostingAssets.add( + createDomainSubSetupAssetEntity( + domainSetupAsset, + DOMAIN_MBOX_SETUP, + builder -> builder + .assignedToAsset(assignedToUnixUserAsset.getParentAsset()) + .identifier(getDomainName() + "|MBOX") + .caption("HTTP-Setup für " + IDN.toUnicode(getDomainName()))) + ); + + subHostingAssets.add( + createDomainSubSetupAssetEntity( + domainSetupAsset, + DOMAIN_SMTP_SETUP, + builder -> builder + .assignedToAsset(assignedToUnixUserAsset.getParentAsset()) + .identifier(getDomainName() + "|SMTP") + .caption("HTTP-Setup für " + IDN.toUnicode(getDomainName()))) + ); return domainSetupAsset; } @@ -75,68 +102,41 @@ public class DomainSetupHostingAssetFactory extends HostingAssetFactory { .identifier(domainName) .caption(asset.getCaption() != null ? asset.getCaption() : domainName) .alarmContact(ref(HsOfficeContactRealEntity.class, asset.getAlarmContactUuid())) - .subHostingAssets( // FIXME: is this even used? - standardMapper.mapList(getSubHostingAssetResources(), HsHostingAssetRealEntity.class) - ) + // the sub-hosting-assets get added later .build(); } - private HsHostingAssetRealEntity createDomainHttpSetupAssetEntity( - final List subHostingAssetResources, - final String domainName, - final HsHostingAssetRealEntity domainSetupAsset) { - final var domainHttpSetupAssetResource = subHostingAssetResources.stream() - .filter(ha -> ha.getType() == HsHostingAssetTypeResource.DOMAIN_HTTP_SETUP) - .findFirst().orElseThrow(() -> new ValidationException( - domainName + ": missing target unix user (assignedToHostingAssetUuid) for DOMAIN_HTTP_SETUP ")); - final var domainHttpSetupAsset = domainSetupAsset.getSubHostingAssets() - .stream() - .filter(sha -> sha.getType() == DOMAIN_HTTP_SETUP) - .findFirst() - .orElseThrow(); - domainHttpSetupAsset.setParentAsset(domainSetupAsset); - final var assignedToUnixUserAsset = - emw.find(HsHostingAssetRealEntity.class, domainHttpSetupAssetResource.getAssignedToAssetUuid()); - domainHttpSetupAsset.setAssignedToAsset(assignedToUnixUserAsset); - return domainHttpSetupAsset; - } - - private void assertDomainDnsSetupAssetNotSupplied(final HsHostingAssetRealEntity domainSetupAsset) { - if (getSubHostingAssetResources().stream().anyMatch(ha -> ha.getType() == DOMAIN_DNS_SETUP)) { - throw new ValidationException("domain DNS setup not allowed for legacy compatibility"); - } - } - - private void createDomainSubSetupAssetEntity( + private HsHostingAssetRealEntity createDomainSubSetupAssetEntity( final HsHostingAssetRealEntity domainSetupAsset, final HsHostingAssetType subAssetType, final Function, HsHostingAssetRealEntity.HsHostingAssetRealEntityBuilder> builderTransformer) { final var resourceType = HsHostingAssetTypeResource.valueOf(subAssetType.name()); - final var subAssetResource = getSubHostingAssetResources().stream() + + final var subAssetResourceOptional = getSubHostingAssetResources().stream() .filter(ha -> ha.getType() == resourceType) .reduce(Reducer::toSingleElement); - final var subAssetEntity = builderTransformer.apply( + + subAssetResourceOptional.ifPresentOrElse( + subAssetResource -> verifyNotOverspecified(subAssetResource), + () -> { throw new ValidationException("sub-asset of type " + resourceType.name() + " required in legacy mode, but missing"); } + ); + + return builderTransformer.apply( HsHostingAssetRealEntity.builder() .type(subAssetType) .parentAsset(domainSetupAsset)) .build(); - domainSetupAsset.getSubHostingAssets().add(subAssetEntity); - subAssetResource.ifPresent( - res -> { - ofNullable(res.getAssignedToAssetUuid()) - .map(uuid -> emw.find(HsHostingAssetRealEntity.class, uuid)) - .ifPresent(subAssetEntity::setAssignedToAsset); - ofNullable(res.getAlarmContactUuid()) - .map(uuid -> emw.find(HsOfficeContactRealEntity.class, uuid)) - .ifPresent(subAssetEntity::setAlarmContact); - ofNullable(res.getIdentifier()).ifPresent(subAssetEntity::setIdentifier); - ofNullable(res.getCaption()).ifPresent(subAssetEntity::setCaption); - res.getConfig(); - res.get + } + // TODO.legacy: while we need to stay compatible, only default values can be used, thus only the type can be specified + private void verifyNotOverspecified(final HsHostingAssetSubInsertResource givenSubAssetResource) { + final var convert = new ToStringConverter().ignoring("assignedToAssetUuid"); + final var expectedSubAssetResource = new HsHostingAssetSubInsertResource(); + expectedSubAssetResource.setType(givenSubAssetResource.getType()); + if ( !convert.from(givenSubAssetResource).equals(convert.from(expectedSubAssetResource)) ) { + throw new ValidationException("sub asset " + givenSubAssetResource.getType() + " is over-specified, in compatibilty mode, only default values allowed"); + } - } - ); } private String getDomainName() { diff --git a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/factories/ToStringConverter.java b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/factories/ToStringConverter.java new file mode 100644 index 00000000..e436cfec --- /dev/null +++ b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/factories/ToStringConverter.java @@ -0,0 +1,39 @@ +package net.hostsharing.hsadminng.hs.hosting.asset.factories; +import net.hostsharing.hsadminng.mapper.Array; + +import java.lang.reflect.Field; +import java.util.Arrays; +import java.util.HashSet; +import java.util.Objects; +import java.util.Set; + +import static java.util.stream.Collectors.joining; + +public class ToStringConverter { + + final public Set ignoredFields = new HashSet<>(); + + public ToStringConverter ignoring(final String fieldName) { + ignoredFields.add(fieldName); + return this; + } + + public String from(Object obj) { + StringBuilder result = new StringBuilder(); + return "{ " + + Arrays.stream(obj.getClass().getDeclaredFields()) + .filter(f -> !ignoredFields.contains(f.getName())) + .map(field -> { + try { + field.setAccessible(true); + return field.getName() + ": " + field.get(obj); + } catch (IllegalAccessException e) { + // ignore inaccessible fields + return null; + } + }) + .filter(Objects::nonNull) + .collect(joining(", ")) + + " }"; + } +} diff --git a/src/test/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemControllerAcceptanceTest.java b/src/test/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemControllerAcceptanceTest.java index 15855fc9..d55b22bb 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemControllerAcceptanceTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemControllerAcceptanceTest.java @@ -201,33 +201,33 @@ class HsBookingItemControllerAcceptanceTest extends ContextBasedTestWithCleanup .header("current-subject", "superuser-alex@hostsharing.net") .contentType(ContentType.JSON) .body(""" -{ - "projectUuid": "{projectUuid}", - "type": "DOMAIN_SETUP", - "caption": "Domain-Setup for example.org", - "resources": { - "domainName": "example.org", - "verificationCode": "just-a-fake-verification-code" - }, - "asset": { // FIXME: rename to hostingAsset - "identifier": "example.org", // also as default for all subAssets - "subHostingAssets": [ - { - "type": "DOMAIN_DNS_SETUP" - }, - { - "type": "DOMAIN_HTTP_SETUP", - "assignedToAssetUuid": "{unixUserUuid}" - }, - { - "type": "DOMAIN_MBOX_SETUP" - }, - { - "type": "DOMAIN_SMTP_SETUP" - } - ] - } -} + { + "projectUuid": "{projectUuid}", + "type": "DOMAIN_SETUP", + "caption": "Domain-Setup for example.org", + "resources": { + "domainName": "example.org", + "verificationCode": "just-a-fake-verification-code" + }, + "asset": { // FIXME: rename to hostingAsset + "identifier": "example.org", // also as default for all subAssets + "subHostingAssets": [ + { + "type": "DOMAIN_DNS_SETUP" + }, + { + "type": "DOMAIN_HTTP_SETUP", + "assignedToAssetUuid": "{unixUserUuid}" + }, + { + "type": "DOMAIN_MBOX_SETUP" + }, + { + "type": "DOMAIN_SMTP_SETUP" + } + ] + } + } """ .replace("{projectUuid}", givenProject.getUuid().toString()) .replace("{unixUserUuid}", givenUnixUser.getUuid().toString()) -- 2.39.5 From 568602dfcb7a6af71d41e2b585234a4bed6be4dd Mon Sep 17 00:00:00 2001 From: Michael Hoennig Date: Sat, 5 Oct 2024 13:35:46 +0200 Subject: [PATCH 19/31] for legacy compatibility, require all domain-sub-setup-hosting-assets but mainly just the type --- .../DomainSetupHostingAssetFactory.java | 48 +++++++++---------- .../asset/factories/ToStringConverter.java | 2 - 2 files changed, 23 insertions(+), 27 deletions(-) diff --git a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/factories/DomainSetupHostingAssetFactory.java b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/factories/DomainSetupHostingAssetFactory.java index 3babaf41..d12e8dcf 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/factories/DomainSetupHostingAssetFactory.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/factories/DomainSetupHostingAssetFactory.java @@ -15,6 +15,7 @@ import net.hostsharing.hsadminng.persistence.EntityManagerWrapper; import jakarta.validation.ValidationException; import java.net.IDN; import java.util.List; +import java.util.Optional; import java.util.function.Function; import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.DOMAIN_DNS_SETUP; @@ -39,47 +40,40 @@ public class DomainSetupHostingAssetFactory extends HostingAssetFactory { // TODO.legacy: as long as we need to be compatible, we always do all technical domain-setups - final var subHostingAssetResources = getSubHostingAssetResources(); + final var domainHttpSetupAssetResource = findSubHostingAssetResource(HsHostingAssetTypeResource.DOMAIN_HTTP_SETUP); + final var assignedToUnixUserAssetEntity = domainHttpSetupAssetResource + .map(HsHostingAssetSubInsertResource::getAssignedToAssetUuid) + .map(uuid -> emw.find(HsHostingAssetRealEntity.class, uuid)) + .orElseThrow(() -> new ValidationException("DOMAIN_HTTP_SETUP subAsset with assignedToAssetUuid required in compatibility mode")); subHostingAssets.add( createDomainSubSetupAssetEntity( domainSetupAsset, DOMAIN_HTTP_SETUP, builder -> builder - .assignedToAsset(assignedToUnixUserAsset.getParentAsset()) - .identifier(getDomainName() + "|MBOX") + .assignedToAsset(assignedToUnixUserAssetEntity) + .identifier(getDomainName() + "|HTTP") .caption("HTTP-Setup für " + IDN.toUnicode(getDomainName()))) ); - domainHttpSetupAsset.setParentAsset(domainSetupAsset); - final var assignedToUnixUserAsset = - emw.find(HsHostingAssetRealEntity.class, domainHttpSetupAssetResource.getAssignedToAssetUuid()); - domainHttpSetupAsset.setAssignedToAsset(assignedToUnixUserAsset); - - - final var domainHttpSetupAsset = createDomainHttpSetupAssetEntity( - subHostingAssetResources, - getDomainName(), - domainSetupAsset); - final var assignedToUnixUserAsset = domainHttpSetupAsset.getAssignedToAsset(); - - // do not add to subHostingAssets, in compatibility mode, DNS setup works via file system + // Do not add to subHostingAssets in compatibility mode, in this case, DNS setup works via file system. + // The entity is created just for validation purposes. createDomainSubSetupAssetEntity( domainSetupAsset, DOMAIN_DNS_SETUP, builder -> builder - .assignedToAsset(assignedToUnixUserAsset.getParentAsset()) + .assignedToAsset(assignedToUnixUserAssetEntity.getParentAsset()) .identifier(getDomainName() + "|DNS") - .caption("HTTP-Setup für " + IDN.toUnicode(getDomainName()))); + .caption("DNS-Setup für " + IDN.toUnicode(getDomainName()))); subHostingAssets.add( createDomainSubSetupAssetEntity( domainSetupAsset, DOMAIN_MBOX_SETUP, builder -> builder - .assignedToAsset(assignedToUnixUserAsset.getParentAsset()) + .assignedToAsset(assignedToUnixUserAssetEntity.getParentAsset()) .identifier(getDomainName() + "|MBOX") - .caption("HTTP-Setup für " + IDN.toUnicode(getDomainName()))) + .caption("MBOX-Setup für " + IDN.toUnicode(getDomainName()))) ); subHostingAssets.add( @@ -87,9 +81,9 @@ public class DomainSetupHostingAssetFactory extends HostingAssetFactory { domainSetupAsset, DOMAIN_SMTP_SETUP, builder -> builder - .assignedToAsset(assignedToUnixUserAsset.getParentAsset()) + .assignedToAsset(assignedToUnixUserAssetEntity.getParentAsset()) .identifier(getDomainName() + "|SMTP") - .caption("HTTP-Setup für " + IDN.toUnicode(getDomainName()))) + .caption("SMTP-Setup für " + IDN.toUnicode(getDomainName()))) ); return domainSetupAsset; @@ -112,9 +106,7 @@ public class DomainSetupHostingAssetFactory extends HostingAssetFactory { final Function, HsHostingAssetRealEntity.HsHostingAssetRealEntityBuilder> builderTransformer) { final var resourceType = HsHostingAssetTypeResource.valueOf(subAssetType.name()); - final var subAssetResourceOptional = getSubHostingAssetResources().stream() - .filter(ha -> ha.getType() == resourceType) - .reduce(Reducer::toSingleElement); + final var subAssetResourceOptional = findSubHostingAssetResource(resourceType); subAssetResourceOptional.ifPresentOrElse( subAssetResource -> verifyNotOverspecified(subAssetResource), @@ -128,6 +120,12 @@ public class DomainSetupHostingAssetFactory extends HostingAssetFactory { .build(); } + private Optional findSubHostingAssetResource(final HsHostingAssetTypeResource resourceType) { + return getSubHostingAssetResources().stream() + .filter(ha -> ha.getType() == resourceType) + .reduce(Reducer::toSingleElement); + } + // TODO.legacy: while we need to stay compatible, only default values can be used, thus only the type can be specified private void verifyNotOverspecified(final HsHostingAssetSubInsertResource givenSubAssetResource) { final var convert = new ToStringConverter().ignoring("assignedToAssetUuid"); diff --git a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/factories/ToStringConverter.java b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/factories/ToStringConverter.java index e436cfec..bf0ec002 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/factories/ToStringConverter.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/factories/ToStringConverter.java @@ -1,7 +1,5 @@ package net.hostsharing.hsadminng.hs.hosting.asset.factories; -import net.hostsharing.hsadminng.mapper.Array; -import java.lang.reflect.Field; import java.util.Arrays; import java.util.HashSet; import java.util.Objects; -- 2.39.5 From c66917f110d9054d72ae00edb14569c37d724fd1 Mon Sep 17 00:00:00 2001 From: Michael Hoennig Date: Sat, 5 Oct 2024 13:49:19 +0200 Subject: [PATCH 20/31] add domain-sub-assets to another test --- .../item/HsBookingItemControllerAcceptanceTest.java | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/test/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemControllerAcceptanceTest.java b/src/test/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemControllerAcceptanceTest.java index d55b22bb..21bafcaf 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemControllerAcceptanceTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemControllerAcceptanceTest.java @@ -302,9 +302,18 @@ class HsBookingItemControllerAcceptanceTest extends ContextBasedTestWithCleanup "asset": { // FIXME: rename to hostingAsset "identifier": "example.org", // also as default for all subAssets "subHostingAssets": [ + { + "type": "DOMAIN_DNS_SETUP" + }, { "type": "DOMAIN_HTTP_SETUP", "assignedToAssetUuid": "{unixUserUuid}" + }, + { + "type": "DOMAIN_MBOX_SETUP" + }, + { + "type": "DOMAIN_SMTP_SETUP" } ] } -- 2.39.5 From 2560c402f89432d1e0f5241f4145ddeddc84267d Mon Sep 17 00:00:00 2001 From: Michael Hoennig Date: Sat, 5 Oct 2024 14:06:39 +0200 Subject: [PATCH 21/31] add domain-sub-assets to more tests --- ...omainSetupHostingAssetFactoryUnitTest.java | 36 ++++++++++++++++--- 1 file changed, 32 insertions(+), 4 deletions(-) diff --git a/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/factories/DomainSetupHostingAssetFactoryUnitTest.java b/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/factories/DomainSetupHostingAssetFactoryUnitTest.java index 97c70952..8b6ae464 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/factories/DomainSetupHostingAssetFactoryUnitTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/factories/DomainSetupHostingAssetFactoryUnitTest.java @@ -84,6 +84,15 @@ class DomainSetupHostingAssetFactoryUnitTest { { "type": "DOMAIN_HTTP_SETUP", "assignedToAssetUuid": "{unixUserHostingAssetUuid}" + }, + { + "type": "DOMAIN_DNS_SETUP" + }, + { + "type": "DOMAIN_MBOX_SETUP" + }, + { + "type": "DOMAIN_SMTP_SETUP" } ] } @@ -115,6 +124,15 @@ class DomainSetupHostingAssetFactoryUnitTest { { "type": "DOMAIN_HTTP_SETUP", "assignedToAssetUuid": "{unixUserHostingAssetUuid}" + }, + { + "type": "DOMAIN_DNS_SETUP" + }, + { + "type": "DOMAIN_MBOX_SETUP" + }, + { + "type": "DOMAIN_SMTP_SETUP" } ] } @@ -148,8 +166,13 @@ class DomainSetupHostingAssetFactoryUnitTest { "assignedToAssetUuid": "{unixUserHostingAssetUuid}" }, { - "type": "DOMAIN_DNS_SETUP", - "assignedToAssetUuid": "{managedWebspaceHostingAssetUuid}" + "type": "DOMAIN_DNS_SETUP" + }, + { + "type": "DOMAIN_MBOX_SETUP" + }, + { + "type": "DOMAIN_SMTP_SETUP" } ] } @@ -185,8 +208,13 @@ class DomainSetupHostingAssetFactoryUnitTest { "assignedToAssetUuid": "{unixUserHostingAssetUuid}" }, { - "type": "DOMAIN_SMTP_SETUP", - "assignedToAssetUuid": "{managedWebspaceHostingAssetUuid}" + "type": "DOMAIN_DNS_SETUP" + }, + { + "type": "DOMAIN_MBOX_SETUP" + }, + { + "type": "DOMAIN_SMTP_SETUP" } ] } -- 2.39.5 From 41f5eabd329c6f5fa81d41b9083c4fc2611ceab6 Mon Sep 17 00:00:00 2001 From: Michael Hoennig Date: Sat, 5 Oct 2024 14:25:37 +0200 Subject: [PATCH 22/31] fix ArchitectureTest by adding package lambda --- .../java/net/hostsharing/hsadminng/arch/ArchitectureTest.java | 1 + 1 file changed, 1 insertion(+) diff --git a/src/test/java/net/hostsharing/hsadminng/arch/ArchitectureTest.java b/src/test/java/net/hostsharing/hsadminng/arch/ArchitectureTest.java index aecfc7a8..5041f2eb 100644 --- a/src/test/java/net/hostsharing/hsadminng/arch/ArchitectureTest.java +++ b/src/test/java/net/hostsharing/hsadminng/arch/ArchitectureTest.java @@ -43,6 +43,7 @@ public class ArchitectureTest { "..test.dom", "..context", "..hash", + "..lambda", "..generated..", "..persistence..", "..system..", -- 2.39.5 From f5b839cf5245e32b65324e09fa98a2e5230b3213 Mon Sep 17 00:00:00 2001 From: Michael Hoennig Date: Mon, 7 Oct 2024 09:56:43 +0200 Subject: [PATCH 23/31] tests for auto-creating hosting asset for managed webspace booking item --- .../item/BookingItemCreatedEventEntity.java | 2 + .../hs/hosting/asset/HsHostingAsset.java | 10 +- .../HsBookingItemCreatedListener.java | 16 +- .../ManagedWebspaceHostingAssetFactory.java | 44 ++++++ .../hostsharing/hsadminng/mapper/Mapper.java | 10 +- .../hsadminng/mapper/StandardMapper.java | 5 +- .../hsadminng/mapper/StrictMapper.java | 5 +- ...HsBookingItemControllerAcceptanceTest.java | 64 +++++++- ...omainSetupHostingAssetFactoryUnitTest.java | 7 +- ...edWebspaceHostingAssetFactoryUnitTest.java | 137 ++++++++++++++++++ ...OfficeMembershipEntityPatcherUnitTest.java | 6 +- .../persistence/EntityManagerWrapperFake.java | 7 +- 12 files changed, 293 insertions(+), 20 deletions(-) create mode 100644 src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/factories/ManagedWebspaceHostingAssetFactory.java create mode 100644 src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/factories/ManagedWebspaceHostingAssetFactoryUnitTest.java diff --git a/src/main/java/net/hostsharing/hsadminng/hs/booking/item/BookingItemCreatedEventEntity.java b/src/main/java/net/hostsharing/hsadminng/hs/booking/item/BookingItemCreatedEventEntity.java index 245b056f..19c6c208 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/booking/item/BookingItemCreatedEventEntity.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/booking/item/BookingItemCreatedEventEntity.java @@ -3,6 +3,7 @@ package net.hostsharing.hsadminng.hs.booking.item; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; +import lombok.ToString; import lombok.experimental.SuperBuilder; import net.hostsharing.hsadminng.rbac.object.BaseEntity; @@ -23,6 +24,7 @@ import java.util.UUID; @Table(schema = "hs_booking", name = "item_created_event") @SuperBuilder(toBuilder = true) @Getter +@ToString @NoArgsConstructor public class BookingItemCreatedEventEntity implements BaseEntity { @Id diff --git a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAsset.java b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAsset.java index 4510655c..63864065 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAsset.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAsset.java @@ -89,10 +89,9 @@ public abstract class HsHostingAsset implements Stringifyable, BaseEntity subHostingAssets = new ArrayList<>(); + private List subHostingAssets; @Column(name = "identifier") private String identifier; // e.g. vm1234, xyz00, example.org, xyz00_abc @@ -125,6 +124,13 @@ public abstract class HsHostingAsset implements Stringifyable, BaseEntity {configWrapper = newWrapper;}, config).assign(newConfig); } + public List getSubHostingAssets() { + if (subHostingAssets == null) { + subHostingAssets = new ArrayList<>(); + } + return subHostingAssets; + } + @Override public PatchableMapWrapper directProps() { return getConfig(); diff --git a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/factories/HsBookingItemCreatedListener.java b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/factories/HsBookingItemCreatedListener.java index 1b031a05..d388c61c 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/factories/HsBookingItemCreatedListener.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/factories/HsBookingItemCreatedListener.java @@ -1,5 +1,6 @@ package net.hostsharing.hsadminng.hs.hosting.asset.factories; +import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; import lombok.SneakyThrows; import net.hostsharing.hsadminng.hs.booking.generated.api.v1.model.HsHostingAssetAutoInsertResource; @@ -10,6 +11,7 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.ApplicationListener; import org.springframework.stereotype.Component; + @Component public class HsBookingItemCreatedListener implements ApplicationListener { @@ -24,12 +26,22 @@ public class HsBookingItemCreatedListener implements ApplicationListener null; // for now, no automatic HostingAsset possible - case MANAGED_WEBSPACE -> null; // FIXME: implement ManagedWebspace HostingAsset creation, where possible + case MANAGED_WEBSPACE -> new ManagedWebspaceHostingAssetFactory(emw, newBookingItemRealEntity, asset, standardMapper); case DOMAIN_SETUP -> new DomainSetupHostingAssetFactory(emw, newBookingItemRealEntity, asset, standardMapper); }; if (factory != null) { diff --git a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/factories/ManagedWebspaceHostingAssetFactory.java b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/factories/ManagedWebspaceHostingAssetFactory.java new file mode 100644 index 00000000..70752572 --- /dev/null +++ b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/factories/ManagedWebspaceHostingAssetFactory.java @@ -0,0 +1,44 @@ +package net.hostsharing.hsadminng.hs.hosting.asset.factories; + +import net.hostsharing.hsadminng.hs.booking.generated.api.v1.model.HsHostingAssetAutoInsertResource; +import net.hostsharing.hsadminng.hs.booking.generated.api.v1.model.HsHostingAssetTypeResource; +import net.hostsharing.hsadminng.hs.booking.item.HsBookingItemRealEntity; +import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAsset; +import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetRealEntity; +import net.hostsharing.hsadminng.mapper.StandardMapper; +import net.hostsharing.hsadminng.persistence.EntityManagerWrapper; + +import jakarta.validation.ValidationException; + +import static java.util.Optional.ofNullable; + +public class ManagedWebspaceHostingAssetFactory extends HostingAssetFactory { + + public ManagedWebspaceHostingAssetFactory( + final EntityManagerWrapper emw, + final HsBookingItemRealEntity newBookingItemRealEntity, + final HsHostingAssetAutoInsertResource asset, + final StandardMapper standardMapper) { + super(emw, newBookingItemRealEntity, asset, standardMapper); + } + + @Override + protected HsHostingAsset create() { + if (asset.getType() != HsHostingAssetTypeResource.MANAGED_WEBSPACE) { + throw new ValidationException("requires MANAGED_WEBSPACE hosting asset, but got " + + ofNullable(asset) + .map(HsHostingAssetAutoInsertResource::getType) + .map(Enum::name) + .orElse(null)); + } + final var managedWebspaceHostingAsset = standardMapper.map(asset, HsHostingAssetRealEntity.class); + managedWebspaceHostingAsset.setBookingItem(fromBookingItem); + + return managedWebspaceHostingAsset; + } + + @Override + protected void persist(final HsHostingAsset newManagedWebspaceHostingAsset) { + super.persist(newManagedWebspaceHostingAsset); + } +} diff --git a/src/main/java/net/hostsharing/hsadminng/mapper/Mapper.java b/src/main/java/net/hostsharing/hsadminng/mapper/Mapper.java index 21779a5c..a98b02e6 100644 --- a/src/main/java/net/hostsharing/hsadminng/mapper/Mapper.java +++ b/src/main/java/net/hostsharing/hsadminng/mapper/Mapper.java @@ -1,11 +1,11 @@ package net.hostsharing.hsadminng.mapper; +import net.hostsharing.hsadminng.persistence.EntityManagerWrapper; import org.modelmapper.ModelMapper; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.util.ReflectionUtils; -import jakarta.persistence.EntityManager; import jakarta.persistence.ManyToOne; -import jakarta.persistence.PersistenceContext; import jakarta.validation.ValidationException; import java.lang.reflect.Field; import java.util.List; @@ -21,10 +21,10 @@ import static net.hostsharing.hsadminng.errors.DisplayAs.DisplayName; */ abstract class Mapper extends ModelMapper { - @PersistenceContext - EntityManager em; + EntityManagerWrapper em; - Mapper() { + Mapper(@Autowired final EntityManagerWrapper em) { + this.em = em; getConfiguration().setAmbiguityIgnored(true); } diff --git a/src/main/java/net/hostsharing/hsadminng/mapper/StandardMapper.java b/src/main/java/net/hostsharing/hsadminng/mapper/StandardMapper.java index 42725d3d..3f6a851b 100644 --- a/src/main/java/net/hostsharing/hsadminng/mapper/StandardMapper.java +++ b/src/main/java/net/hostsharing/hsadminng/mapper/StandardMapper.java @@ -1,5 +1,7 @@ package net.hostsharing.hsadminng.mapper; +import net.hostsharing.hsadminng.persistence.EntityManagerWrapper; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; /** @@ -8,7 +10,8 @@ import org.springframework.stereotype.Component; @Component public class StandardMapper extends Mapper { - public StandardMapper() { + public StandardMapper(@Autowired final EntityManagerWrapper em) { + super(em); 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 index a6d3c3fc..12ffa6e1 100644 --- a/src/main/java/net/hostsharing/hsadminng/mapper/StrictMapper.java +++ b/src/main/java/net/hostsharing/hsadminng/mapper/StrictMapper.java @@ -1,5 +1,7 @@ package net.hostsharing.hsadminng.mapper; +import net.hostsharing.hsadminng.persistence.EntityManagerWrapper; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import static org.modelmapper.convention.MatchingStrategies.STRICT; @@ -13,7 +15,8 @@ import static org.modelmapper.convention.MatchingStrategies.STRICT; @Component public class StrictMapper extends Mapper { - public StrictMapper() { + public StrictMapper(@Autowired final EntityManagerWrapper em) { + super(em); getConfiguration().setMatchingStrategy(STRICT); } } diff --git a/src/test/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemControllerAcceptanceTest.java b/src/test/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemControllerAcceptanceTest.java index 21bafcaf..66e2dff1 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemControllerAcceptanceTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemControllerAcceptanceTest.java @@ -185,7 +185,69 @@ class HsBookingItemControllerAcceptanceTest extends ContextBasedTestWithCleanup } @Test - void projectAgent_canAddBookingItemWithHostingAsset() { + void projectAgent_canAddManagedWebspaceBookingItemWithHostingAsset() { + + context.define("superuser-alex@hostsharing.net", "hs_booking.project#D-1000111-D-1000111defaultproject:AGENT"); + final var givenProject = findDefaultProjectOfDebitorNumber(1000111); + + final var location = RestAssured // @formatter:off + .given() + .header("current-subject", "superuser-alex@hostsharing.net") + .contentType(ContentType.JSON) + .body(""" + { + "projectUuid": "{projectUuid}", + "type": "MANAGED_WEBSPACE", + "caption": "some managed webspace", + "resources": { + "SSD": 25, + "Traffic": 250 + }, + "asset": { + "type": "MANAGED_WEBSPACE", + "identifier": "fir00" + } + } + """ + .replace("{projectUuid}", givenProject.getUuid().toString()) + ) + .port(port) + .when() + .post("http://localhost/api/hs/booking/items") + .then().log().all().assertThat() + .statusCode(201) + .contentType(ContentType.JSON) + .body("", lenientlyEquals(""" + { + "type": "MANAGED_WEBSPACE", + "caption": "some managed webspace", + "validFrom": "{today}", + "validTo": null + } + """ + .replace("{today}", LocalDate.now().toString()) + .replace("{todayPlus1Month}", LocalDate.now().plusMonths(1).toString())) + ) + .header("Location", matchesRegex("http://localhost:[1-9][0-9]*/api/hs/booking/items/[^/]*")) + .extract().header("Location"); // @formatter:on + + // then, the new BookingItem can be accessed under the generated UUID + final var newBookingItem = fetchRealBookingItemFromURI(location); + assertThat(newBookingItem) + .extracting(HsBookingItem::getCaption) + .isEqualTo("some managed webspace"); + + // and the related HostingAssets are also got created + final var domainSetupHostingAsset = realHostingAssetRepo.findByIdentifier("fir00"); + assertThat(domainSetupHostingAsset).isNotEmpty() + .map(HsHostingAsset::getBookingItem) + .contains(newBookingItem); + final var event = bookingItemCreationEventRepo.findByBookingItem(newBookingItem); + assertThat(event).isNull(); + } + + @Test + void projectAgent_canAddDomainSetupBookingItemWithHostingAsset() { context.define("superuser-alex@hostsharing.net", "hs_booking.project#D-1000111-D-1000111defaultproject:AGENT"); final var givenProject = findDefaultProjectOfDebitorNumber(1000111); diff --git a/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/factories/DomainSetupHostingAssetFactoryUnitTest.java b/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/factories/DomainSetupHostingAssetFactoryUnitTest.java index 8b6ae464..14762bd0 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/factories/DomainSetupHostingAssetFactoryUnitTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/factories/DomainSetupHostingAssetFactoryUnitTest.java @@ -59,7 +59,7 @@ class DomainSetupHostingAssetFactoryUnitTest { private ObjectMapper jsonMapper = new JsonObjectMapperConfiguration().customObjectMapper().build(); @Spy - private StandardMapper standardMapper = new StandardMapper(); + private StandardMapper standardMapper = new StandardMapper(emw); @InjectMocks private HsBookingItemCreatedListener listener; @@ -107,8 +107,9 @@ class DomainSetupHostingAssetFactoryUnitTest { ); // then - assertThat(emwFake.stream(BookingItemCreatedEventEntity.class).findAny().isEmpty()) - .as("the event should not have been persisted, but got persisted").isTrue(); + assertThat(emwFake.stream(BookingItemCreatedEventEntity.class)) + .as("the event should not have been persisted, but got persisted") + .isEmpty(); } @Test diff --git a/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/factories/ManagedWebspaceHostingAssetFactoryUnitTest.java b/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/factories/ManagedWebspaceHostingAssetFactoryUnitTest.java new file mode 100644 index 00000000..511e408c --- /dev/null +++ b/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/factories/ManagedWebspaceHostingAssetFactoryUnitTest.java @@ -0,0 +1,137 @@ +package net.hostsharing.hsadminng.hs.hosting.asset.factories; + +import com.fasterxml.jackson.databind.ObjectMapper; +import net.hostsharing.hsadminng.config.JsonObjectMapperConfiguration; +import net.hostsharing.hsadminng.hs.booking.debitor.HsBookingDebitorEntity; +import net.hostsharing.hsadminng.hs.booking.item.BookingItemCreatedAppEvent; +import net.hostsharing.hsadminng.hs.booking.item.BookingItemCreatedEventEntity; +import net.hostsharing.hsadminng.hs.booking.item.HsBookingItemRealEntity; +import net.hostsharing.hsadminng.hs.booking.item.HsBookingItemType; +import net.hostsharing.hsadminng.hs.booking.project.HsBookingProjectRealEntity; +import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetRealEntity; +import net.hostsharing.hsadminng.hs.hosting.asset.validators.Dns; +import net.hostsharing.hsadminng.hs.office.contact.HsOfficeContact; +import net.hostsharing.hsadminng.hs.office.contact.HsOfficeContactRealEntity; +import net.hostsharing.hsadminng.lambda.Reducer; +import net.hostsharing.hsadminng.mapper.StandardMapper; +import net.hostsharing.hsadminng.persistence.EntityManagerWrapper; +import net.hostsharing.hsadminng.persistence.EntityManagerWrapperFake; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Spy; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.util.Map; +import java.util.UUID; + +import static net.hostsharing.hsadminng.mapper.PatchableMapWrapper.entry; +import static org.assertj.core.api.Assertions.assertThat; + +// Tests the DomainSetupHostingAssetFactory through a HsBookingItemCreatedListener instance. +@ExtendWith(MockitoExtension.class) +class ManagedWebspaceHostingAssetFactoryUnitTest { + + final HsBookingDebitorEntity debitor = HsBookingDebitorEntity.builder() + .debitorNumber(12345) + .defaultPrefix("xyz") + .build(); + final HsBookingProjectRealEntity project = HsBookingProjectRealEntity.builder() + .debitor(debitor) + .caption("Test-Project") + .build(); + final HsOfficeContact alarmContact = HsOfficeContactRealEntity.builder() + .uuid(UUID.randomUUID()) + .caption("Alarm Contact xyz") + .build(); + + private EntityManagerWrapperFake emwFake = new EntityManagerWrapperFake(); + + @Spy + private EntityManagerWrapper emw = emwFake; + + @Spy + private ObjectMapper jsonMapper = new JsonObjectMapperConfiguration().customObjectMapper().build(); + + @Spy + private StandardMapper standardMapper = new StandardMapper(emw); + + @InjectMocks + private HsBookingItemCreatedListener listener; + + @BeforeEach + void initMocks() { + emwFake.persist(alarmContact); + } + + @Test + void doesNotPersistAnyEntityWithoutHostingAssetWithoutValidationErrors() { + // given + final var givenBookingItem = HsBookingItemRealEntity.builder() + .type(HsBookingItemType.MANAGED_WEBSPACE) + .project(project) + .caption("Test Managed-Webspace") + .resources(Map.ofEntries( + Map.entry("RAM", 25), + Map.entry("Traffic", 250) + )) + .build(); + + // when + listener.onApplicationEvent( + new BookingItemCreatedAppEvent(this, givenBookingItem, null) + ); + + // then + assertThat(emwFake.stream(BookingItemCreatedEventEntity.class).findAny().isEmpty()) + .as("the event should not have been persisted, but got persisted").isTrue(); + assertThat(emwFake.stream(HsHostingAssetRealEntity.class).findAny().isEmpty()) + .as("the hosting asset should not have been persisted, but got persisted").isTrue(); + } + + @Test + void persistsEventEntityIfDomainSetupVerificationFails() { + // given + final var givenBookingItem = createBookingItemFromResources( + entry("domainName", "example.org") + ); + final var givenAssetJson = """ + { + "identifier": "xyz00" + } + """; + Dns.fakeResultForDomain("example.org", Dns.Result.fromRecords()); // without valid verificationCode + + // when + listener.onApplicationEvent( + new BookingItemCreatedAppEvent(this, givenBookingItem, givenAssetJson) + ); + + // then + assertEventStatus(givenBookingItem, givenAssetJson, + "requires MANAGED_WEBSPACE hosting asset, but got null"); + } + + @SafeVarargs + private static HsBookingItemRealEntity createBookingItemFromResources(final Map.Entry... givenResources) { + return HsBookingItemRealEntity.builder() + .type(HsBookingItemType.MANAGED_WEBSPACE) + .resources(Map.ofEntries(givenResources)) + .build(); + } + + private void assertEventStatus( + final HsBookingItemRealEntity givenBookingItem, + final String givenAssetJson, + final String expectedErrorMessage) { + emwFake.stream(BookingItemCreatedEventEntity.class) + .reduce(Reducer::toSingleElement) + .map(eventEntity -> { + assertThat(eventEntity.getBookingItem()).isSameAs(givenBookingItem); + assertThat(eventEntity.getAssetJson()).isEqualTo(givenAssetJson); + assertThat(eventEntity.getStatusMessage()).isEqualTo(expectedErrorMessage); + return true; + }); + } +} 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 841e7e12..f6ab56fa 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 @@ -5,6 +5,7 @@ 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.StandardMapper; +import net.hostsharing.hsadminng.persistence.EntityManagerWrapper; import net.hostsharing.hsadminng.rbac.test.PatchUnitTestBase; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.TestInstance; @@ -12,7 +13,6 @@ import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; -import jakarta.persistence.EntityManager; import java.time.LocalDate; import java.util.UUID; import java.util.stream.Stream; @@ -38,9 +38,9 @@ class HsOfficeMembershipEntityPatcherUnitTest extends PatchUnitTestBase< private static final Boolean PATCHED_MEMBERSHIP_FEE_BILLABLE = false; @Mock - private EntityManager em; + private EntityManagerWrapper em; - private StandardMapper mapper = new StandardMapper(); + private StandardMapper mapper = new StandardMapper(em); @BeforeEach void initMocks() { diff --git a/src/test/java/net/hostsharing/hsadminng/persistence/EntityManagerWrapperFake.java b/src/test/java/net/hostsharing/hsadminng/persistence/EntityManagerWrapperFake.java index 4644677a..e1ce8e2e 100644 --- a/src/test/java/net/hostsharing/hsadminng/persistence/EntityManagerWrapperFake.java +++ b/src/test/java/net/hostsharing/hsadminng/persistence/EntityManagerWrapperFake.java @@ -21,9 +21,13 @@ public class EntityManagerWrapperFake extends EntityManagerWrapper { return find(entity.getClass(), id) != null; } + @Override + public T getReference(final Class entityClass, final Object primaryKey) { + return find(entityClass, primaryKey); + } + @Override public T find(final Class entityClass, final Object primaryKey) { - final var self = this; if (entityClasses.containsKey(entityClass)) { final var entities = entityClasses.get(entityClass); //noinspection unchecked @@ -87,5 +91,4 @@ public class EntityManagerWrapperFake extends EntityManagerWrapper { } throw new IllegalArgumentException("No @Id field found in entity class: " + entity.getClass().getName()); } - } -- 2.39.5 From 54d741a6c710f75fde9c88d7e6caf6b613eebb79 Mon Sep 17 00:00:00 2001 From: Michael Hoennig Date: Mon, 7 Oct 2024 14:20:44 +0200 Subject: [PATCH 24/31] acceptance test for auto-creating hosting asset for managed webspace booking item --- .../booking/item/HsBookingItemController.java | 23 ++++++++++++------- .../ManagedWebspaceHostingAssetFactory.java | 11 +++++++-- .../hs-booking/hs-booking-item-schemas.yaml | 4 ++++ ...HsBookingItemControllerAcceptanceTest.java | 6 +++++ 4 files changed, 34 insertions(+), 10 deletions(-) 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 8a78ba2a..4a6b3729 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 @@ -25,6 +25,7 @@ import java.util.List; import java.util.UUID; import java.util.function.BiConsumer; +import static java.util.Optional.ofNullable; import static net.hostsharing.hsadminng.mapper.PostgresDateRange.toPostgresDateRange; @RestController @@ -81,14 +82,7 @@ public class HsBookingItemController implements HsBookingItemsApi { .validateContext() .mapUsing(e -> mapper.map(e, HsBookingItemResource.class, ITEM_TO_RESOURCE_POSTMAPPER)) .revampProperties(); - - try { - final var bookingItemRealEntity = em.getReference(HsBookingItemRealEntity.class, saveProcessor.getEntity().getUuid()); - applicationEventPublisher.publishEvent(new BookingItemCreatedAppEvent( - this, bookingItemRealEntity, jsonMapper.writeValueAsString(body.getAsset()))); - } catch (JsonProcessingException e) { - throw new RuntimeException(e); - } + publishSavedEvent(saveProcessor, body); final var uri = MvcUriComponentsBuilder.fromController(getClass()) @@ -148,6 +142,16 @@ public class HsBookingItemController implements HsBookingItemsApi { return ResponseEntity.ok(mapped); } + private void publishSavedEvent(final BookingItemEntitySaveProcessor saveProcessor, final HsBookingItemInsertResource body) { + try { + final var bookingItemRealEntity = em.getReference(HsBookingItemRealEntity.class, saveProcessor.getEntity().getUuid()); + applicationEventPublisher.publishEvent(new BookingItemCreatedAppEvent( + this, bookingItemRealEntity, jsonMapper.writeValueAsString(body.getAsset()))); + } catch (JsonProcessingException e) { + throw new RuntimeException(e); + } + } + final BiConsumer ITEM_TO_RESOURCE_POSTMAPPER = (entity, resource) -> { resource.setValidFrom(entity.getValidity().lower()); if (entity.getValidity().hasUpperBound()) { @@ -159,6 +163,9 @@ public class HsBookingItemController implements HsBookingItemsApi { final BiConsumer RESOURCE_TO_ENTITY_POSTMAPPER = (resource, entity) -> { entity.setProject(em.find(HsBookingProjectRealEntity.class, resource.getProjectUuid())); + ofNullable(resource.getParentItemUuid()) + .map(parentItemUuid -> em.find(HsBookingItemRealEntity.class, parentItemUuid)) + .ifPresent(entity::setParentItem); entity.setValidity(toPostgresDateRange(LocalDate.now(), resource.getValidTo())); entity.putResources(KeyValueMap.from(resource.getResources())); }; diff --git a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/factories/ManagedWebspaceHostingAssetFactory.java b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/factories/ManagedWebspaceHostingAssetFactory.java index 70752572..e820ad40 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/factories/ManagedWebspaceHostingAssetFactory.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/factories/ManagedWebspaceHostingAssetFactory.java @@ -10,7 +10,8 @@ import net.hostsharing.hsadminng.persistence.EntityManagerWrapper; import jakarta.validation.ValidationException; -import static java.util.Optional.ofNullable; +import java.util.Optional; + public class ManagedWebspaceHostingAssetFactory extends HostingAssetFactory { @@ -26,13 +27,19 @@ public class ManagedWebspaceHostingAssetFactory extends HostingAssetFactory { protected HsHostingAsset create() { if (asset.getType() != HsHostingAssetTypeResource.MANAGED_WEBSPACE) { throw new ValidationException("requires MANAGED_WEBSPACE hosting asset, but got " + - ofNullable(asset) + Optional.of(asset) .map(HsHostingAssetAutoInsertResource::getType) .map(Enum::name) .orElse(null)); } final var managedWebspaceHostingAsset = standardMapper.map(asset, HsHostingAssetRealEntity.class); managedWebspaceHostingAsset.setBookingItem(fromBookingItem); + emw.createQuery( + "SELECT asset FROM HsHostingAssetRealEntity asset WHERE asset.bookingItem.uuid=:bookingItemUuid", + HsHostingAssetRealEntity.class) + .setParameter("bookingItemUuid", fromBookingItem.getParentItem().getUuid()) + .getResultStream().findFirst() + .ifPresent(managedWebspaceHostingAsset::setParentAsset); return managedWebspaceHostingAsset; } diff --git a/src/main/resources/api-definition/hs-booking/hs-booking-item-schemas.yaml b/src/main/resources/api-definition/hs-booking/hs-booking-item-schemas.yaml index 22d17ce3..354bec43 100644 --- a/src/main/resources/api-definition/hs-booking/hs-booking-item-schemas.yaml +++ b/src/main/resources/api-definition/hs-booking/hs-booking-item-schemas.yaml @@ -56,6 +56,10 @@ components: type: string format: uuid nullable: false + parentItemUuid: + type: string + format: uuid + nullable: false type: $ref: '#/components/schemas/HsBookingItemType' identifier: diff --git a/src/test/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemControllerAcceptanceTest.java b/src/test/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemControllerAcceptanceTest.java index 66e2dff1..0217667c 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemControllerAcceptanceTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemControllerAcceptanceTest.java @@ -34,6 +34,7 @@ import java.util.UUID; import static java.util.Map.entry; import static java.util.Optional.ofNullable; import static net.hostsharing.hsadminng.hs.booking.item.HsBookingItemType.MANAGED_WEBSPACE; +import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.MANAGED_SERVER; import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.UNIX_USER; import static net.hostsharing.hsadminng.rbac.test.JsonMatcher.lenientlyEquals; import static org.assertj.core.api.Assertions.assertThat; @@ -189,6 +190,9 @@ class HsBookingItemControllerAcceptanceTest extends ContextBasedTestWithCleanup context.define("superuser-alex@hostsharing.net", "hs_booking.project#D-1000111-D-1000111defaultproject:AGENT"); final var givenProject = findDefaultProjectOfDebitorNumber(1000111); + final var givenManagedServer = realHostingAssetRepo.findByTypeAndIdentifier(MANAGED_SERVER, "vm1011").stream() + .map(HsHostingAsset::getBookingItem) + .findFirst().orElseThrow(); final var location = RestAssured // @formatter:off .given() @@ -197,6 +201,7 @@ class HsBookingItemControllerAcceptanceTest extends ContextBasedTestWithCleanup .body(""" { "projectUuid": "{projectUuid}", + "parentItemUuid": "{managedServerUuid}", "type": "MANAGED_WEBSPACE", "caption": "some managed webspace", "resources": { @@ -210,6 +215,7 @@ class HsBookingItemControllerAcceptanceTest extends ContextBasedTestWithCleanup } """ .replace("{projectUuid}", givenProject.getUuid().toString()) + .replace("{managedServerUuid}", givenManagedServer.getUuid().toString()) ) .port(port) .when() -- 2.39.5 From 03b0062acd5cba40ccd9c7e1f72ab6c402f7a4b7 Mon Sep 17 00:00:00 2001 From: Michael Hoennig Date: Mon, 7 Oct 2024 15:13:26 +0200 Subject: [PATCH 25/31] fix EntityManager -> EntityManagerWrapper in tests which use mapper --- .../HsOfficeMembershipControllerRestTest.java | 21 ++----------------- .../HsOfficePartnerControllerRestTest.java | 3 ++- 2 files changed, 4 insertions(+), 20 deletions(-) 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 64de089c..1fcc9f11 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 @@ -4,12 +4,11 @@ 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.StandardMapper; -import org.junit.jupiter.api.BeforeEach; +import net.hostsharing.hsadminng.persistence.EntityManagerWrapper; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.EnumSource; -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.mock.mockito.MockBean; @@ -18,15 +17,10 @@ import org.springframework.http.MediaType; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; -import jakarta.persistence.EntityManager; -import jakarta.persistence.EntityManagerFactory; -import jakarta.persistence.SynchronizationType; -import java.util.Map; import java.util.UUID; import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.is; -import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.when; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @@ -47,19 +41,8 @@ public class HsOfficeMembershipControllerRestTest { @MockBean HsOfficeMembershipRepository membershipRepo; - @Mock - EntityManager em; - @MockBean - EntityManagerFactory emf; - - @BeforeEach - void init() { - when(emf.createEntityManager()).thenReturn(em); - when(emf.createEntityManager(any(Map.class))).thenReturn(em); - when(emf.createEntityManager(any(SynchronizationType.class))).thenReturn(em); - when(emf.createEntityManager(any(SynchronizationType.class), any(Map.class))).thenReturn(em); - } + EntityManagerWrapper em; @Nested class AddMembership { 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 2af222dc..6f07b222 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 @@ -6,6 +6,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.mapper.StandardMapper; +import net.hostsharing.hsadminng.persistence.EntityManagerWrapper; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; @@ -57,7 +58,7 @@ class HsOfficePartnerControllerRestTest { HsOfficeRelationRealRepository relationRepo; @MockBean - EntityManager em; + EntityManagerWrapper em; @MockBean EntityManagerFactory emf; -- 2.39.5 From 7c598632eda37b809811d87baf8b02009ea2a00c Mon Sep 17 00:00:00 2001 From: Michael Hoennig Date: Mon, 7 Oct 2024 16:29:56 +0200 Subject: [PATCH 26/31] refactor rbac.object.BaseEntity -> persistence.BaseEntity --- .../item/BookingItemCreatedEventEntity.java | 2 +- .../hs/booking/item/HsBookingItem.java | 2 +- .../hs/booking/project/HsBookingProject.java | 2 +- .../hs/hosting/asset/HsHostingAsset.java | 2 +- .../HsOfficeBankAccountEntity.java | 2 +- .../hs/office/contact/HsOfficeContact.java | 2 +- .../HsOfficeCoopAssetsTransactionEntity.java | 2 +- .../HsOfficeCoopSharesTransactionEntity.java | 2 +- .../office/debitor/HsOfficeDebitorEntity.java | 2 +- .../membership/HsOfficeMembershipEntity.java | 2 +- .../partner/HsOfficePartnerController.java | 2 +- .../partner/HsOfficePartnerDetailsEntity.java | 2 +- .../office/partner/HsOfficePartnerEntity.java | 2 +- .../office/person/HsOfficePersonEntity.java | 2 +- .../hs/office/relation/HsOfficeRelation.java | 2 +- .../HsOfficeSepaMandateEntity.java | 2 +- .../object => persistence}/BaseEntity.java | 3 +-- .../persistence/EntityExistsValidator.java | 1 - .../hsadminng/rbac/generator/RbacView.java | 2 +- .../rbac/test/cust/TestCustomerEntity.java | 2 +- .../rbac/test/dom/TestDomainEntity.java | 2 +- .../rbac/test/pac/TestPackageEntity.java | 2 +- .../hsadminng/hs/migration/CsvDataImport.java | 2 +- .../EntityManagerWrapperUnitTest.java | 5 ++--- .../rbac/context/ContextIntegrationTests.java | 3 ++- .../rbac/role/RbacRoleControllerRestTest.java | 5 +++-- .../RbacSubjectControllerRestTest.java | 21 ++----------------- .../test/ContextBasedTestWithCleanup.java | 2 +- .../hsadminng/rbac/test/EntityList.java | 2 +- .../hsadminng/rbac/test/MapperUnitTest.java | 3 ++- .../rbac/test/PatchUnitTestBase.java | 2 +- 31 files changed, 36 insertions(+), 53 deletions(-) rename src/main/java/net/hostsharing/hsadminng/{rbac/object => persistence}/BaseEntity.java (66%) diff --git a/src/main/java/net/hostsharing/hsadminng/hs/booking/item/BookingItemCreatedEventEntity.java b/src/main/java/net/hostsharing/hsadminng/hs/booking/item/BookingItemCreatedEventEntity.java index 19c6c208..e290313b 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/booking/item/BookingItemCreatedEventEntity.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/booking/item/BookingItemCreatedEventEntity.java @@ -5,7 +5,7 @@ import lombok.NoArgsConstructor; import lombok.Setter; import lombok.ToString; import lombok.experimental.SuperBuilder; -import net.hostsharing.hsadminng.rbac.object.BaseEntity; +import net.hostsharing.hsadminng.persistence.BaseEntity; import jakarta.persistence.Column; import jakarta.persistence.Entity; diff --git a/src/main/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItem.java b/src/main/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItem.java index 7b7e2174..07d85007 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItem.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItem.java @@ -14,7 +14,7 @@ import net.hostsharing.hsadminng.hs.booking.project.HsBookingProject; import net.hostsharing.hsadminng.hs.booking.project.HsBookingProjectRealEntity; import net.hostsharing.hsadminng.hs.validation.PropertiesProvider; import net.hostsharing.hsadminng.mapper.PatchableMapWrapper; -import net.hostsharing.hsadminng.rbac.object.BaseEntity; +import net.hostsharing.hsadminng.persistence.BaseEntity; import net.hostsharing.hsadminng.stringify.Stringify; import net.hostsharing.hsadminng.stringify.Stringifyable; import org.hibernate.annotations.Type; diff --git a/src/main/java/net/hostsharing/hsadminng/hs/booking/project/HsBookingProject.java b/src/main/java/net/hostsharing/hsadminng/hs/booking/project/HsBookingProject.java index 55069224..8b49aef9 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/booking/project/HsBookingProject.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/booking/project/HsBookingProject.java @@ -7,7 +7,7 @@ import net.hostsharing.hsadminng.hs.office.debitor.HsOfficeDebitorEntity; import net.hostsharing.hsadminng.hs.office.relation.HsOfficeRelationRbacEntity; import net.hostsharing.hsadminng.rbac.generator.RbacView; import net.hostsharing.hsadminng.rbac.generator.RbacView.SQL; -import net.hostsharing.hsadminng.rbac.object.BaseEntity; +import net.hostsharing.hsadminng.persistence.BaseEntity; import net.hostsharing.hsadminng.stringify.Stringify; import net.hostsharing.hsadminng.stringify.Stringifyable; diff --git a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAsset.java b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAsset.java index 63864065..e89e962f 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAsset.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAsset.java @@ -14,7 +14,7 @@ import net.hostsharing.hsadminng.hs.booking.project.HsBookingProject; import net.hostsharing.hsadminng.hs.office.contact.HsOfficeContactRealEntity; import net.hostsharing.hsadminng.hs.validation.PropertiesProvider; import net.hostsharing.hsadminng.mapper.PatchableMapWrapper; -import net.hostsharing.hsadminng.rbac.object.BaseEntity; +import net.hostsharing.hsadminng.persistence.BaseEntity; import net.hostsharing.hsadminng.stringify.Stringify; import net.hostsharing.hsadminng.stringify.Stringifyable; import org.hibernate.annotations.Type; diff --git a/src/main/java/net/hostsharing/hsadminng/hs/office/bankaccount/HsOfficeBankAccountEntity.java b/src/main/java/net/hostsharing/hsadminng/hs/office/bankaccount/HsOfficeBankAccountEntity.java index 74cb5f0b..5be09962 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/office/bankaccount/HsOfficeBankAccountEntity.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/office/bankaccount/HsOfficeBankAccountEntity.java @@ -3,7 +3,7 @@ package net.hostsharing.hsadminng.hs.office.bankaccount; import lombok.*; import lombok.experimental.FieldNameConstants; import net.hostsharing.hsadminng.errors.DisplayAs; -import net.hostsharing.hsadminng.rbac.object.BaseEntity; +import net.hostsharing.hsadminng.persistence.BaseEntity; import net.hostsharing.hsadminng.rbac.generator.RbacView; import net.hostsharing.hsadminng.stringify.Stringify; import net.hostsharing.hsadminng.stringify.Stringifyable; diff --git a/src/main/java/net/hostsharing/hsadminng/hs/office/contact/HsOfficeContact.java b/src/main/java/net/hostsharing/hsadminng/hs/office/contact/HsOfficeContact.java index 196e2d40..62519731 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/office/contact/HsOfficeContact.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/office/contact/HsOfficeContact.java @@ -11,7 +11,7 @@ import lombok.experimental.FieldNameConstants; import lombok.experimental.SuperBuilder; import net.hostsharing.hsadminng.errors.DisplayAs; import net.hostsharing.hsadminng.mapper.PatchableMapWrapper; -import net.hostsharing.hsadminng.rbac.object.BaseEntity; +import net.hostsharing.hsadminng.persistence.BaseEntity; import net.hostsharing.hsadminng.stringify.Stringify; import net.hostsharing.hsadminng.stringify.Stringifyable; import org.hibernate.annotations.GenericGenerator; diff --git a/src/main/java/net/hostsharing/hsadminng/hs/office/coopassets/HsOfficeCoopAssetsTransactionEntity.java b/src/main/java/net/hostsharing/hsadminng/hs/office/coopassets/HsOfficeCoopAssetsTransactionEntity.java index 0993b9e5..20ef39b4 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/office/coopassets/HsOfficeCoopAssetsTransactionEntity.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/office/coopassets/HsOfficeCoopAssetsTransactionEntity.java @@ -8,7 +8,7 @@ import lombok.NoArgsConstructor; import lombok.Setter; import net.hostsharing.hsadminng.errors.DisplayAs; import net.hostsharing.hsadminng.hs.office.membership.HsOfficeMembershipEntity; -import net.hostsharing.hsadminng.rbac.object.BaseEntity; +import net.hostsharing.hsadminng.persistence.BaseEntity; import net.hostsharing.hsadminng.rbac.generator.RbacView; import net.hostsharing.hsadminng.stringify.Stringify; import net.hostsharing.hsadminng.stringify.Stringifyable; diff --git a/src/main/java/net/hostsharing/hsadminng/hs/office/coopshares/HsOfficeCoopSharesTransactionEntity.java b/src/main/java/net/hostsharing/hsadminng/hs/office/coopshares/HsOfficeCoopSharesTransactionEntity.java index 2bbf287d..8af8b624 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/office/coopshares/HsOfficeCoopSharesTransactionEntity.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/office/coopshares/HsOfficeCoopSharesTransactionEntity.java @@ -8,7 +8,7 @@ import lombok.Setter; import net.hostsharing.hsadminng.errors.DisplayAs; import net.hostsharing.hsadminng.hs.office.membership.HsOfficeMembershipEntity; import net.hostsharing.hsadminng.rbac.generator.RbacView; -import net.hostsharing.hsadminng.rbac.object.BaseEntity; +import net.hostsharing.hsadminng.persistence.BaseEntity; import net.hostsharing.hsadminng.rbac.generator.RbacView.SQL; import net.hostsharing.hsadminng.stringify.Stringify; import net.hostsharing.hsadminng.stringify.Stringifyable; diff --git a/src/main/java/net/hostsharing/hsadminng/hs/office/debitor/HsOfficeDebitorEntity.java b/src/main/java/net/hostsharing/hsadminng/hs/office/debitor/HsOfficeDebitorEntity.java index 7b15662b..2c339605 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/office/debitor/HsOfficeDebitorEntity.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/office/debitor/HsOfficeDebitorEntity.java @@ -11,7 +11,7 @@ import net.hostsharing.hsadminng.hs.office.partner.HsOfficePartnerEntity; import net.hostsharing.hsadminng.hs.office.relation.HsOfficeRelation; import net.hostsharing.hsadminng.hs.office.relation.HsOfficeRelationRealEntity; import net.hostsharing.hsadminng.hs.office.relation.HsOfficeRelationRbacEntity; -import net.hostsharing.hsadminng.rbac.object.BaseEntity; +import net.hostsharing.hsadminng.persistence.BaseEntity; import net.hostsharing.hsadminng.rbac.generator.RbacView; import net.hostsharing.hsadminng.rbac.generator.RbacView.SQL; import net.hostsharing.hsadminng.stringify.Stringify; diff --git a/src/main/java/net/hostsharing/hsadminng/hs/office/membership/HsOfficeMembershipEntity.java b/src/main/java/net/hostsharing/hsadminng/hs/office/membership/HsOfficeMembershipEntity.java index b8c238c1..893623d7 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/office/membership/HsOfficeMembershipEntity.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/office/membership/HsOfficeMembershipEntity.java @@ -9,7 +9,7 @@ import lombok.NoArgsConstructor; import lombok.Setter; import net.hostsharing.hsadminng.errors.DisplayAs; import net.hostsharing.hsadminng.hs.office.relation.HsOfficeRelationRbacEntity; -import net.hostsharing.hsadminng.rbac.object.BaseEntity; +import net.hostsharing.hsadminng.persistence.BaseEntity; import net.hostsharing.hsadminng.hs.office.partner.HsOfficePartnerEntity; import net.hostsharing.hsadminng.rbac.generator.RbacView; import net.hostsharing.hsadminng.rbac.generator.RbacView.SQL; 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 55c280f3..1c86698a 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 @@ -13,7 +13,7 @@ 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.StandardMapper; -import net.hostsharing.hsadminng.rbac.object.BaseEntity; +import net.hostsharing.hsadminng.persistence.BaseEntity; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; diff --git a/src/main/java/net/hostsharing/hsadminng/hs/office/partner/HsOfficePartnerDetailsEntity.java b/src/main/java/net/hostsharing/hsadminng/hs/office/partner/HsOfficePartnerDetailsEntity.java index 0de01f6d..e6e43996 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/office/partner/HsOfficePartnerDetailsEntity.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/office/partner/HsOfficePartnerDetailsEntity.java @@ -2,7 +2,7 @@ package net.hostsharing.hsadminng.hs.office.partner; import lombok.*; import net.hostsharing.hsadminng.errors.DisplayAs; -import net.hostsharing.hsadminng.rbac.object.BaseEntity; +import net.hostsharing.hsadminng.persistence.BaseEntity; import net.hostsharing.hsadminng.rbac.generator.RbacView; import net.hostsharing.hsadminng.rbac.generator.RbacView.SQL; import net.hostsharing.hsadminng.stringify.Stringify; diff --git a/src/main/java/net/hostsharing/hsadminng/hs/office/partner/HsOfficePartnerEntity.java b/src/main/java/net/hostsharing/hsadminng/hs/office/partner/HsOfficePartnerEntity.java index 2f81dd45..b91f3ab1 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/office/partner/HsOfficePartnerEntity.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/office/partner/HsOfficePartnerEntity.java @@ -10,7 +10,7 @@ import net.hostsharing.hsadminng.hs.office.contact.HsOfficeContact; import net.hostsharing.hsadminng.hs.office.person.HsOfficePersonEntity; import net.hostsharing.hsadminng.hs.office.relation.HsOfficeRelationRealEntity; import net.hostsharing.hsadminng.hs.office.relation.HsOfficeRelationRbacEntity; -import net.hostsharing.hsadminng.rbac.object.BaseEntity; +import net.hostsharing.hsadminng.persistence.BaseEntity; import net.hostsharing.hsadminng.hs.office.relation.HsOfficeRelation; import net.hostsharing.hsadminng.rbac.generator.RbacView; import net.hostsharing.hsadminng.rbac.generator.RbacView.SQL; diff --git a/src/main/java/net/hostsharing/hsadminng/hs/office/person/HsOfficePersonEntity.java b/src/main/java/net/hostsharing/hsadminng/hs/office/person/HsOfficePersonEntity.java index 4d3e55ea..f28bd4ab 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/office/person/HsOfficePersonEntity.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/office/person/HsOfficePersonEntity.java @@ -3,7 +3,7 @@ package net.hostsharing.hsadminng.hs.office.person; import lombok.*; import lombok.experimental.FieldNameConstants; import net.hostsharing.hsadminng.errors.DisplayAs; -import net.hostsharing.hsadminng.rbac.object.BaseEntity; +import net.hostsharing.hsadminng.persistence.BaseEntity; import net.hostsharing.hsadminng.rbac.generator.RbacView; import net.hostsharing.hsadminng.rbac.generator.RbacView.SQL; import net.hostsharing.hsadminng.stringify.Stringify; diff --git a/src/main/java/net/hostsharing/hsadminng/hs/office/relation/HsOfficeRelation.java b/src/main/java/net/hostsharing/hsadminng/hs/office/relation/HsOfficeRelation.java index 66e954a4..2a4fbd42 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/office/relation/HsOfficeRelation.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/office/relation/HsOfficeRelation.java @@ -5,7 +5,7 @@ import lombok.experimental.FieldNameConstants; import lombok.experimental.SuperBuilder; import net.hostsharing.hsadminng.hs.office.contact.HsOfficeContactRealEntity; import net.hostsharing.hsadminng.hs.office.person.HsOfficePersonEntity; -import net.hostsharing.hsadminng.rbac.object.BaseEntity; +import net.hostsharing.hsadminng.persistence.BaseEntity; import net.hostsharing.hsadminng.stringify.Stringify; import net.hostsharing.hsadminng.stringify.Stringifyable; diff --git a/src/main/java/net/hostsharing/hsadminng/hs/office/sepamandate/HsOfficeSepaMandateEntity.java b/src/main/java/net/hostsharing/hsadminng/hs/office/sepamandate/HsOfficeSepaMandateEntity.java index bd91c44d..f781212e 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/office/sepamandate/HsOfficeSepaMandateEntity.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/office/sepamandate/HsOfficeSepaMandateEntity.java @@ -7,7 +7,7 @@ import net.hostsharing.hsadminng.errors.DisplayAs; import net.hostsharing.hsadminng.hs.office.bankaccount.HsOfficeBankAccountEntity; import net.hostsharing.hsadminng.hs.office.debitor.HsOfficeDebitorEntity; import net.hostsharing.hsadminng.hs.office.relation.HsOfficeRelationRbacEntity; -import net.hostsharing.hsadminng.rbac.object.BaseEntity; +import net.hostsharing.hsadminng.persistence.BaseEntity; import net.hostsharing.hsadminng.rbac.generator.RbacView; import net.hostsharing.hsadminng.stringify.Stringify; import net.hostsharing.hsadminng.stringify.Stringifyable; diff --git a/src/main/java/net/hostsharing/hsadminng/rbac/object/BaseEntity.java b/src/main/java/net/hostsharing/hsadminng/persistence/BaseEntity.java similarity index 66% rename from src/main/java/net/hostsharing/hsadminng/rbac/object/BaseEntity.java rename to src/main/java/net/hostsharing/hsadminng/persistence/BaseEntity.java index 1d0211bb..b3e5a535 100644 --- a/src/main/java/net/hostsharing/hsadminng/rbac/object/BaseEntity.java +++ b/src/main/java/net/hostsharing/hsadminng/persistence/BaseEntity.java @@ -1,11 +1,10 @@ -package net.hostsharing.hsadminng.rbac.object; +package net.hostsharing.hsadminng.persistence; import org.hibernate.Hibernate; import java.util.UUID; -// TODO.impl: this class does not really belong into this package, but there is no right place yet public interface BaseEntity> { UUID getUuid(); diff --git a/src/main/java/net/hostsharing/hsadminng/persistence/EntityExistsValidator.java b/src/main/java/net/hostsharing/hsadminng/persistence/EntityExistsValidator.java index fac98d33..1b29e007 100644 --- a/src/main/java/net/hostsharing/hsadminng/persistence/EntityExistsValidator.java +++ b/src/main/java/net/hostsharing/hsadminng/persistence/EntityExistsValidator.java @@ -1,7 +1,6 @@ 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; diff --git a/src/main/java/net/hostsharing/hsadminng/rbac/generator/RbacView.java b/src/main/java/net/hostsharing/hsadminng/rbac/generator/RbacView.java index 07838dad..dfdf3821 100644 --- a/src/main/java/net/hostsharing/hsadminng/rbac/generator/RbacView.java +++ b/src/main/java/net/hostsharing/hsadminng/rbac/generator/RbacView.java @@ -2,7 +2,7 @@ package net.hostsharing.hsadminng.rbac.generator; import lombok.EqualsAndHashCode; import lombok.Getter; -import net.hostsharing.hsadminng.rbac.object.BaseEntity; +import net.hostsharing.hsadminng.persistence.BaseEntity; import org.reflections.Reflections; import org.reflections.scanners.TypeAnnotationsScanner; diff --git a/src/main/java/net/hostsharing/hsadminng/rbac/test/cust/TestCustomerEntity.java b/src/main/java/net/hostsharing/hsadminng/rbac/test/cust/TestCustomerEntity.java index 37e45161..86fe4d57 100644 --- a/src/main/java/net/hostsharing/hsadminng/rbac/test/cust/TestCustomerEntity.java +++ b/src/main/java/net/hostsharing/hsadminng/rbac/test/cust/TestCustomerEntity.java @@ -5,7 +5,7 @@ import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; import lombok.ToString; -import net.hostsharing.hsadminng.rbac.object.BaseEntity; +import net.hostsharing.hsadminng.persistence.BaseEntity; import net.hostsharing.hsadminng.rbac.generator.RbacView; import net.hostsharing.hsadminng.rbac.generator.RbacView.SQL; diff --git a/src/main/java/net/hostsharing/hsadminng/rbac/test/dom/TestDomainEntity.java b/src/main/java/net/hostsharing/hsadminng/rbac/test/dom/TestDomainEntity.java index 4895375d..00a78a20 100644 --- a/src/main/java/net/hostsharing/hsadminng/rbac/test/dom/TestDomainEntity.java +++ b/src/main/java/net/hostsharing/hsadminng/rbac/test/dom/TestDomainEntity.java @@ -4,7 +4,7 @@ import lombok.AllArgsConstructor; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; -import net.hostsharing.hsadminng.rbac.object.BaseEntity; +import net.hostsharing.hsadminng.persistence.BaseEntity; import net.hostsharing.hsadminng.rbac.generator.RbacView; import net.hostsharing.hsadminng.rbac.generator.RbacView.SQL; import net.hostsharing.hsadminng.rbac.test.pac.TestPackageEntity; diff --git a/src/main/java/net/hostsharing/hsadminng/rbac/test/pac/TestPackageEntity.java b/src/main/java/net/hostsharing/hsadminng/rbac/test/pac/TestPackageEntity.java index fcbaff4f..1efadadc 100644 --- a/src/main/java/net/hostsharing/hsadminng/rbac/test/pac/TestPackageEntity.java +++ b/src/main/java/net/hostsharing/hsadminng/rbac/test/pac/TestPackageEntity.java @@ -4,7 +4,7 @@ import lombok.AllArgsConstructor; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; -import net.hostsharing.hsadminng.rbac.object.BaseEntity; +import net.hostsharing.hsadminng.persistence.BaseEntity; import net.hostsharing.hsadminng.rbac.generator.RbacView; import net.hostsharing.hsadminng.rbac.generator.RbacView.SQL; import net.hostsharing.hsadminng.rbac.test.cust.TestCustomerEntity; diff --git a/src/test/java/net/hostsharing/hsadminng/hs/migration/CsvDataImport.java b/src/test/java/net/hostsharing/hsadminng/hs/migration/CsvDataImport.java index 7f1ac1f2..b5b7de8e 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/migration/CsvDataImport.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/migration/CsvDataImport.java @@ -6,7 +6,7 @@ import com.opencsv.CSVReaderBuilder; import lombok.SneakyThrows; import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAsset; import net.hostsharing.hsadminng.rbac.context.ContextBasedTest; -import net.hostsharing.hsadminng.rbac.object.BaseEntity; +import net.hostsharing.hsadminng.persistence.BaseEntity; import net.hostsharing.hsadminng.rbac.test.JpaAttempt; import org.junit.jupiter.api.extension.BeforeEachCallback; import org.junit.jupiter.api.extension.ExtensionContext; diff --git a/src/test/java/net/hostsharing/hsadminng/persistence/EntityManagerWrapperUnitTest.java b/src/test/java/net/hostsharing/hsadminng/persistence/EntityManagerWrapperUnitTest.java index f9db2070..b47fe0fd 100644 --- a/src/test/java/net/hostsharing/hsadminng/persistence/EntityManagerWrapperUnitTest.java +++ b/src/test/java/net/hostsharing/hsadminng/persistence/EntityManagerWrapperUnitTest.java @@ -1,6 +1,5 @@ package net.hostsharing.hsadminng.persistence; -import net.hostsharing.hsadminng.mapper.Array; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.mockito.Mockito; @@ -53,9 +52,9 @@ class EntityManagerWrapperUnitTest { if (type == double.class) return 0.0; if (type == char.class) return '\0'; if (type == String.class) return "dummy"; - if (type == String[].class) return Array.of("dummy"); + if (type == String[].class) return new String[]{"dummy"}; if (type == Class.class) return String.class; - if (type == Class[].class) return Array.of(String.class); + if (type == Class[].class) return new Class[0]; return mock(type); } } 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 cf5f387e..ef1c482c 100644 --- a/src/test/java/net/hostsharing/hsadminng/rbac/context/ContextIntegrationTests.java +++ b/src/test/java/net/hostsharing/hsadminng/rbac/context/ContextIntegrationTests.java @@ -3,6 +3,7 @@ package net.hostsharing.hsadminng.rbac.context; import net.hostsharing.hsadminng.context.Context; import net.hostsharing.hsadminng.mapper.StandardMapper; import net.hostsharing.hsadminng.mapper.Array; +import net.hostsharing.hsadminng.persistence.EntityManagerWrapper; import net.hostsharing.hsadminng.rbac.test.JpaAttempt; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; @@ -17,7 +18,7 @@ import jakarta.servlet.http.HttpServletRequest; import static org.assertj.core.api.Assertions.assertThat; @DataJpaTest -@ComponentScan(basePackageClasses = { Context.class, JpaAttempt.class, StandardMapper.class }) +@ComponentScan(basePackageClasses = { Context.class, JpaAttempt.class, EntityManagerWrapper.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 7d38b0e9..bf171f59 100644 --- a/src/test/java/net/hostsharing/hsadminng/rbac/role/RbacRoleControllerRestTest.java +++ b/src/test/java/net/hostsharing/hsadminng/rbac/role/RbacRoleControllerRestTest.java @@ -2,6 +2,7 @@ package net.hostsharing.hsadminng.rbac.role; import net.hostsharing.hsadminng.context.Context; import net.hostsharing.hsadminng.mapper.StandardMapper; +import net.hostsharing.hsadminng.persistence.EntityManagerWrapper; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.runner.RunWith; @@ -43,8 +44,8 @@ class RbacRoleControllerRestTest { @MockBean RbacRoleRepository rbacRoleRepository; - @Mock - EntityManager em; + @MockBean + EntityManagerWrapper em; @MockBean EntityManagerFactory emf; 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 2131c7d9..f1067753 100644 --- a/src/test/java/net/hostsharing/hsadminng/rbac/subject/RbacSubjectControllerRestTest.java +++ b/src/test/java/net/hostsharing/hsadminng/rbac/subject/RbacSubjectControllerRestTest.java @@ -2,10 +2,9 @@ package net.hostsharing.hsadminng.rbac.subject; import net.hostsharing.hsadminng.context.Context; import net.hostsharing.hsadminng.mapper.StandardMapper; -import org.junit.jupiter.api.BeforeEach; +import net.hostsharing.hsadminng.persistence.EntityManagerWrapper; 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.mock.mockito.MockBean; @@ -15,18 +14,12 @@ import org.springframework.test.context.junit4.SpringRunner; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; -import jakarta.persistence.EntityManager; -import jakarta.persistence.EntityManagerFactory; -import jakarta.persistence.SynchronizationType; -import java.util.Map; import java.util.UUID; import static net.hostsharing.hsadminng.rbac.test.IsValidUuidMatcher.isUuidValid; import static org.hamcrest.Matchers.is; -import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.argThat; import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @@ -44,19 +37,9 @@ class RbacSubjectControllerRestTest { @MockBean RbacSubjectRepository rbacSubjectRepository; - @Mock - EntityManager em; - @MockBean - EntityManagerFactory emf; + EntityManagerWrapper em; - @BeforeEach - void init() { - when(emf.createEntityManager()).thenReturn(em); - when(emf.createEntityManager(any(Map.class))).thenReturn(em); - when(emf.createEntityManager(any(SynchronizationType.class))).thenReturn(em); - when(emf.createEntityManager(any(SynchronizationType.class), any(Map.class))).thenReturn(em); - } @Test void createSubjectUsesGivenUuid() throws Exception { diff --git a/src/test/java/net/hostsharing/hsadminng/rbac/test/ContextBasedTestWithCleanup.java b/src/test/java/net/hostsharing/hsadminng/rbac/test/ContextBasedTestWithCleanup.java index 1d2622a0..6742192c 100644 --- a/src/test/java/net/hostsharing/hsadminng/rbac/test/ContextBasedTestWithCleanup.java +++ b/src/test/java/net/hostsharing/hsadminng/rbac/test/ContextBasedTestWithCleanup.java @@ -1,7 +1,7 @@ package net.hostsharing.hsadminng.rbac.test; import net.hostsharing.hsadminng.rbac.context.ContextBasedTest; -import net.hostsharing.hsadminng.rbac.object.BaseEntity; +import net.hostsharing.hsadminng.persistence.BaseEntity; import net.hostsharing.hsadminng.rbac.grant.RbacGrantEntity; import net.hostsharing.hsadminng.rbac.grant.RbacGrantRepository; import net.hostsharing.hsadminng.rbac.grant.RbacGrantsDiagramService; diff --git a/src/test/java/net/hostsharing/hsadminng/rbac/test/EntityList.java b/src/test/java/net/hostsharing/hsadminng/rbac/test/EntityList.java index 09e982b9..93a3f1a0 100644 --- a/src/test/java/net/hostsharing/hsadminng/rbac/test/EntityList.java +++ b/src/test/java/net/hostsharing/hsadminng/rbac/test/EntityList.java @@ -1,6 +1,6 @@ package net.hostsharing.hsadminng.rbac.test; -import net.hostsharing.hsadminng.rbac.object.BaseEntity; +import net.hostsharing.hsadminng.persistence.BaseEntity; import java.util.List; 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 b90c7cb1..32bf1865 100644 --- a/src/test/java/net/hostsharing/hsadminng/rbac/test/MapperUnitTest.java +++ b/src/test/java/net/hostsharing/hsadminng/rbac/test/MapperUnitTest.java @@ -3,6 +3,7 @@ package net.hostsharing.hsadminng.rbac.test; import lombok.*; import net.hostsharing.hsadminng.errors.DisplayAs; import net.hostsharing.hsadminng.mapper.StandardMapper; +import net.hostsharing.hsadminng.persistence.EntityManagerWrapper; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.InjectMocks; @@ -24,7 +25,7 @@ import static org.mockito.Mockito.when; class MapperUnitTest { @Mock - EntityManager em; + EntityManagerWrapper em; @InjectMocks StandardMapper mapper; diff --git a/src/test/java/net/hostsharing/hsadminng/rbac/test/PatchUnitTestBase.java b/src/test/java/net/hostsharing/hsadminng/rbac/test/PatchUnitTestBase.java index 67880dec..f2b7e8bb 100644 --- a/src/test/java/net/hostsharing/hsadminng/rbac/test/PatchUnitTestBase.java +++ b/src/test/java/net/hostsharing/hsadminng/rbac/test/PatchUnitTestBase.java @@ -1,6 +1,6 @@ package net.hostsharing.hsadminng.rbac.test; -import net.hostsharing.hsadminng.rbac.object.BaseEntity; +import net.hostsharing.hsadminng.persistence.BaseEntity; import net.hostsharing.hsadminng.mapper.EntityPatcher; import org.junit.jupiter.api.Named; import org.junit.jupiter.api.Test; -- 2.39.5 From 81bfa4288fb00419024d735205b6c1125b9bdec4 Mon Sep 17 00:00:00 2001 From: Michael Hoennig Date: Mon, 7 Oct 2024 16:46:57 +0200 Subject: [PATCH 27/31] fix lastUnixUserId --- .../HsHostingAssetControllerAcceptanceTest.java | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetControllerAcceptanceTest.java b/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetControllerAcceptanceTest.java index 6f2d42d4..a782e45d 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetControllerAcceptanceTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetControllerAcceptanceTest.java @@ -184,10 +184,12 @@ class HsHostingAssetControllerAcceptanceTest extends ContextBasedTestWithCleanup "identifier": "fir10", "caption": "some separate ManagedWebspace HA", "config": { - "groupid": 1000000 + "groupid": {lastUnixUserId} } } - """)) + """ + .replace("{lastUnixUserId}", lastUnixUserId().toString()) + )) .header("Location", matchesRegex("http://localhost:[1-9][0-9]*/api/hs/hosting/assets/[^/]*")) .extract().header("Location"); // @formatter:on @@ -777,4 +779,11 @@ class HsHostingAssetControllerAcceptanceTest extends ContextBasedTestWithCleanup }).returnedValue(); } + + private Integer lastUnixUserId() { + final Object result = em.createNativeQuery("SELECT last_value from hs_hosting.asset_unixuser_system_id_seq", Integer.class) + .getSingleResult(); + return (Integer) result; + } + } -- 2.39.5 From 03b720251a6c5b4839981759f79eafbb506803a1 Mon Sep 17 00:00:00 2001 From: Michael Hoennig Date: Mon, 7 Oct 2024 17:07:26 +0200 Subject: [PATCH 28/31] fix lastUnixUserId - 2nd try --- .../hosting/asset/HsHostingAssetControllerAcceptanceTest.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetControllerAcceptanceTest.java b/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetControllerAcceptanceTest.java index a782e45d..43cb0daf 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetControllerAcceptanceTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetControllerAcceptanceTest.java @@ -157,6 +157,7 @@ class HsHostingAssetControllerAcceptanceTest extends ContextBasedTestWithCleanup ) ); final var givenParentAsset = givenParentAsset(MANAGED_SERVER, "vm1011"); + final var expectedUnixUserId = lastUnixUserId(); final var location = RestAssured // @formatter:off .given() @@ -188,7 +189,7 @@ class HsHostingAssetControllerAcceptanceTest extends ContextBasedTestWithCleanup } } """ - .replace("{lastUnixUserId}", lastUnixUserId().toString()) + .replace("{lastUnixUserId}", expectedUnixUserId.toString()) )) .header("Location", matchesRegex("http://localhost:[1-9][0-9]*/api/hs/hosting/assets/[^/]*")) .extract().header("Location"); // @formatter:on -- 2.39.5 From 05ffbac8828fa4b4eaf46ce5279067c890558463 Mon Sep 17 00:00:00 2001 From: Michael Hoennig Date: Mon, 7 Oct 2024 17:22:44 +0200 Subject: [PATCH 29/31] fix lastUnixUserId - 3rd try --- .../HsHostingAssetControllerAcceptanceTest.java | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetControllerAcceptanceTest.java b/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetControllerAcceptanceTest.java index 43cb0daf..30b62a5a 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetControllerAcceptanceTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetControllerAcceptanceTest.java @@ -157,7 +157,7 @@ class HsHostingAssetControllerAcceptanceTest extends ContextBasedTestWithCleanup ) ); final var givenParentAsset = givenParentAsset(MANAGED_SERVER, "vm1011"); - final var expectedUnixUserId = lastUnixUserId(); + final var expectedUnixUserId = nextUnixUserId(); final var location = RestAssured // @formatter:off .given() @@ -208,9 +208,11 @@ class HsHostingAssetControllerAcceptanceTest extends ContextBasedTestWithCleanup .isEqualTo(""" HsHostingAsset(UNIX_USER, fir10, fir10 webspace user, MANAGED_WEBSPACE:fir10, { "password" : null, - "userid" : 1000000 + "userid" : {lastUnixUserId} }) - """.trim()); + """ + .replace("{lastUnixUserId}", expectedUnixUserId.toString()) + .trim()); } @Test @@ -781,10 +783,10 @@ class HsHostingAssetControllerAcceptanceTest extends ContextBasedTestWithCleanup } - private Integer lastUnixUserId() { - final Object result = em.createNativeQuery("SELECT last_value from hs_hosting.asset_unixuser_system_id_seq", Integer.class) + private Integer nextUnixUserId() { + final Object result = em.createNativeQuery("SELECT nextval('hs_hosting.asset_unixuser_system_id_seq')", Integer.class) .getSingleResult(); - return (Integer) result; + return (Integer) result + 1; } } -- 2.39.5 From d7ee5fb8c5d1f152a58139beda66503f3e2a171f Mon Sep 17 00:00:00 2001 From: Michael Hoennig Date: Tue, 8 Oct 2024 09:57:37 +0200 Subject: [PATCH 30/31] error status for booking items without automatic hosting asset creation --- .../HsBookingItemCreatedListener.java | 26 ++++- .../HsBookingItemCreatedListenerUnitTest.java | 98 +++++++++++++++++++ .../HsOfficePartnerControllerRestTest.java | 1 - .../rbac/role/RbacRoleControllerRestTest.java | 2 - .../hsadminng/rbac/test/MapperUnitTest.java | 1 - 5 files changed, 123 insertions(+), 5 deletions(-) create mode 100644 src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/factories/HsBookingItemCreatedListenerUnitTest.java diff --git a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/factories/HsBookingItemCreatedListener.java b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/factories/HsBookingItemCreatedListener.java index d388c61c..651d5277 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/factories/HsBookingItemCreatedListener.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/factories/HsBookingItemCreatedListener.java @@ -5,6 +5,8 @@ import com.fasterxml.jackson.databind.ObjectMapper; import lombok.SneakyThrows; import net.hostsharing.hsadminng.hs.booking.generated.api.v1.model.HsHostingAssetAutoInsertResource; import net.hostsharing.hsadminng.hs.booking.item.BookingItemCreatedAppEvent; +import net.hostsharing.hsadminng.hs.booking.item.HsBookingItemRealEntity; +import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAsset; import net.hostsharing.hsadminng.mapper.StandardMapper; import net.hostsharing.hsadminng.persistence.EntityManagerWrapper; import org.springframework.beans.factory.annotation.Autowired; @@ -40,7 +42,8 @@ public class HsBookingItemCreatedListener implements ApplicationListener null; // for now, no automatic HostingAsset possible + case PRIVATE_CLOUD, CLOUD_SERVER, MANAGED_SERVER -> + forNowNoAutomaticHostingAssetCreationPossible(emw, newBookingItemRealEntity, asset, standardMapper); case MANAGED_WEBSPACE -> new ManagedWebspaceHostingAssetFactory(emw, newBookingItemRealEntity, asset, standardMapper); case DOMAIN_SETUP -> new DomainSetupHostingAssetFactory(emw, newBookingItemRealEntity, asset, standardMapper); }; @@ -53,4 +56,25 @@ public class HsBookingItemCreatedListener implements ApplicationListener bookingItemTypesWithoutAutomaticAssetCreation() { + return Arrays.stream(HsBookingItemType.values()) + .filter(v -> v != MANAGED_WEBSPACE && v != DOMAIN_SETUP) + .toList(); + } + + private static HsBookingItemRealEntity createBookingItemFromResources( + final HsBookingItemType bookingItemType + ) { + return HsBookingItemRealEntity.builder() + .type(bookingItemType) + .build(); + } + + private void assertEventStatus( + final HsBookingItemRealEntity givenBookingItem, + final String givenAssetJson, + final String expectedErrorMessage) { + emwFake.stream(BookingItemCreatedEventEntity.class) + .reduce(Reducer::toSingleElement) + .map(eventEntity -> { + assertThat(eventEntity.getBookingItem()).isSameAs(givenBookingItem); + assertThat(eventEntity.getAssetJson()).isEqualTo(givenAssetJson); + assertThat(eventEntity.getStatusMessage()).isEqualTo(expectedErrorMessage); + return true; + }); + } +} 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 6f07b222..42d0566c 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 @@ -19,7 +19,6 @@ import org.springframework.http.MediaType; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; -import jakarta.persistence.EntityManager; import jakarta.persistence.EntityManagerFactory; import jakarta.persistence.EntityNotFoundException; import jakarta.persistence.SynchronizationType; 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 bf171f59..0f1abce6 100644 --- a/src/test/java/net/hostsharing/hsadminng/rbac/role/RbacRoleControllerRestTest.java +++ b/src/test/java/net/hostsharing/hsadminng/rbac/role/RbacRoleControllerRestTest.java @@ -6,7 +6,6 @@ import net.hostsharing.hsadminng.persistence.EntityManagerWrapper; import org.junit.jupiter.api.BeforeEach; 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.mock.mockito.MockBean; @@ -16,7 +15,6 @@ import org.springframework.test.context.junit4.SpringRunner; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; -import jakarta.persistence.EntityManager; import jakarta.persistence.EntityManagerFactory; import jakarta.persistence.SynchronizationType; import java.util.Map; 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 32bf1865..5d64903f 100644 --- a/src/test/java/net/hostsharing/hsadminng/rbac/test/MapperUnitTest.java +++ b/src/test/java/net/hostsharing/hsadminng/rbac/test/MapperUnitTest.java @@ -10,7 +10,6 @@ import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; -import jakarta.persistence.EntityManager; import jakarta.persistence.ManyToOne; import jakarta.validation.ValidationException; import java.util.List; -- 2.39.5 From f132f4d2936eecdc92243e815d9415d74384f358 Mon Sep 17 00:00:00 2001 From: Michael Hoennig Date: Tue, 8 Oct 2024 11:37:31 +0200 Subject: [PATCH 31/31] amendmends after code-review --- .../hs/booking/item/HsBookingItemController.java | 2 +- .../factories/DomainSetupHostingAssetFactory.java | 2 +- .../hs-booking/hs-booking-item-schemas.yaml | 13 +------------ .../item/HsBookingItemControllerAcceptanceTest.java | 6 +++--- .../hs/migration/BaseOfficeDataImport.java | 2 +- 5 files changed, 7 insertions(+), 18 deletions(-) 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 4a6b3729..e8441b01 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 @@ -146,7 +146,7 @@ public class HsBookingItemController implements HsBookingItemsApi { try { final var bookingItemRealEntity = em.getReference(HsBookingItemRealEntity.class, saveProcessor.getEntity().getUuid()); applicationEventPublisher.publishEvent(new BookingItemCreatedAppEvent( - this, bookingItemRealEntity, jsonMapper.writeValueAsString(body.getAsset()))); + this, bookingItemRealEntity, jsonMapper.writeValueAsString(body.getHostingAsset()))); } catch (JsonProcessingException e) { throw new RuntimeException(e); } diff --git a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/factories/DomainSetupHostingAssetFactory.java b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/factories/DomainSetupHostingAssetFactory.java index d12e8dcf..de6b4f02 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/factories/DomainSetupHostingAssetFactory.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/factories/DomainSetupHostingAssetFactory.java @@ -132,7 +132,7 @@ public class DomainSetupHostingAssetFactory extends HostingAssetFactory { final var expectedSubAssetResource = new HsHostingAssetSubInsertResource(); expectedSubAssetResource.setType(givenSubAssetResource.getType()); if ( !convert.from(givenSubAssetResource).equals(convert.from(expectedSubAssetResource)) ) { - throw new ValidationException("sub asset " + givenSubAssetResource.getType() + " is over-specified, in compatibilty mode, only default values allowed"); + throw new ValidationException("sub asset " + givenSubAssetResource.getType() + " is over-specified, in compatibility mode, only default values allowed"); } } diff --git a/src/main/resources/api-definition/hs-booking/hs-booking-item-schemas.yaml b/src/main/resources/api-definition/hs-booking/hs-booking-item-schemas.yaml index 354bec43..ef0ac307 100644 --- a/src/main/resources/api-definition/hs-booking/hs-booking-item-schemas.yaml +++ b/src/main/resources/api-definition/hs-booking/hs-booking-item-schemas.yaml @@ -62,17 +62,6 @@ components: nullable: false type: $ref: '#/components/schemas/HsBookingItemType' - identifier: - type: string - minLength: 3 - maxLength: 80 - nullable: false - description: only used as a default value for automatically created hosting assets, not part of the booking item - assignedToHostingAssetUuid: - type: string - format: uuid - nullable: false - description: only used as a default value for automatically created hosting assets, not part of the booking item caption: type: string minLength: 3 @@ -84,7 +73,7 @@ components: nullable: true resources: $ref: '#/components/schemas/BookingResources' - asset: + hostingAsset: $ref: '../hs-hosting/hs-hosting-asset-schemas.yaml#/components/schemas/HsHostingAssetAutoInsert' required: - caption diff --git a/src/test/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemControllerAcceptanceTest.java b/src/test/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemControllerAcceptanceTest.java index 0217667c..6b4a4b29 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemControllerAcceptanceTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemControllerAcceptanceTest.java @@ -208,7 +208,7 @@ class HsBookingItemControllerAcceptanceTest extends ContextBasedTestWithCleanup "SSD": 25, "Traffic": 250 }, - "asset": { + "hostingAsset": { "type": "MANAGED_WEBSPACE", "identifier": "fir00" } @@ -277,7 +277,7 @@ class HsBookingItemControllerAcceptanceTest extends ContextBasedTestWithCleanup "domainName": "example.org", "verificationCode": "just-a-fake-verification-code" }, - "asset": { // FIXME: rename to hostingAsset + "hostingAsset": { "identifier": "example.org", // also as default for all subAssets "subHostingAssets": [ { @@ -367,7 +367,7 @@ class HsBookingItemControllerAcceptanceTest extends ContextBasedTestWithCleanup "domainName": "example.org", "verificationCode": "just-a-fake-verification-code" }, - "asset": { // FIXME: rename to hostingAsset + "hostingAsset": { "identifier": "example.org", // also as default for all subAssets "subHostingAssets": [ { diff --git a/src/test/java/net/hostsharing/hsadminng/hs/migration/BaseOfficeDataImport.java b/src/test/java/net/hostsharing/hsadminng/hs/migration/BaseOfficeDataImport.java index 9aa92773..511647aa 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/migration/BaseOfficeDataImport.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/migration/BaseOfficeDataImport.java @@ -502,7 +502,7 @@ public abstract class BaseOfficeDataImport extends CsvDataImport { // this happens if a natural person is marked as 'contractual' for itself final var idsToRemove = new HashSet(); relations.forEach((id, r) -> { - if (r.getType() == HsOfficeRelationType.REPRESENTATIVE && r.getHolder() == r.getAnchor()) { + if (r.getHolder() == r.getAnchor()) { idsToRemove.add(id); } }); -- 2.39.5