From 60341bf64438f2e2cea80e59ba23001c47aa278b Mon Sep 17 00:00:00 2001 From: Michael Hoennig Date: Tue, 8 Oct 2024 11:48:31 +0200 Subject: [PATCH] add DomainSetup-HostingAssets for new BookingItem via created-event (#111) Co-authored-by: Michael Hoennig Reviewed-on: https://dev.hostsharing.net/hostsharing/hs.hsadmin.ng/pulls/111 Reviewed-by: Timotheus Pokorra --- ...ng-asset-creation-for-new-booking-items.md | 119 ++++++++ .../config/JsonObjectMapperConfiguration.java | 2 +- .../hsadminng/hash/HashGenerator.java | 2 +- .../item/BookingItemCreatedAppEvent.java | 20 ++ .../booking/item/BookingItemCreatedEvent.java | 16 -- .../item/BookingItemCreatedEventEntity.java | 55 ++++ .../BookingItemCreatedEventRepository.java | 12 + .../hs/booking/item/HsBookingItem.java | 2 +- .../booking/item/HsBookingItemController.java | 22 +- .../BookingItemEntitySaveProcessor.java | 2 +- .../HsDomainSetupBookingItemValidator.java | 10 +- .../hs/booking/project/HsBookingProject.java | 2 +- .../asset/HsBookingItemCreatedListener.java | 56 ---- .../hs/hosting/asset/HsHostingAsset.java | 12 +- .../asset/HsHostingAssetRealRepository.java | 13 + .../DomainSetupHostingAssetFactory.java | 153 +++++++++++ .../asset/factories/HostingAssetFactory.java | 45 +++ .../HsBookingItemCreatedListener.java | 80 ++++++ .../ManagedWebspaceHostingAssetFactory.java | 51 ++++ .../asset/factories/ToStringConverter.java | 37 +++ .../HostingAssetEntitySaveProcessor.java | 2 +- ...HsDomainDnsSetupHostingAssetValidator.java | 4 +- .../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 +- .../hs/validation/PasswordProperty.java | 2 +- .../hostsharing/hsadminng/lambda/Reducer.java | 8 + .../hostsharing/hsadminng/mapper/Mapper.java | 10 +- .../hsadminng/mapper/StandardMapper.java | 5 +- .../hsadminng/mapper/StrictMapper.java | 5 +- .../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 +- .../hs-booking/hs-booking-item-schemas.yaml | 6 + .../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 +- .../630-booking-item/6300-hs-booking-item.sql | 14 + .../7010-hs-hosting-asset.sql | 2 +- .../7016-hs-hosting-asset-migration.sql | 2 +- .../hsadminng/arch/ArchitectureTest.java | 2 + ...HsBookingItemControllerAcceptanceTest.java | 216 ++++++++++++--- ...mainSetupBookingItemValidatorUnitTest.java | 36 +-- ...sHostingAssetControllerAcceptanceTest.java | 20 +- ...omainSetupHostingAssetFactoryUnitTest.java | 259 ++++++++++++++++++ .../HsBookingItemCreatedListenerUnitTest.java | 98 +++++++ ...edWebspaceHostingAssetFactoryUnitTest.java | 137 +++++++++ ...ainSetupHostingAssetValidatorUnitTest.java | 3 +- .../hsadminng/hs/migration/CsvDataImport.java | 2 +- .../hs/migration/ImportHostingAssets.java | 7 +- .../HsOfficeMembershipControllerRestTest.java | 21 +- ...OfficeMembershipEntityPatcherUnitTest.java | 6 +- .../HsOfficePartnerControllerRestTest.java | 4 +- .../persistence/EntityManagerWrapperFake.java | 94 +++++++ .../EntityManagerWrapperUnitTest.java | 5 +- .../rbac/context/ContextIntegrationTests.java | 3 +- .../rbac/role/RbacRoleControllerRestTest.java | 7 +- .../RbacSubjectControllerRestTest.java | 21 +- .../test/ContextBasedTestWithCleanup.java | 2 +- .../hsadminng/rbac/test/EntityList.java | 2 +- .../hsadminng/rbac/test/MapperUnitTest.java | 4 +- .../rbac/test/PatchUnitTestBase.java | 2 +- 77 files changed, 1558 insertions(+), 273 deletions(-) create mode 100644 doc/adr/2024-09-27-automatic-hosting-asset-creation-for-new-booking-items.md 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 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 create mode 100644 src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/factories/ManagedWebspaceHostingAssetFactory.java create mode 100644 src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/factories/ToStringConverter.java create mode 100644 src/main/java/net/hostsharing/hsadminng/lambda/Reducer.java rename src/main/java/net/hostsharing/hsadminng/{rbac/object => persistence}/BaseEntity.java (66%) create mode 100644 src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/factories/DomainSetupHostingAssetFactoryUnitTest.java create mode 100644 src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/factories/HsBookingItemCreatedListenerUnitTest.java create mode 100644 src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/factories/ManagedWebspaceHostingAssetFactoryUnitTest.java create mode 100644 src/test/java/net/hostsharing/hsadminng/persistence/EntityManagerWrapperFake.java 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. 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/BookingItemCreatedAppEvent.java b/src/main/java/net/hostsharing/hsadminng/hs/booking/item/BookingItemCreatedAppEvent.java new file mode 100644 index 00000000..6960d626 --- /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 Object 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 bea6c9ae..00000000 --- a/src/main/java/net/hostsharing/hsadminng/hs/booking/item/BookingItemCreatedEvent.java +++ /dev/null @@ -1,16 +0,0 @@ -package net.hostsharing.hsadminng.hs.booking.item; - -import lombok.Getter; -import org.springframework.context.ApplicationEvent; - -import jakarta.validation.constraints.NotNull; - -@Getter -public class BookingItemCreatedEvent extends ApplicationEvent { - private final @NotNull HsBookingItem newBookingItem; - - public BookingItemCreatedEvent(@NotNull HsBookingItemController source, @NotNull final HsBookingItem newBookingItem) { - super(source); - this.newBookingItem = newBookingItem; - } -} 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..e290313b --- /dev/null +++ b/src/main/java/net/hostsharing/hsadminng/hs/booking/item/BookingItemCreatedEventEntity.java @@ -0,0 +1,55 @@ +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.persistence.BaseEntity; + +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 +@ToString +@NoArgsConstructor +public class BookingItemCreatedEventEntity implements BaseEntity { + @Id + @Column(name="bookingitemuuid") + private UUID uuid; + + @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; + + 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/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/item/HsBookingItemController.java b/src/main/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemController.java index b3e3250e..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 @@ -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; @@ -23,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 @@ -40,6 +43,9 @@ public class HsBookingItemController implements HsBookingItemsApi { @Autowired private HsBookingItemRbacRepository bookingItemRepo; + @Autowired + private ObjectMapper jsonMapper; + @Autowired private EntityManagerWrapper em; @@ -76,8 +82,7 @@ public class HsBookingItemController implements HsBookingItemsApi { .validateContext() .mapUsing(e -> mapper.map(e, HsBookingItemResource.class, ITEM_TO_RESOURCE_POSTMAPPER)) .revampProperties(); - - applicationEventPublisher.publishEvent(new BookingItemCreatedEvent(this, saveProcessor.getEntity())); + publishSavedEvent(saveProcessor, body); final var uri = MvcUriComponentsBuilder.fromController(getClass()) @@ -137,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.getHostingAsset()))); + } catch (JsonProcessingException e) { + throw new RuntimeException(e); + } + } + final BiConsumer ITEM_TO_RESOURCE_POSTMAPPER = (entity, resource) -> { resource.setValidFrom(entity.getValidity().lower()); if (entity.getValidity().hasUpperBound()) { @@ -148,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/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..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 @@ -13,27 +13,21 @@ import static net.hostsharing.hsadminng.hs.hosting.asset.validators.Dns.REGISTRA 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}(? { - - @Autowired - private EntityManagerWrapper emw; - - @Override - public void onApplicationEvent(final BookingItemCreatedEvent event) { - System.out.println("Received newly created booking item: " + event.getNewBookingItem()); - final var newBookingItemRealEntity = - emw.getReference(HsBookingItemRealEntity.class, event.getNewBookingItem().getUuid()); - final var newHostingAsset = switch (newBookingItemRealEntity.getType()) { - case PRIVATE_CLOUD -> null; - case CLOUD_SERVER -> null; - case MANAGED_SERVER -> null; - case MANAGED_WEBSPACE -> null; - case DOMAIN_SETUP -> createDomainSetupHostingAsset(newBookingItemRealEntity); - }; - 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()); - } - } - } - - 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(); - } -} 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..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; @@ -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/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/factories/DomainSetupHostingAssetFactory.java b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/factories/DomainSetupHostingAssetFactory.java new file mode 100644 index 00000000..de6b4f02 --- /dev/null +++ b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/factories/DomainSetupHostingAssetFactory.java @@ -0,0 +1,153 @@ +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.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 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; + +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()); + final var subHostingAssets = domainSetupAsset.getSubHostingAssets(); + + // TODO.legacy: as long as we need to be compatible, we always do all technical domain-setups + + 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(assignedToUnixUserAssetEntity) + .identifier(getDomainName() + "|HTTP") + .caption("HTTP-Setup für " + IDN.toUnicode(getDomainName()))) + ); + + // 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(assignedToUnixUserAssetEntity.getParentAsset()) + .identifier(getDomainName() + "|DNS") + .caption("DNS-Setup für " + IDN.toUnicode(getDomainName()))); + + subHostingAssets.add( + createDomainSubSetupAssetEntity( + domainSetupAsset, + DOMAIN_MBOX_SETUP, + builder -> builder + .assignedToAsset(assignedToUnixUserAssetEntity.getParentAsset()) + .identifier(getDomainName() + "|MBOX") + .caption("MBOX-Setup für " + IDN.toUnicode(getDomainName()))) + ); + + subHostingAssets.add( + createDomainSubSetupAssetEntity( + domainSetupAsset, + DOMAIN_SMTP_SETUP, + builder -> builder + .assignedToAsset(assignedToUnixUserAssetEntity.getParentAsset()) + .identifier(getDomainName() + "|SMTP") + .caption("SMTP-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) + .alarmContact(ref(HsOfficeContactRealEntity.class, asset.getAlarmContactUuid())) + // the sub-hosting-assets get added later + .build(); + } + + private HsHostingAssetRealEntity createDomainSubSetupAssetEntity( + final HsHostingAssetRealEntity domainSetupAsset, + final HsHostingAssetType subAssetType, + final Function, HsHostingAssetRealEntity.HsHostingAssetRealEntityBuilder> builderTransformer) { + final var resourceType = HsHostingAssetTypeResource.valueOf(subAssetType.name()); + + final var subAssetResourceOptional = findSubHostingAssetResource(resourceType); + + 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(); + } + + 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"); + 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 compatibility mode, only default values allowed"); + } + + } + + 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..83984bb0 --- /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() { + try { + final var newHostingAsset = create(); + 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..651d5277 --- /dev/null +++ b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/factories/HsBookingItemCreatedListener.java @@ -0,0 +1,80 @@ +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; +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; +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 bookingItemCreatedAppEvent) { + if (containsAssetJson(bookingItemCreatedAppEvent)) { + createRelatedHostingAsset(bookingItemCreatedAppEvent); + } + } + + private static boolean containsAssetJson(final BookingItemCreatedAppEvent bookingItemCreatedAppEvent) { + return bookingItemCreatedAppEvent.getEntity().getAssetJson() != null; + } + + private void createRelatedHostingAsset(final BookingItemCreatedAppEvent event) throws JsonProcessingException { + 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 -> + forNowNoAutomaticHostingAssetCreationPossible(emw, newBookingItemRealEntity, asset, standardMapper); + case MANAGED_WEBSPACE -> new ManagedWebspaceHostingAssetFactory(emw, newBookingItemRealEntity, asset, standardMapper); + case DOMAIN_SETUP -> new DomainSetupHostingAssetFactory(emw, newBookingItemRealEntity, asset, standardMapper); + }; + if (factory != null) { + 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()); + } + } + } + + private HostingAssetFactory forNowNoAutomaticHostingAssetCreationPossible( + final EntityManagerWrapper emw, + final HsBookingItemRealEntity fromBookingItem, + final HsHostingAssetAutoInsertResource asset, + final StandardMapper standardMapper + ) { + return new HostingAssetFactory(emw, fromBookingItem, asset, standardMapper) { + + @Override + protected HsHostingAsset create() { + // TODO.impl: we should validate the asset JSON, but some violations are un-avoidable at that stage + return null; + } + + @Override + public String performSaveProcess() { + return "waiting for manual setup of hosting asset for booking item of type " + fromBookingItem.getType(); + } + }; + } +} 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..e820ad40 --- /dev/null +++ b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/factories/ManagedWebspaceHostingAssetFactory.java @@ -0,0 +1,51 @@ +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 java.util.Optional; + + +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 " + + 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; + } + + @Override + protected void persist(final HsHostingAsset newManagedWebspaceHostingAsset) { + super.persist(newManagedWebspaceHostingAsset); + } +} 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..bf0ec002 --- /dev/null +++ b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/factories/ToStringConverter.java @@ -0,0 +1,37 @@ +package net.hostsharing.hsadminng.hs.hosting.asset.factories; + +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/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/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/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/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/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/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/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..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 @@ -56,6 +56,10 @@ components: type: string format: uuid nullable: false + parentItemUuid: + type: string + format: uuid + nullable: false type: $ref: '#/components/schemas/HsBookingItemType' caption: @@ -69,6 +73,8 @@ components: nullable: true resources: $ref: '#/components/schemas/BookingResources' + hostingAsset: + $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..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 @@ -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: + type: + $ref: '#/components/schemas/HsHostingAssetType' + identifier: + type: string + minLength: 3 + maxLength: 80 + nullable: false + caption: + type: string + minLength: 3 + maxLength: 80 + nullable: false + assignedToAssetUuid: + type: string + format: uuid + 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/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 cf19aa32..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 @@ -31,6 +31,20 @@ create table if not exists hs_booking.item --// +-- ============================================================================ +--changeset michael.hoennig:hs-booking-item-EVENT-TABLE endDelimiter:--// +-- ---------------------------------------------------------------------------- + +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 +); +--// + + -- ============================================================================ --changeset michael.hoennig:hs-booking-item-MAIN-TABLE-JOURNAL endDelimiter:--// -- ---------------------------------------------------------------------------- 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/arch/ArchitectureTest.java b/src/test/java/net/hostsharing/hsadminng/arch/ArchitectureTest.java index 2190d29f..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..", @@ -64,6 +65,7 @@ public class ArchitectureTest { "..hs.booking.item.validators", "..hs.hosting.asset", "..hs.hosting.asset.validators", + "..hs.hosting.asset.factories", "..errors", "..mapper", "..ping", 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..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 @@ -5,12 +5,15 @@ 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.AfterEach; import org.junit.jupiter.api.ClassOrderer; import org.junit.jupiter.api.MethodOrderer; import org.junit.jupiter.api.Nested; @@ -31,6 +34,8 @@ 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; import static org.hamcrest.Matchers.matchesRegex; @@ -58,6 +63,9 @@ class HsBookingItemControllerAcceptanceTest extends ContextBasedTestWithCleanup @Autowired HsHostingAssetRealRepository realHostingAssetRepo; + @Autowired + BookingItemCreatedEventRepository bookingItemCreationEventRepo; + @Autowired JpaAttempt jpaAttempt; @@ -70,11 +78,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 +142,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() @@ -186,37 +186,121 @@ 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 = debitorRepo.findByDebitorNumber(1000111).stream() - .map(d -> realProjectRepo.findAllByDebitorUuid(d.getUuid())) - .flatMap(List::stream) - .findFirst() - .orElseThrow(); - - Dns.fakeResultForDomain("example.org", - Dns.Result.fromRecords("Hostsharing-domain-setup-verification-code=just-a-fake-verification-code")); + 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() .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" + { + "projectUuid": "{projectUuid}", + "parentItemUuid": "{managedServerUuid}", + "type": "MANAGED_WEBSPACE", + "caption": "some managed webspace", + "resources": { + "SSD": 25, + "Traffic": 250 + }, + "hostingAsset": { + "type": "MANAGED_WEBSPACE", + "identifier": "fir00" + } } - } - """ + """ .replace("{projectUuid}", givenProject.getUuid().toString()) + .replace("{managedServerUuid}", givenManagedServer.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); + // TODO.impl: "sec01-web" should not work, but does + 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": "Domain-Setup for example.org", + "resources": { + "domainName": "example.org", + "verificationCode": "just-a-fake-verification-code" + }, + "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()) + ) + .port(port) .when() .post("http://localhost/api/hs/booking/items") .then().log().all().assertThat() @@ -225,10 +309,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 +323,34 @@ 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); + // TODO.legacy: add check for example.org|DNS + 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 event = bookingItemCreationEventRepo.findByBookingItem(newBookingItem); + assertThat(event).isNull(); } @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 +365,30 @@ class HsBookingItemControllerAcceptanceTest extends ContextBasedTestWithCleanup "caption": "some new domain-setup booking", "resources": { "domainName": "example.org", - "targetUnixUser": "fir01-web", "verificationCode": "just-a-fake-verification-code" + }, + "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()) ) .port(port) .when() @@ -291,7 +402,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 +416,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 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 @@ -314,6 +425,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) @@ -534,6 +653,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); } 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}(?... givenResources) { + return HsBookingItemRealEntity.builder() + .type(HsBookingItemType.DOMAIN_SETUP) + .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/hosting/asset/factories/HsBookingItemCreatedListenerUnitTest.java b/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/factories/HsBookingItemCreatedListenerUnitTest.java new file mode 100644 index 00000000..07442a71 --- /dev/null +++ b/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/factories/HsBookingItemCreatedListenerUnitTest.java @@ -0,0 +1,98 @@ +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.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.extension.ExtendWith; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; +import org.mockito.InjectMocks; +import org.mockito.Spy; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.util.Arrays; +import java.util.List; + +import static net.hostsharing.hsadminng.hs.booking.item.HsBookingItemType.DOMAIN_SETUP; +import static net.hostsharing.hsadminng.hs.booking.item.HsBookingItemType.MANAGED_WEBSPACE; +import static org.assertj.core.api.Assertions.assertThat; + +@ExtendWith(MockitoExtension.class) +class HsBookingItemCreatedListenerUnitTest { + + final HsBookingDebitorEntity debitor = HsBookingDebitorEntity.builder() + .debitorNumber(12345) + .defaultPrefix("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; + + @ParameterizedTest + @MethodSource("bookingItemTypesWithoutAutomaticAssetCreation") + void persistsEventEntityIfBookingItemTypeDoesNotSupportAutomaticHostingAssetCreation(final HsBookingItemType bookingItemType) { + // given + final var givenBookingItem = createBookingItemFromResources(bookingItemType); + final var givenAssetJson = """ + { + // anything should be rejected + } + """; + + // when + listener.onApplicationEvent( + new BookingItemCreatedAppEvent(this, givenBookingItem, givenAssetJson) + ); + + // then + assertEventStatus(givenBookingItem, givenAssetJson, + "waiting for manual setup of hosting asset for booking item of type " + bookingItemType); + } + + static List 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/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/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() 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/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); 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/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/hs/office/partner/HsOfficePartnerControllerRestTest.java b/src/test/java/net/hostsharing/hsadminng/hs/office/partner/HsOfficePartnerControllerRestTest.java index 2af222dc..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 @@ -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; @@ -18,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; @@ -57,7 +57,7 @@ class HsOfficePartnerControllerRestTest { HsOfficeRelationRealRepository relationRepo; @MockBean - EntityManager em; + EntityManagerWrapper em; @MockBean EntityManagerFactory emf; 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..e1ce8e2e --- /dev/null +++ b/src/test/java/net/hostsharing/hsadminng/persistence/EntityManagerWrapperFake.java @@ -0,0 +1,94 @@ +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<>(); + + @Override + public boolean contains(final Object entity) { + final var id = getEntityId(entity); + 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) { + 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()); + } +} 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..0f1abce6 100644 --- a/src/test/java/net/hostsharing/hsadminng/rbac/role/RbacRoleControllerRestTest.java +++ b/src/test/java/net/hostsharing/hsadminng/rbac/role/RbacRoleControllerRestTest.java @@ -2,10 +2,10 @@ 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; -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,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; @@ -43,8 +42,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..5d64903f 100644 --- a/src/test/java/net/hostsharing/hsadminng/rbac/test/MapperUnitTest.java +++ b/src/test/java/net/hostsharing/hsadminng/rbac/test/MapperUnitTest.java @@ -3,13 +3,13 @@ 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; 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; @@ -24,7 +24,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;