From 8ee04ecee3589dfbff08a3e57e96da79cac82be6 Mon Sep 17 00:00:00 2001 From: Michael Hoennig Date: Mon, 30 Sep 2024 15:21:02 +0200 Subject: [PATCH] HsBookingItemControllerAcceptanceTest passing --- .../config/JsonObjectMapperConfiguration.java | 2 +- .../hsadminng/hash/HashGenerator.java | 2 +- .../booking/item/BookingItemCreatedEvent.java | 50 ++++- .../booking/item/HsBookingItemController.java | 12 +- .../BookingItemEntitySaveProcessor.java | 2 +- .../HsDomainSetupBookingItemValidator.java | 26 +-- .../asset/HsBookingItemCreatedListener.java | 171 +++++++++++++++--- .../asset/HsHostingAssetRealRepository.java | 13 ++ .../HostingAssetEntitySaveProcessor.java | 2 +- ...HsDomainDnsSetupHostingAssetValidator.java | 4 +- .../hs/validation/PasswordProperty.java | 2 +- .../hs-booking/hs-booking-item-schemas.yaml | 13 ++ .../hs-hosting/hs-hosting-asset-schemas.yaml | 63 ++++++- .../5016-hs-office-contact-migration.sql | 2 +- .../5046-hs-office-partner-migration.sql | 2 +- .../5076-hs-office-sepamandate-migration.sql | 2 +- .../5116-hs-office-coopshares-migration.sql | 2 +- .../5126-hs-office-coopassets-migration.sql | 2 +- .../7010-hs-hosting-asset.sql | 2 +- .../7016-hs-hosting-asset-migration.sql | 2 +- ...HsBookingItemControllerAcceptanceTest.java | 125 ++++++++----- .../hs/migration/BaseOfficeDataImport.java | 2 +- 22 files changed, 386 insertions(+), 117 deletions(-) diff --git a/src/main/java/net/hostsharing/hsadminng/config/JsonObjectMapperConfiguration.java b/src/main/java/net/hostsharing/hsadminng/config/JsonObjectMapperConfiguration.java index 921d0c34..b7011f7f 100644 --- a/src/main/java/net/hostsharing/hsadminng/config/JsonObjectMapperConfiguration.java +++ b/src/main/java/net/hostsharing/hsadminng/config/JsonObjectMapperConfiguration.java @@ -17,7 +17,7 @@ public class JsonObjectMapperConfiguration { public Jackson2ObjectMapperBuilder customObjectMapper() { return new Jackson2ObjectMapperBuilder() .modules(new JsonNullableModule(), new JavaTimeModule()) - .featuresToEnable(JsonParser.Feature.ALLOW_COMMENTS) + .featuresToEnable(JsonParser.Feature.ALLOW_COMMENTS, JsonParser.Feature.ALLOW_COMMENTS) .featuresToDisable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS); } } diff --git a/src/main/java/net/hostsharing/hsadminng/hash/HashGenerator.java b/src/main/java/net/hostsharing/hsadminng/hash/HashGenerator.java index cd16b697..268375eb 100644 --- a/src/main/java/net/hostsharing/hsadminng/hash/HashGenerator.java +++ b/src/main/java/net/hostsharing/hsadminng/hash/HashGenerator.java @@ -27,7 +27,7 @@ public final class HashGenerator { "abcdefghijklmnopqrstuvwxyz" + "ABCDEFGHIJKLMNOPQRSTUVWXYZ" + "0123456789/."; - private static boolean couldBeHashEnabled; // TODO.impl: remove after legacy data is migrated + private static boolean couldBeHashEnabled; // TODO.legacy: remove after legacy data is migrated public enum Algorithm { LINUX_SHA512(LinuxEtcShadowHashGenerator::hash, "6"), diff --git a/src/main/java/net/hostsharing/hsadminng/hs/booking/item/BookingItemCreatedEvent.java b/src/main/java/net/hostsharing/hsadminng/hs/booking/item/BookingItemCreatedEvent.java index bea6c9ae..1b723353 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/booking/item/BookingItemCreatedEvent.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/booking/item/BookingItemCreatedEvent.java @@ -4,13 +4,57 @@ import lombok.Getter; import org.springframework.context.ApplicationEvent; import jakarta.validation.constraints.NotNull; +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; @Getter public class BookingItemCreatedEvent extends ApplicationEvent { - private final @NotNull HsBookingItem newBookingItem; - public BookingItemCreatedEvent(@NotNull HsBookingItemController source, @NotNull final HsBookingItem newBookingItem) { + private static final Map events = new HashMap<>(); // FIXME: use DB table + + static BookingItemCreatedEvent of(final HsBookingItemRealEntity bookingItem) { + return events.get(bookingItem.getUuid()); + } + + @Getter + public static class Status { + + private final String message; + + private Status(final String message) { + this.message = message; + } + + public static Status finished() { + return new Status(null); + } + + public static Status failed(final String errorMessage) { + return new Status(errorMessage); + } + + boolean isFinished() { + return message == null; + } + } + + private final @NotNull UUID bookingItemUuid; + private final @NotNull String assetJson; + + private Status status; + + public BookingItemCreatedEvent( + @NotNull final HsBookingItemController source, + @NotNull final HsBookingItem newBookingItem, + final String assetJson) { super(source); - this.newBookingItem = newBookingItem; + this.bookingItemUuid = newBookingItem.getUuid(); + this.assetJson = assetJson; + } + + public void setStatus(final Status status) { + this.status = status; + events.put(bookingItemUuid, this); } } diff --git a/src/main/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemController.java b/src/main/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemController.java index b3e3250e..801fff0f 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemController.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemController.java @@ -1,5 +1,7 @@ package net.hostsharing.hsadminng.hs.booking.item; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; import net.hostsharing.hsadminng.context.Context; import net.hostsharing.hsadminng.hs.booking.generated.api.v1.api.HsBookingItemsApi; import net.hostsharing.hsadminng.hs.booking.generated.api.v1.model.HsBookingItemInsertResource; @@ -40,6 +42,9 @@ public class HsBookingItemController implements HsBookingItemsApi { @Autowired private HsBookingItemRbacRepository bookingItemRepo; + @Autowired + private ObjectMapper jsonMapper; + @Autowired private EntityManagerWrapper em; @@ -77,7 +82,12 @@ public class HsBookingItemController implements HsBookingItemsApi { .mapUsing(e -> mapper.map(e, HsBookingItemResource.class, ITEM_TO_RESOURCE_POSTMAPPER)) .revampProperties(); - applicationEventPublisher.publishEvent(new BookingItemCreatedEvent(this, saveProcessor.getEntity())); + try { + applicationEventPublisher.publishEvent(new BookingItemCreatedEvent( + this, saveProcessor.getEntity(), jsonMapper.writeValueAsString(body.getAsset()))); + } catch (JsonProcessingException e) { + throw new RuntimeException(e); + } final var uri = MvcUriComponentsBuilder.fromController(getClass()) diff --git a/src/main/java/net/hostsharing/hsadminng/hs/booking/item/validators/BookingItemEntitySaveProcessor.java b/src/main/java/net/hostsharing/hsadminng/hs/booking/item/validators/BookingItemEntitySaveProcessor.java index 1e712ad3..b1680084 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/booking/item/validators/BookingItemEntitySaveProcessor.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/booking/item/validators/BookingItemEntitySaveProcessor.java @@ -48,7 +48,7 @@ public class BookingItemEntitySaveProcessor { return this; } - // TODO.impl: remove once the migration of legacy data is done + // TODO.legacy: remove once the migration of legacy data is done /// validates the entity itself including its properties, but ignoring some error messages for import of legacy data public BookingItemEntitySaveProcessor validateEntityIgnoring(final String... ignoreRegExp) { step("validateEntity", "prepareForSave"); diff --git a/src/main/java/net/hostsharing/hsadminng/hs/booking/item/validators/HsDomainSetupBookingItemValidator.java b/src/main/java/net/hostsharing/hsadminng/hs/booking/item/validators/HsDomainSetupBookingItemValidator.java index 266ff641..5c12d21a 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/booking/item/validators/HsDomainSetupBookingItemValidator.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/booking/item/validators/HsDomainSetupBookingItemValidator.java @@ -1,39 +1,30 @@ package net.hostsharing.hsadminng.hs.booking.item.validators; -import net.hostsharing.hsadminng.hs.booking.item.HsBookingItem; import net.hostsharing.hsadminng.hs.validation.PropertiesProvider; import jakarta.persistence.EntityManager; import java.security.SecureRandom; -import java.util.ArrayList; -import java.util.List; import static net.hostsharing.hsadminng.hs.hosting.asset.validators.Dns.REGISTRAR_LEVEL_DOMAINS; import static net.hostsharing.hsadminng.hs.validation.StringProperty.stringProperty; class HsDomainSetupBookingItemValidator extends HsBookingItemEntityValidator { + public static final String DOMAIN_NAME_PROPERTY_NAME = "domainName"; public static final String FQDN_REGEX = "^((?!-)[A-Za-z0-9-]{1,63}(? validateEntity(final HsBookingItem bookingItem) { - final var violations = new ArrayList(); - final var domainName = bookingItem.getDirectValue(DOMAIN_NAME_PROPERTY_NAME, String.class); - if (!bookingItem.isLoaded() && - domainName.matches("hostsharing.(com|net|org|coop|de)")) { - violations.add("'" + bookingItem.toShortString() + ".resources." + DOMAIN_NAME_PROPERTY_NAME + "' = '" + domainName - + "' is a forbidden Hostsharing domain name"); - } - violations.addAll(super.validateEntity(bookingItem)); - return violations; - } - private static String generateVerificationCode(final EntityManager em, final PropertiesProvider propertiesProvider) { final var userDefinedVerificationCode = propertiesProvider.getDirectValue(VERIFICATION_CODE_PROPERTY_NAME, String.class); if (userDefinedVerificationCode != null) { diff --git a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/HsBookingItemCreatedListener.java b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/HsBookingItemCreatedListener.java index c625076a..4312f5d3 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/HsBookingItemCreatedListener.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/HsBookingItemCreatedListener.java @@ -1,14 +1,31 @@ package net.hostsharing.hsadminng.hs.hosting.asset; +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.RequiredArgsConstructor; +import lombok.SneakyThrows; +import net.hostsharing.hsadminng.hs.booking.generated.api.v1.model.HsHostingAssetAutoInsertResource; +import net.hostsharing.hsadminng.hs.booking.generated.api.v1.model.HsHostingAssetTypeResource; import net.hostsharing.hsadminng.hs.booking.item.BookingItemCreatedEvent; +import net.hostsharing.hsadminng.hs.booking.item.BookingItemCreatedEvent.Status; import net.hostsharing.hsadminng.hs.booking.item.HsBookingItemRealEntity; import net.hostsharing.hsadminng.hs.hosting.asset.validators.HostingAssetEntitySaveProcessor; +import net.hostsharing.hsadminng.hs.office.contact.HsOfficeContactRealEntity; +import net.hostsharing.hsadminng.mapper.StandardMapper; import net.hostsharing.hsadminng.persistence.EntityManagerWrapper; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.ApplicationListener; import org.springframework.stereotype.Component; -import java.util.List; +import jakarta.validation.ValidationException; +import java.net.IDN; +import java.util.Map; +import java.util.UUID; + +import static java.util.Map.entry; +import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.DOMAIN_DNS_SETUP; +import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.DOMAIN_HTTP_SETUP; +import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.DOMAIN_MBOX_SETUP; +import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.DOMAIN_SMTP_SETUP; @Component public class HsBookingItemCreatedListener implements ApplicationListener { @@ -16,41 +33,143 @@ public class HsBookingItemCreatedListener implements ApplicationListener null; case CLOUD_SERVER -> null; case MANAGED_SERVER -> null; case MANAGED_WEBSPACE -> null; - case DOMAIN_SETUP -> createDomainSetupHostingAsset(newBookingItemRealEntity); + case DOMAIN_SETUP -> new DomainSetupHostingAssetFactory(newBookingItemRealEntity, asset); }; - if (newHostingAsset != null) { - try { - new HostingAssetEntitySaveProcessor(emw, newHostingAsset) - .preprocessEntity() - .validateEntity() - .prepareForSave() - .save() - .validateContext(); - } catch (final Exception e) { - // TODO.impl: store status in a separate field, maybe enum+message - newBookingItemRealEntity.getResources().put("status", e.getMessage()); - } + if (factory != null) { + event.setStatus(factory.performSaveProcess()); } } - private HsHostingAsset createDomainSetupHostingAsset(final HsBookingItemRealEntity fromBookingItem) { - return HsHostingAssetRbacEntity.builder() - .bookingItem(fromBookingItem) - .type(HsHostingAssetType.DOMAIN_SETUP) - .identifier(fromBookingItem.getDirectValue("domainName", String.class)) - .subHostingAssets(List.of( - // TARGET_UNIX_USER_PROPERTY_NAME - )) - .build(); + private T ref(final Class entityClass, final UUID uuid) { + return uuid != null ? emw.getReference(entityClass, uuid) : null; + } + + @RequiredArgsConstructor + abstract class HostingAssetFactory { + + final HsBookingItemRealEntity fromBookingItem; + final HsHostingAssetAutoInsertResource asset; + + protected HsHostingAsset create() { + return null; + } + + Status performSaveProcess() { + final var newHostingAsset = create(); + try { + return persist(newHostingAsset); + } catch (final Exception e) { + return Status.failed(e.getMessage()); + } + } + + protected Status persist(final HsHostingAsset newHostingAsset) { + new HostingAssetEntitySaveProcessor(emw, newHostingAsset) + .preprocessEntity() + .validateEntity() + .prepareForSave() + .save() + .validateContext(); + return Status.finished(); + } + } + + class DomainSetupHostingAssetFactory extends HostingAssetFactory { + + public DomainSetupHostingAssetFactory( + final HsBookingItemRealEntity newBookingItemRealEntity, + final HsHostingAssetAutoInsertResource asset) { + super(newBookingItemRealEntity, asset); + } + + @Override + protected HsHostingAsset create() { + final String domainName = asset.getIdentifier(); + final var domainSetupAsset = createDomainSetupAsset(domainName); + + // TODO.legacy: as long as we need to be compatible, we always do all technical domain-setups + final var subHostingAssetResources = asset.getSubHostingAssets(); + final var domainHttpSetupAssetResource = subHostingAssetResources.stream() + .filter(ha -> ha.getType() == HsHostingAssetTypeResource.DOMAIN_HTTP_SETUP) + .findFirst().orElseThrow(() -> new ValidationException( + domainName + ": missing target unix user (assignedToHostingAssetUuid) for DOMAIN_HTTP_SETUP ")); + final var domainHttpSetupAsset = domainSetupAsset.getSubHostingAssets().stream().filter(sha -> sha.getType() == DOMAIN_HTTP_SETUP).findFirst().orElseThrow(); + domainHttpSetupAsset.setParentAsset(domainSetupAsset); + final HsHostingAssetRealEntity assignedToUnixUserAsset = + emw.find(HsHostingAssetRealEntity.class, domainHttpSetupAssetResource.getAssignedToAssetUuid()); + domainHttpSetupAsset.setAssignedToAsset(assignedToUnixUserAsset); + + if (subHostingAssetResources.stream().noneMatch(ha -> ha.getType() == HsHostingAssetTypeResource.DOMAIN_DNS_SETUP)) { + domainSetupAsset.getSubHostingAssets().add(HsHostingAssetRealEntity.builder() + .type(DOMAIN_DNS_SETUP) + .parentAsset(domainSetupAsset) + .assignedToAsset(assignedToUnixUserAsset.getParentAsset()) // FIXME: why is that needed? + .identifier(domainName + "|DNS") + .config(Map.ofEntries( + // FIXME: + entry("TTL", 21600), + entry("auto-SOA", true) + )) + .build()); + } + if (subHostingAssetResources.stream().noneMatch(ha -> ha.getType() == HsHostingAssetTypeResource.DOMAIN_MBOX_SETUP)) { + domainSetupAsset.getSubHostingAssets().add(HsHostingAssetRealEntity.builder() + .type(DOMAIN_MBOX_SETUP) + .parentAsset(domainSetupAsset) + .assignedToAsset(assignedToUnixUserAsset.getParentAsset()) + .identifier(domainName + "|MBOX") + .caption("HTTP-Setup für " + IDN.toUnicode(domainName)) + .build()); + } + if (subHostingAssetResources.stream().noneMatch(ha -> ha.getType() == HsHostingAssetTypeResource.DOMAIN_SMTP_SETUP)) { + domainSetupAsset.getSubHostingAssets().add(HsHostingAssetRealEntity.builder() + .type(DOMAIN_SMTP_SETUP) + .parentAsset(domainSetupAsset) + .assignedToAsset(assignedToUnixUserAsset.getParentAsset()) + .identifier(domainName + "|SMTP") + .caption("HTTP-Setup für " + IDN.toUnicode(domainName)) + .build()); + } + return domainSetupAsset; + } + + private HsHostingAssetRealEntity createDomainSetupAsset(final String domainName) { + return HsHostingAssetRealEntity.builder() + .bookingItem(fromBookingItem) + .type(HsHostingAssetType.DOMAIN_SETUP) + .identifier(domainName) + .caption(asset.getCaption() != null ? asset.getCaption() : domainName) + .parentAsset(ref(HsHostingAssetRealEntity.class, asset.getParentAssetUuid())) + .alarmContact(ref(HsOfficeContactRealEntity.class, asset.getAlarmContactUuid())) + // FIXME .config(asset.getConfig()) + .subHostingAssets( + standardMapper.mapList(asset.getSubHostingAssets(), HsHostingAssetRealEntity.class) + ) + .build(); + } + + @Override + protected Status persist(final HsHostingAsset newHostingAsset) { + final var status = super.persist(newHostingAsset); + newHostingAsset.getSubHostingAssets().forEach(super::persist); + return status; + } } } diff --git a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetRealRepository.java b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetRealRepository.java index 1e177524..c3709039 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetRealRepository.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetRealRepository.java @@ -3,6 +3,7 @@ package net.hostsharing.hsadminng.hs.hosting.asset; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.Repository; +import jakarta.validation.constraints.NotNull; import java.util.List; import java.util.Optional; import java.util.UUID; @@ -13,6 +14,18 @@ public interface HsHostingAssetRealRepository extends HsHostingAssetRepository findByIdentifier(String assetIdentifier); + default List findByTypeAndIdentifier(@NotNull HsHostingAssetType type, @NotNull String identifier) { + return findByTypeAndIdentifierImpl(type.name(), identifier); + } + + @Query(""" + select ha + from HsHostingAssetRealEntity ha + where cast(ha.type as String) = :type + and ha.identifier = :identifier + """) + List findByTypeAndIdentifierImpl(@NotNull String type, @NotNull String identifier); + @Query(value = """ select ha.uuid, ha.alarmcontactuuid, diff --git a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HostingAssetEntitySaveProcessor.java b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HostingAssetEntitySaveProcessor.java index 3e5850e5..e6c43be2 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HostingAssetEntitySaveProcessor.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HostingAssetEntitySaveProcessor.java @@ -42,7 +42,7 @@ public class HostingAssetEntitySaveProcessor { return this; } - // TODO.impl: remove once the migration of legacy data is done + // TODO.legacy: remove once the migration of legacy data is done /// validates the entity itself including its properties, but ignoring some error messages for import of legacy data public HostingAssetEntitySaveProcessor validateEntityIgnoring(final String... ignoreRegExp) { step("validateEntity", "prepareForSave"); diff --git a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsDomainDnsSetupHostingAssetValidator.java b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsDomainDnsSetupHostingAssetValidator.java index 57ffc279..72738416 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsDomainDnsSetupHostingAssetValidator.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsDomainDnsSetupHostingAssetValidator.java @@ -15,7 +15,7 @@ import static net.hostsharing.hsadminng.hs.validation.BooleanProperty.booleanPro import static net.hostsharing.hsadminng.hs.validation.IntegerProperty.integerProperty; import static net.hostsharing.hsadminng.hs.validation.StringProperty.stringProperty; -// TODO.impl: make package private once we've migrated the legacy data +// TODO.legacy: make package private once we've migrated the legacy data public class HsDomainDnsSetupHostingAssetValidator extends HostingAssetEntityValidator { // according to RFC 1035 (section 5) and RFC 1034 @@ -33,7 +33,7 @@ public class HsDomainDnsSetupHostingAssetValidator extends HostingAssetEntityVal RR_REGEX_NAME + RR_REGEX_IN + RR_REGEX_TTL + RR_RECORD_TYPE + RR_RECORD_DATA + RR_COMMENT; public static final String IDENTIFIER_SUFFIX = "|DNS"; - private static List zoneFileErrors = null; // TODO.impl: remove once legacy data is migrated + private static List zoneFileErrors = null; // TODO.legacy: remove once legacy data is migrated HsDomainDnsSetupHostingAssetValidator() { super( diff --git a/src/main/java/net/hostsharing/hsadminng/hs/validation/PasswordProperty.java b/src/main/java/net/hostsharing/hsadminng/hs/validation/PasswordProperty.java index ceaf2603..52cc3931 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/validation/PasswordProperty.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/validation/PasswordProperty.java @@ -31,7 +31,7 @@ public class PasswordProperty extends StringProperty { @Override protected void validate(final List result, final String propValue, final PropertiesProvider propProvider) { - // TODO.impl: remove after legacy data is migrated + // TODO.legacy: remove after legacy data is migrated if (HashGenerator.using(hashedUsing).couldBeHash(propValue) && propValue.length() > this.maxLength()) { // already hashed => do not validate return; diff --git a/src/main/resources/api-definition/hs-booking/hs-booking-item-schemas.yaml b/src/main/resources/api-definition/hs-booking/hs-booking-item-schemas.yaml index 92875b90..22d17ce3 100644 --- a/src/main/resources/api-definition/hs-booking/hs-booking-item-schemas.yaml +++ b/src/main/resources/api-definition/hs-booking/hs-booking-item-schemas.yaml @@ -58,6 +58,17 @@ components: nullable: false type: $ref: '#/components/schemas/HsBookingItemType' + identifier: + type: string + minLength: 3 + maxLength: 80 + nullable: false + description: only used as a default value for automatically created hosting assets, not part of the booking item + assignedToHostingAssetUuid: + type: string + format: uuid + nullable: false + description: only used as a default value for automatically created hosting assets, not part of the booking item caption: type: string minLength: 3 @@ -69,6 +80,8 @@ components: nullable: true resources: $ref: '#/components/schemas/BookingResources' + asset: + $ref: '../hs-hosting/hs-hosting-asset-schemas.yaml#/components/schemas/HsHostingAssetAutoInsert' required: - caption - projectUuid diff --git a/src/main/resources/api-definition/hs-hosting/hs-hosting-asset-schemas.yaml b/src/main/resources/api-definition/hs-hosting/hs-hosting-asset-schemas.yaml index b65a8a51..9ab6cf07 100644 --- a/src/main/resources/api-definition/hs-hosting/hs-hosting-asset-schemas.yaml +++ b/src/main/resources/api-definition/hs-hosting/hs-hosting-asset-schemas.yaml @@ -94,7 +94,68 @@ components: - type - identifier - caption - - config + additionalProperties: false + + HsHostingAssetAutoInsert: + type: object + properties: + parentAssetUuid: + type: string + format: uuid + nullable: true + assignedToAssetUuid: + type: string + format: uuid + type: + $ref: '#/components/schemas/HsHostingAssetType' + identifier: + type: string + minLength: 3 + maxLength: 80 + nullable: false + caption: + type: string + minLength: 3 + maxLength: 80 + nullable: false + alarmContactUuid: + type: string + format: uuid + nullable: true + config: + $ref: '#/components/schemas/HsHostingAssetConfiguration' + subHostingAssets: + type: array + items: + $ref: '#/components/schemas/HsHostingAssetSubInsert' + required: + - identifier + additionalProperties: false + + HsHostingAssetSubInsert: + type: object + properties: + assignedToAssetUuid: + type: string + format: uuid + type: + $ref: '#/components/schemas/HsHostingAssetType' + identifier: + type: string + minLength: 3 + maxLength: 80 + nullable: false + caption: + type: string + minLength: 3 + maxLength: 80 + nullable: false + alarmContactUuid: + type: string + format: uuid + nullable: true + config: + $ref: '#/components/schemas/HsHostingAssetConfiguration' additionalProperties: false HsHostingAssetConfiguration: diff --git a/src/main/resources/db/changelog/5-hs-office/501-contact/5016-hs-office-contact-migration.sql b/src/main/resources/db/changelog/5-hs-office/501-contact/5016-hs-office-contact-migration.sql index 4e0683a8..c9cb699f 100644 --- a/src/main/resources/db/changelog/5-hs-office/501-contact/5016-hs-office-contact-migration.sql +++ b/src/main/resources/db/changelog/5-hs-office/501-contact/5016-hs-office-contact-migration.sql @@ -1,6 +1,6 @@ --liquibase formatted sql --- TODO: These changesets are just for the external remote views to simulate the legacy tables. +-- TODO.legacy: These changesets are just for the external remote views to simulate the legacy tables. -- Once we don't need the external remote views anymore, create revert changesets. -- ============================================================================ diff --git a/src/main/resources/db/changelog/5-hs-office/504-partner/5046-hs-office-partner-migration.sql b/src/main/resources/db/changelog/5-hs-office/504-partner/5046-hs-office-partner-migration.sql index 0a4da2cd..8118102e 100644 --- a/src/main/resources/db/changelog/5-hs-office/504-partner/5046-hs-office-partner-migration.sql +++ b/src/main/resources/db/changelog/5-hs-office/504-partner/5046-hs-office-partner-migration.sql @@ -1,6 +1,6 @@ --liquibase formatted sql --- TODO: These changesets are just for the external remote views to simulate the legacy tables. +-- TODO.legacy: These changesets are just for the external remote views to simulate the legacy tables. -- Once we don't need the external remote views anymore, create revert changesets. -- ============================================================================ diff --git a/src/main/resources/db/changelog/5-hs-office/507-sepamandate/5076-hs-office-sepamandate-migration.sql b/src/main/resources/db/changelog/5-hs-office/507-sepamandate/5076-hs-office-sepamandate-migration.sql index 977bd8d0..c92b0ea1 100644 --- a/src/main/resources/db/changelog/5-hs-office/507-sepamandate/5076-hs-office-sepamandate-migration.sql +++ b/src/main/resources/db/changelog/5-hs-office/507-sepamandate/5076-hs-office-sepamandate-migration.sql @@ -1,6 +1,6 @@ --liquibase formatted sql --- TODO: These changesets are just for the external remote views to simulate the legacy tables. +-- TODO.legacy: These changesets are just for the external remote views to simulate the legacy tables. -- Once we don't need the external remote views anymore, create revert changesets. -- ============================================================================ diff --git a/src/main/resources/db/changelog/5-hs-office/511-coopshares/5116-hs-office-coopshares-migration.sql b/src/main/resources/db/changelog/5-hs-office/511-coopshares/5116-hs-office-coopshares-migration.sql index 1e4efb42..8e620bcd 100644 --- a/src/main/resources/db/changelog/5-hs-office/511-coopshares/5116-hs-office-coopshares-migration.sql +++ b/src/main/resources/db/changelog/5-hs-office/511-coopshares/5116-hs-office-coopshares-migration.sql @@ -1,6 +1,6 @@ --liquibase formatted sql --- TODO: These changesets are just for the external remote views to simulate the legacy tables. +-- TODO.legacy: These changesets are just for the external remote views to simulate the legacy tables. -- Once we don't need the external remote views anymore, create revert changesets. -- ============================================================================ diff --git a/src/main/resources/db/changelog/5-hs-office/512-coopassets/5126-hs-office-coopassets-migration.sql b/src/main/resources/db/changelog/5-hs-office/512-coopassets/5126-hs-office-coopassets-migration.sql index 1bb0d500..f8d5f610 100644 --- a/src/main/resources/db/changelog/5-hs-office/512-coopassets/5126-hs-office-coopassets-migration.sql +++ b/src/main/resources/db/changelog/5-hs-office/512-coopassets/5126-hs-office-coopassets-migration.sql @@ -1,6 +1,6 @@ --liquibase formatted sql --- TODO: These changesets are just for the external remote views to simulate the legacy tables. +-- TODO.legacy: These changesets are just for the external remote views to simulate the legacy tables. -- Once we don't need the external remote views anymore, create revert changesets. -- ============================================================================ diff --git a/src/main/resources/db/changelog/7-hs-hosting/701-hosting-asset/7010-hs-hosting-asset.sql b/src/main/resources/db/changelog/7-hs-hosting/701-hosting-asset/7010-hs-hosting-asset.sql index d8d1393e..ed2fdd30 100644 --- a/src/main/resources/db/changelog/7-hs-hosting/701-hosting-asset/7010-hs-hosting-asset.sql +++ b/src/main/resources/db/changelog/7-hs-hosting/701-hosting-asset/7010-hs-hosting-asset.sql @@ -41,7 +41,7 @@ create table if not exists hs_hosting.asset config jsonb not null, alarmContactUuid uuid null references hs_office.contact(uuid) initially deferred, - unique (type, identifier), -- at least as long as we need to be compatible to the legacy system + unique (type, identifier), -- TODO.legacy: at least as long as we need to be compatible to the legacy system constraint hosting_asset_has_booking_item_or_parent_asset check (bookingItemUuid is not null or parentAssetUuid is not null or type in ('DOMAIN_SETUP', 'IPV4_NUMBER', 'IPV6_NUMBER')) diff --git a/src/main/resources/db/changelog/7-hs-hosting/701-hosting-asset/7016-hs-hosting-asset-migration.sql b/src/main/resources/db/changelog/7-hs-hosting/701-hosting-asset/7016-hs-hosting-asset-migration.sql index b6edea34..a0305e8d 100644 --- a/src/main/resources/db/changelog/7-hs-hosting/701-hosting-asset/7016-hs-hosting-asset-migration.sql +++ b/src/main/resources/db/changelog/7-hs-hosting/701-hosting-asset/7016-hs-hosting-asset-migration.sql @@ -1,6 +1,6 @@ --liquibase formatted sql --- TODO: These changesets are just for the external remote views to simulate the legacy tables. +-- TODO.legacy: These changesets are just for the external remote views to simulate the legacy tables. -- Once we don't need the external remote views anymore, create revert changesets. -- ============================================================================ diff --git a/src/test/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemControllerAcceptanceTest.java b/src/test/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemControllerAcceptanceTest.java index cf43f8cb..ddce12f8 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemControllerAcceptanceTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemControllerAcceptanceTest.java @@ -5,12 +5,14 @@ import io.restassured.RestAssured; import io.restassured.http.ContentType; import net.hostsharing.hsadminng.HsadminNgApplication; import net.hostsharing.hsadminng.hs.booking.debitor.HsBookingDebitorRepository; +import net.hostsharing.hsadminng.hs.booking.project.HsBookingProjectRealEntity; import net.hostsharing.hsadminng.hs.booking.project.HsBookingProjectRealRepository; import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAsset; import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetRealRepository; import net.hostsharing.hsadminng.hs.hosting.asset.validators.Dns; import net.hostsharing.hsadminng.rbac.test.ContextBasedTestWithCleanup; import net.hostsharing.hsadminng.rbac.test.JpaAttempt; +import org.jetbrains.annotations.NotNull; import org.junit.jupiter.api.ClassOrderer; import org.junit.jupiter.api.MethodOrderer; import org.junit.jupiter.api.Nested; @@ -31,6 +33,7 @@ import java.util.UUID; import static java.util.Map.entry; import static java.util.Optional.ofNullable; import static net.hostsharing.hsadminng.hs.booking.item.HsBookingItemType.MANAGED_WEBSPACE; +import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.UNIX_USER; import static net.hostsharing.hsadminng.rbac.test.JsonMatcher.lenientlyEquals; import static org.assertj.core.api.Assertions.assertThat; import static org.hamcrest.Matchers.matchesRegex; @@ -70,11 +73,7 @@ class HsBookingItemControllerAcceptanceTest extends ContextBasedTestWithCleanup // given context("superuser-alex@hostsharing.net"); - final var givenProject = debitorRepo.findByDebitorNumber(1000111).stream() - .map(d -> realProjectRepo.findAllByDebitorUuid(d.getUuid())) - .flatMap(List::stream) - .findFirst() - .orElseThrow(); + final var givenProject = findDefaultProjectOfDebitorNumber(1000111); RestAssured // @formatter:off .given() @@ -138,11 +137,7 @@ class HsBookingItemControllerAcceptanceTest extends ContextBasedTestWithCleanup void globalAdmin_canAddBookingItem() { context.define("superuser-alex@hostsharing.net"); - final var givenProject = debitorRepo.findByDebitorNumber(1000111).stream() - .map(d -> realProjectRepo.findAllByDebitorUuid(d.getUuid())) - .flatMap(List::stream) - .findFirst() - .orElseThrow(); + final var givenProject = findDefaultProjectOfDebitorNumber(1000111); final var location = RestAssured // @formatter:off .given() @@ -189,34 +184,41 @@ class HsBookingItemControllerAcceptanceTest extends ContextBasedTestWithCleanup void projectAgent_canAddBookingItemWithHostingAsset() { context.define("superuser-alex@hostsharing.net", "hs_booking.project#D-1000111-D-1000111defaultproject:AGENT"); - final var givenProject = debitorRepo.findByDebitorNumber(1000111).stream() - .map(d -> realProjectRepo.findAllByDebitorUuid(d.getUuid())) - .flatMap(List::stream) - .findFirst() - .orElseThrow(); + final var givenProject = findDefaultProjectOfDebitorNumber(1000111); + final var givenUnixUser = realHostingAssetRepo.findByTypeAndIdentifier(UNIX_USER, "fir01-web").stream() + .findFirst().orElseThrow(); Dns.fakeResultForDomain("example.org", Dns.Result.fromRecords("Hostsharing-domain-setup-verification-code=just-a-fake-verification-code")); final var location = RestAssured // @formatter:off .given() - .header("current-subject", "superuser-alex@hostsharing.net") - .contentType(ContentType.JSON) - .body(""" - { - "projectUuid": "{projectUuid}", - "type": "DOMAIN_SETUP", - "caption": "some new domain-setup booking", - "resources": { - "domainName": "example.org", - "targetUnixUser": "fir01-web", - "verificationCode": "just-a-fake-verification-code" + .header("current-subject", "superuser-alex@hostsharing.net") + .contentType(ContentType.JSON) + .body(""" + { + "projectUuid": "{projectUuid}", + "type": "DOMAIN_SETUP", + "caption": "Domain-Setup for example.org", + "resources": { + "domainName": "example.org", + "verificationCode": "just-a-fake-verification-code" + }, + "asset": { // FIXME: rename to hostingAsset + "identifier": "example.org", // also as default for all subAssets + "subHostingAssets": [ + { + "type": "DOMAIN_HTTP_SETUP", + "assignedToAssetUuid": "{unixUserUuid}" + } + ] + } } - } - """ - .replace("{projectUuid}", givenProject.getUuid().toString()) - ) - .port(port) + """ + .replace("{projectUuid}", givenProject.getUuid().toString()) + .replace("{unixUserUuid}", givenUnixUser.getUuid().toString()) + ) + .port(port) .when() .post("http://localhost/api/hs/booking/items") .then().log().all().assertThat() @@ -225,10 +227,9 @@ class HsBookingItemControllerAcceptanceTest extends ContextBasedTestWithCleanup .body("", lenientlyEquals(""" { "type": "DOMAIN_SETUP", - "caption": "some new domain-setup booking", + "caption": "Domain-Setup for example.org", "validFrom": "{today}", - "validTo": null, - "resources": { "domainName": "example.org", "targetUnixUser": "fir01-web" } + "validTo": null } """ .replace("{today}", LocalDate.now().toString()) @@ -240,24 +241,37 @@ class HsBookingItemControllerAcceptanceTest extends ContextBasedTestWithCleanup // then, the new BookingItem can be accessed under the generated UUID final var newBookingItem = fetchRealBookingItemFromURI(location); assertThat(newBookingItem) - .extracting(bi -> bi.getDirectValue("domainName", String.class)) - .isEqualTo("example.org"); + .extracting(HsBookingItem::getCaption) + .isEqualTo("Domain-Setup for example.org"); - // and the related HostingAsset also got created - assertThat(realHostingAssetRepo.findByIdentifier("example.org")).isNotEmpty() + // and the related HostingAssets are also got created + final var domainSetupHostingAsset = realHostingAssetRepo.findByIdentifier("example.org"); + assertThat(domainSetupHostingAsset).isNotEmpty() .map(HsHostingAsset::getBookingItem) .contains(newBookingItem); + assertThat(realHostingAssetRepo.findByIdentifier("example.org|DNS")).isNotEmpty() + .map(HsHostingAsset::getParentAsset) + .isEqualTo(domainSetupHostingAsset); + assertThat(realHostingAssetRepo.findByIdentifier("example.org|HTTP")).isNotEmpty() + .map(HsHostingAsset::getParentAsset) + .isEqualTo(domainSetupHostingAsset); + assertThat(realHostingAssetRepo.findByIdentifier("example.org|MBOX")).isNotEmpty() + .map(HsHostingAsset::getParentAsset) + .isEqualTo(domainSetupHostingAsset); + assertThat(realHostingAssetRepo.findByIdentifier("example.org|SMTP")).isNotEmpty() + .map(HsHostingAsset::getParentAsset) + .isEqualTo(domainSetupHostingAsset); + final var status = BookingItemCreatedEvent.of(newBookingItem); + assertThat(status.getStatus().isFinished()); + } @Test void projectAgent_canAddBookingItemEvenIfHostingAssetCreationFails() { context.define("superuser-alex@hostsharing.net", "hs_booking.project#D-1000111-D-1000111defaultproject:AGENT"); - final var givenProject = debitorRepo.findByDebitorNumber(1000111).stream() - .map(d -> realProjectRepo.findAllByDebitorUuid(d.getUuid())) - .flatMap(List::stream) - .findFirst() - .orElseThrow(); + final var givenProject = findDefaultProjectOfDebitorNumber(1000111); + final var givenUnixUser = realHostingAssetRepo.findByIdentifier("fir01-web").stream().findFirst().orElseThrow(); Dns.fakeResultForDomain("example.org", Dns.Result.fromRecords()); // without valid verificationCode @@ -272,12 +286,21 @@ class HsBookingItemControllerAcceptanceTest extends ContextBasedTestWithCleanup "caption": "some new domain-setup booking", "resources": { "domainName": "example.org", - "targetUnixUser": "fir01-web", "verificationCode": "just-a-fake-verification-code" + }, + "asset": { // FIXME: rename to hostingAsset + "identifier": "example.org", // also as default for all subAssets + "subHostingAssets": [ + { + "type": "DOMAIN_HTTP_SETUP", + "assignedToAssetUuid": "{unixUserUuid}" + } + ] } } """ .replace("{projectUuid}", givenProject.getUuid().toString()) + .replace("{unixUserUuid}", givenUnixUser.getUuid().toString()) ) .port(port) .when() @@ -291,7 +314,7 @@ class HsBookingItemControllerAcceptanceTest extends ContextBasedTestWithCleanup "caption": "some new domain-setup booking", "validFrom": "{today}", "validTo": null, - "resources": { "domainName": "example.org", "targetUnixUser": "fir01-web" } + "resources": { "domainName": "example.org" } } """ .replace("{today}", LocalDate.now().toString()) @@ -305,8 +328,8 @@ class HsBookingItemControllerAcceptanceTest extends ContextBasedTestWithCleanup assertThat(newBookingItem) .extracting(bi -> bi.getDirectValue("domainName", String.class)) .isEqualTo("example.org"); - assertThat(newBookingItem) - .extracting(bi -> bi.getDirectValue("status", String.class)) + final var status = BookingItemCreatedEvent.of(newBookingItem); + assertThat(status.getStatus().getMessage()) .isEqualTo("[[DNS] no TXT record 'Hostsharing-domain-setup-verification-code=just-a-fake-verification-code' found for domain name 'example.org' (nor in its super-domain)]"); // but the related HostingAsset did not get created @@ -314,6 +337,14 @@ class HsBookingItemControllerAcceptanceTest extends ContextBasedTestWithCleanup } } + private @NotNull HsBookingProjectRealEntity findDefaultProjectOfDebitorNumber(final int debitorNumber) { + return debitorRepo.findByDebitorNumber(debitorNumber).stream() + .map(d -> realProjectRepo.findAllByDebitorUuid(d.getUuid())) + .flatMap(List::stream) + .findFirst() + .orElseThrow(); + } + @Nested @Order(1) @TestMethodOrder(MethodOrderer.OrderAnnotation.class) diff --git a/src/test/java/net/hostsharing/hsadminng/hs/migration/BaseOfficeDataImport.java b/src/test/java/net/hostsharing/hsadminng/hs/migration/BaseOfficeDataImport.java index 511647aa..9aa92773 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/migration/BaseOfficeDataImport.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/migration/BaseOfficeDataImport.java @@ -502,7 +502,7 @@ public abstract class BaseOfficeDataImport extends CsvDataImport { // this happens if a natural person is marked as 'contractual' for itself final var idsToRemove = new HashSet(); relations.forEach((id, r) -> { - if (r.getHolder() == r.getAnchor()) { + if (r.getType() == HsOfficeRelationType.REPRESENTATIVE && r.getHolder() == r.getAnchor()) { idsToRemove.add(id); } });