From f5b839cf5245e32b65324e09fa98a2e5230b3213 Mon Sep 17 00:00:00 2001 From: Michael Hoennig Date: Mon, 7 Oct 2024 09:56:43 +0200 Subject: [PATCH] tests for auto-creating hosting asset for managed webspace booking item --- .../item/BookingItemCreatedEventEntity.java | 2 + .../hs/hosting/asset/HsHostingAsset.java | 10 +- .../HsBookingItemCreatedListener.java | 16 +- .../ManagedWebspaceHostingAssetFactory.java | 44 ++++++ .../hostsharing/hsadminng/mapper/Mapper.java | 10 +- .../hsadminng/mapper/StandardMapper.java | 5 +- .../hsadminng/mapper/StrictMapper.java | 5 +- ...HsBookingItemControllerAcceptanceTest.java | 64 +++++++- ...omainSetupHostingAssetFactoryUnitTest.java | 7 +- ...edWebspaceHostingAssetFactoryUnitTest.java | 137 ++++++++++++++++++ ...OfficeMembershipEntityPatcherUnitTest.java | 6 +- .../persistence/EntityManagerWrapperFake.java | 7 +- 12 files changed, 293 insertions(+), 20 deletions(-) create mode 100644 src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/factories/ManagedWebspaceHostingAssetFactory.java create mode 100644 src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/factories/ManagedWebspaceHostingAssetFactoryUnitTest.java diff --git a/src/main/java/net/hostsharing/hsadminng/hs/booking/item/BookingItemCreatedEventEntity.java b/src/main/java/net/hostsharing/hsadminng/hs/booking/item/BookingItemCreatedEventEntity.java index 245b056f..19c6c208 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/booking/item/BookingItemCreatedEventEntity.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/booking/item/BookingItemCreatedEventEntity.java @@ -3,6 +3,7 @@ package net.hostsharing.hsadminng.hs.booking.item; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; +import lombok.ToString; import lombok.experimental.SuperBuilder; import net.hostsharing.hsadminng.rbac.object.BaseEntity; @@ -23,6 +24,7 @@ import java.util.UUID; @Table(schema = "hs_booking", name = "item_created_event") @SuperBuilder(toBuilder = true) @Getter +@ToString @NoArgsConstructor public class BookingItemCreatedEventEntity implements BaseEntity { @Id diff --git a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAsset.java b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAsset.java index 4510655c..63864065 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAsset.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAsset.java @@ -89,10 +89,9 @@ public abstract class HsHostingAsset implements Stringifyable, BaseEntity subHostingAssets = new ArrayList<>(); + private List subHostingAssets; @Column(name = "identifier") private String identifier; // e.g. vm1234, xyz00, example.org, xyz00_abc @@ -125,6 +124,13 @@ public abstract class HsHostingAsset implements Stringifyable, BaseEntity {configWrapper = newWrapper;}, config).assign(newConfig); } + public List getSubHostingAssets() { + if (subHostingAssets == null) { + subHostingAssets = new ArrayList<>(); + } + return subHostingAssets; + } + @Override public PatchableMapWrapper directProps() { return getConfig(); diff --git a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/factories/HsBookingItemCreatedListener.java b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/factories/HsBookingItemCreatedListener.java index 1b031a05..d388c61c 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/factories/HsBookingItemCreatedListener.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/factories/HsBookingItemCreatedListener.java @@ -1,5 +1,6 @@ package net.hostsharing.hsadminng.hs.hosting.asset.factories; +import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; import lombok.SneakyThrows; import net.hostsharing.hsadminng.hs.booking.generated.api.v1.model.HsHostingAssetAutoInsertResource; @@ -10,6 +11,7 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.ApplicationListener; import org.springframework.stereotype.Component; + @Component public class HsBookingItemCreatedListener implements ApplicationListener { @@ -24,12 +26,22 @@ public class HsBookingItemCreatedListener implements ApplicationListener null; // for now, no automatic HostingAsset possible - case MANAGED_WEBSPACE -> null; // FIXME: implement ManagedWebspace HostingAsset creation, where possible + case MANAGED_WEBSPACE -> new ManagedWebspaceHostingAssetFactory(emw, newBookingItemRealEntity, asset, standardMapper); case DOMAIN_SETUP -> new DomainSetupHostingAssetFactory(emw, newBookingItemRealEntity, asset, standardMapper); }; if (factory != null) { diff --git a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/factories/ManagedWebspaceHostingAssetFactory.java b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/factories/ManagedWebspaceHostingAssetFactory.java new file mode 100644 index 00000000..70752572 --- /dev/null +++ b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/factories/ManagedWebspaceHostingAssetFactory.java @@ -0,0 +1,44 @@ +package net.hostsharing.hsadminng.hs.hosting.asset.factories; + +import net.hostsharing.hsadminng.hs.booking.generated.api.v1.model.HsHostingAssetAutoInsertResource; +import net.hostsharing.hsadminng.hs.booking.generated.api.v1.model.HsHostingAssetTypeResource; +import net.hostsharing.hsadminng.hs.booking.item.HsBookingItemRealEntity; +import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAsset; +import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetRealEntity; +import net.hostsharing.hsadminng.mapper.StandardMapper; +import net.hostsharing.hsadminng.persistence.EntityManagerWrapper; + +import jakarta.validation.ValidationException; + +import static java.util.Optional.ofNullable; + +public class ManagedWebspaceHostingAssetFactory extends HostingAssetFactory { + + public ManagedWebspaceHostingAssetFactory( + final EntityManagerWrapper emw, + final HsBookingItemRealEntity newBookingItemRealEntity, + final HsHostingAssetAutoInsertResource asset, + final StandardMapper standardMapper) { + super(emw, newBookingItemRealEntity, asset, standardMapper); + } + + @Override + protected HsHostingAsset create() { + if (asset.getType() != HsHostingAssetTypeResource.MANAGED_WEBSPACE) { + throw new ValidationException("requires MANAGED_WEBSPACE hosting asset, but got " + + ofNullable(asset) + .map(HsHostingAssetAutoInsertResource::getType) + .map(Enum::name) + .orElse(null)); + } + final var managedWebspaceHostingAsset = standardMapper.map(asset, HsHostingAssetRealEntity.class); + managedWebspaceHostingAsset.setBookingItem(fromBookingItem); + + return managedWebspaceHostingAsset; + } + + @Override + protected void persist(final HsHostingAsset newManagedWebspaceHostingAsset) { + super.persist(newManagedWebspaceHostingAsset); + } +} diff --git a/src/main/java/net/hostsharing/hsadminng/mapper/Mapper.java b/src/main/java/net/hostsharing/hsadminng/mapper/Mapper.java index 21779a5c..a98b02e6 100644 --- a/src/main/java/net/hostsharing/hsadminng/mapper/Mapper.java +++ b/src/main/java/net/hostsharing/hsadminng/mapper/Mapper.java @@ -1,11 +1,11 @@ package net.hostsharing.hsadminng.mapper; +import net.hostsharing.hsadminng.persistence.EntityManagerWrapper; import org.modelmapper.ModelMapper; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.util.ReflectionUtils; -import jakarta.persistence.EntityManager; import jakarta.persistence.ManyToOne; -import jakarta.persistence.PersistenceContext; import jakarta.validation.ValidationException; import java.lang.reflect.Field; import java.util.List; @@ -21,10 +21,10 @@ import static net.hostsharing.hsadminng.errors.DisplayAs.DisplayName; */ abstract class Mapper extends ModelMapper { - @PersistenceContext - EntityManager em; + EntityManagerWrapper em; - Mapper() { + Mapper(@Autowired final EntityManagerWrapper em) { + this.em = em; getConfiguration().setAmbiguityIgnored(true); } diff --git a/src/main/java/net/hostsharing/hsadminng/mapper/StandardMapper.java b/src/main/java/net/hostsharing/hsadminng/mapper/StandardMapper.java index 42725d3d..3f6a851b 100644 --- a/src/main/java/net/hostsharing/hsadminng/mapper/StandardMapper.java +++ b/src/main/java/net/hostsharing/hsadminng/mapper/StandardMapper.java @@ -1,5 +1,7 @@ package net.hostsharing.hsadminng.mapper; +import net.hostsharing.hsadminng.persistence.EntityManagerWrapper; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; /** @@ -8,7 +10,8 @@ import org.springframework.stereotype.Component; @Component public class StandardMapper extends Mapper { - public StandardMapper() { + public StandardMapper(@Autowired final EntityManagerWrapper em) { + super(em); getConfiguration().setAmbiguityIgnored(true); } } diff --git a/src/main/java/net/hostsharing/hsadminng/mapper/StrictMapper.java b/src/main/java/net/hostsharing/hsadminng/mapper/StrictMapper.java index a6d3c3fc..12ffa6e1 100644 --- a/src/main/java/net/hostsharing/hsadminng/mapper/StrictMapper.java +++ b/src/main/java/net/hostsharing/hsadminng/mapper/StrictMapper.java @@ -1,5 +1,7 @@ package net.hostsharing.hsadminng.mapper; +import net.hostsharing.hsadminng.persistence.EntityManagerWrapper; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import static org.modelmapper.convention.MatchingStrategies.STRICT; @@ -13,7 +15,8 @@ import static org.modelmapper.convention.MatchingStrategies.STRICT; @Component public class StrictMapper extends Mapper { - public StrictMapper() { + public StrictMapper(@Autowired final EntityManagerWrapper em) { + super(em); getConfiguration().setMatchingStrategy(STRICT); } } diff --git a/src/test/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemControllerAcceptanceTest.java b/src/test/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemControllerAcceptanceTest.java index 21bafcaf..66e2dff1 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemControllerAcceptanceTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemControllerAcceptanceTest.java @@ -185,7 +185,69 @@ class HsBookingItemControllerAcceptanceTest extends ContextBasedTestWithCleanup } @Test - void projectAgent_canAddBookingItemWithHostingAsset() { + void projectAgent_canAddManagedWebspaceBookingItemWithHostingAsset() { + + context.define("superuser-alex@hostsharing.net", "hs_booking.project#D-1000111-D-1000111defaultproject:AGENT"); + final var givenProject = findDefaultProjectOfDebitorNumber(1000111); + + final var location = RestAssured // @formatter:off + .given() + .header("current-subject", "superuser-alex@hostsharing.net") + .contentType(ContentType.JSON) + .body(""" + { + "projectUuid": "{projectUuid}", + "type": "MANAGED_WEBSPACE", + "caption": "some managed webspace", + "resources": { + "SSD": 25, + "Traffic": 250 + }, + "asset": { + "type": "MANAGED_WEBSPACE", + "identifier": "fir00" + } + } + """ + .replace("{projectUuid}", givenProject.getUuid().toString()) + ) + .port(port) + .when() + .post("http://localhost/api/hs/booking/items") + .then().log().all().assertThat() + .statusCode(201) + .contentType(ContentType.JSON) + .body("", lenientlyEquals(""" + { + "type": "MANAGED_WEBSPACE", + "caption": "some managed webspace", + "validFrom": "{today}", + "validTo": null + } + """ + .replace("{today}", LocalDate.now().toString()) + .replace("{todayPlus1Month}", LocalDate.now().plusMonths(1).toString())) + ) + .header("Location", matchesRegex("http://localhost:[1-9][0-9]*/api/hs/booking/items/[^/]*")) + .extract().header("Location"); // @formatter:on + + // then, the new BookingItem can be accessed under the generated UUID + final var newBookingItem = fetchRealBookingItemFromURI(location); + assertThat(newBookingItem) + .extracting(HsBookingItem::getCaption) + .isEqualTo("some managed webspace"); + + // and the related HostingAssets are also got created + final var domainSetupHostingAsset = realHostingAssetRepo.findByIdentifier("fir00"); + assertThat(domainSetupHostingAsset).isNotEmpty() + .map(HsHostingAsset::getBookingItem) + .contains(newBookingItem); + final var event = bookingItemCreationEventRepo.findByBookingItem(newBookingItem); + assertThat(event).isNull(); + } + + @Test + void projectAgent_canAddDomainSetupBookingItemWithHostingAsset() { context.define("superuser-alex@hostsharing.net", "hs_booking.project#D-1000111-D-1000111defaultproject:AGENT"); final var givenProject = findDefaultProjectOfDebitorNumber(1000111); diff --git a/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/factories/DomainSetupHostingAssetFactoryUnitTest.java b/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/factories/DomainSetupHostingAssetFactoryUnitTest.java index 8b6ae464..14762bd0 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/factories/DomainSetupHostingAssetFactoryUnitTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/factories/DomainSetupHostingAssetFactoryUnitTest.java @@ -59,7 +59,7 @@ class DomainSetupHostingAssetFactoryUnitTest { private ObjectMapper jsonMapper = new JsonObjectMapperConfiguration().customObjectMapper().build(); @Spy - private StandardMapper standardMapper = new StandardMapper(); + private StandardMapper standardMapper = new StandardMapper(emw); @InjectMocks private HsBookingItemCreatedListener listener; @@ -107,8 +107,9 @@ class DomainSetupHostingAssetFactoryUnitTest { ); // then - assertThat(emwFake.stream(BookingItemCreatedEventEntity.class).findAny().isEmpty()) - .as("the event should not have been persisted, but got persisted").isTrue(); + assertThat(emwFake.stream(BookingItemCreatedEventEntity.class)) + .as("the event should not have been persisted, but got persisted") + .isEmpty(); } @Test diff --git a/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/factories/ManagedWebspaceHostingAssetFactoryUnitTest.java b/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/factories/ManagedWebspaceHostingAssetFactoryUnitTest.java new file mode 100644 index 00000000..511e408c --- /dev/null +++ b/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/factories/ManagedWebspaceHostingAssetFactoryUnitTest.java @@ -0,0 +1,137 @@ +package net.hostsharing.hsadminng.hs.hosting.asset.factories; + +import com.fasterxml.jackson.databind.ObjectMapper; +import net.hostsharing.hsadminng.config.JsonObjectMapperConfiguration; +import net.hostsharing.hsadminng.hs.booking.debitor.HsBookingDebitorEntity; +import net.hostsharing.hsadminng.hs.booking.item.BookingItemCreatedAppEvent; +import net.hostsharing.hsadminng.hs.booking.item.BookingItemCreatedEventEntity; +import net.hostsharing.hsadminng.hs.booking.item.HsBookingItemRealEntity; +import net.hostsharing.hsadminng.hs.booking.item.HsBookingItemType; +import net.hostsharing.hsadminng.hs.booking.project.HsBookingProjectRealEntity; +import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetRealEntity; +import net.hostsharing.hsadminng.hs.hosting.asset.validators.Dns; +import net.hostsharing.hsadminng.hs.office.contact.HsOfficeContact; +import net.hostsharing.hsadminng.hs.office.contact.HsOfficeContactRealEntity; +import net.hostsharing.hsadminng.lambda.Reducer; +import net.hostsharing.hsadminng.mapper.StandardMapper; +import net.hostsharing.hsadminng.persistence.EntityManagerWrapper; +import net.hostsharing.hsadminng.persistence.EntityManagerWrapperFake; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Spy; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.util.Map; +import java.util.UUID; + +import static net.hostsharing.hsadminng.mapper.PatchableMapWrapper.entry; +import static org.assertj.core.api.Assertions.assertThat; + +// Tests the DomainSetupHostingAssetFactory through a HsBookingItemCreatedListener instance. +@ExtendWith(MockitoExtension.class) +class ManagedWebspaceHostingAssetFactoryUnitTest { + + final HsBookingDebitorEntity debitor = HsBookingDebitorEntity.builder() + .debitorNumber(12345) + .defaultPrefix("xyz") + .build(); + final HsBookingProjectRealEntity project = HsBookingProjectRealEntity.builder() + .debitor(debitor) + .caption("Test-Project") + .build(); + final HsOfficeContact alarmContact = HsOfficeContactRealEntity.builder() + .uuid(UUID.randomUUID()) + .caption("Alarm Contact xyz") + .build(); + + private EntityManagerWrapperFake emwFake = new EntityManagerWrapperFake(); + + @Spy + private EntityManagerWrapper emw = emwFake; + + @Spy + private ObjectMapper jsonMapper = new JsonObjectMapperConfiguration().customObjectMapper().build(); + + @Spy + private StandardMapper standardMapper = new StandardMapper(emw); + + @InjectMocks + private HsBookingItemCreatedListener listener; + + @BeforeEach + void initMocks() { + emwFake.persist(alarmContact); + } + + @Test + void doesNotPersistAnyEntityWithoutHostingAssetWithoutValidationErrors() { + // given + final var givenBookingItem = HsBookingItemRealEntity.builder() + .type(HsBookingItemType.MANAGED_WEBSPACE) + .project(project) + .caption("Test Managed-Webspace") + .resources(Map.ofEntries( + Map.entry("RAM", 25), + Map.entry("Traffic", 250) + )) + .build(); + + // when + listener.onApplicationEvent( + new BookingItemCreatedAppEvent(this, givenBookingItem, null) + ); + + // then + assertThat(emwFake.stream(BookingItemCreatedEventEntity.class).findAny().isEmpty()) + .as("the event should not have been persisted, but got persisted").isTrue(); + assertThat(emwFake.stream(HsHostingAssetRealEntity.class).findAny().isEmpty()) + .as("the hosting asset should not have been persisted, but got persisted").isTrue(); + } + + @Test + void persistsEventEntityIfDomainSetupVerificationFails() { + // given + final var givenBookingItem = createBookingItemFromResources( + entry("domainName", "example.org") + ); + final var givenAssetJson = """ + { + "identifier": "xyz00" + } + """; + Dns.fakeResultForDomain("example.org", Dns.Result.fromRecords()); // without valid verificationCode + + // when + listener.onApplicationEvent( + new BookingItemCreatedAppEvent(this, givenBookingItem, givenAssetJson) + ); + + // then + assertEventStatus(givenBookingItem, givenAssetJson, + "requires MANAGED_WEBSPACE hosting asset, but got null"); + } + + @SafeVarargs + private static HsBookingItemRealEntity createBookingItemFromResources(final Map.Entry... givenResources) { + return HsBookingItemRealEntity.builder() + .type(HsBookingItemType.MANAGED_WEBSPACE) + .resources(Map.ofEntries(givenResources)) + .build(); + } + + private void assertEventStatus( + final HsBookingItemRealEntity givenBookingItem, + final String givenAssetJson, + final String expectedErrorMessage) { + emwFake.stream(BookingItemCreatedEventEntity.class) + .reduce(Reducer::toSingleElement) + .map(eventEntity -> { + assertThat(eventEntity.getBookingItem()).isSameAs(givenBookingItem); + assertThat(eventEntity.getAssetJson()).isEqualTo(givenAssetJson); + assertThat(eventEntity.getStatusMessage()).isEqualTo(expectedErrorMessage); + return true; + }); + } +} diff --git a/src/test/java/net/hostsharing/hsadminng/hs/office/membership/HsOfficeMembershipEntityPatcherUnitTest.java b/src/test/java/net/hostsharing/hsadminng/hs/office/membership/HsOfficeMembershipEntityPatcherUnitTest.java index 841e7e12..f6ab56fa 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/office/membership/HsOfficeMembershipEntityPatcherUnitTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/office/membership/HsOfficeMembershipEntityPatcherUnitTest.java @@ -5,6 +5,7 @@ import net.hostsharing.hsadminng.hs.office.debitor.HsOfficeDebitorEntity; import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficeMembershipPatchResource; import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficeMembershipStatusResource; import net.hostsharing.hsadminng.mapper.StandardMapper; +import net.hostsharing.hsadminng.persistence.EntityManagerWrapper; import net.hostsharing.hsadminng.rbac.test.PatchUnitTestBase; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.TestInstance; @@ -12,7 +13,6 @@ import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; -import jakarta.persistence.EntityManager; import java.time.LocalDate; import java.util.UUID; import java.util.stream.Stream; @@ -38,9 +38,9 @@ class HsOfficeMembershipEntityPatcherUnitTest extends PatchUnitTestBase< private static final Boolean PATCHED_MEMBERSHIP_FEE_BILLABLE = false; @Mock - private EntityManager em; + private EntityManagerWrapper em; - private StandardMapper mapper = new StandardMapper(); + private StandardMapper mapper = new StandardMapper(em); @BeforeEach void initMocks() { diff --git a/src/test/java/net/hostsharing/hsadminng/persistence/EntityManagerWrapperFake.java b/src/test/java/net/hostsharing/hsadminng/persistence/EntityManagerWrapperFake.java index 4644677a..e1ce8e2e 100644 --- a/src/test/java/net/hostsharing/hsadminng/persistence/EntityManagerWrapperFake.java +++ b/src/test/java/net/hostsharing/hsadminng/persistence/EntityManagerWrapperFake.java @@ -21,9 +21,13 @@ public class EntityManagerWrapperFake extends EntityManagerWrapper { return find(entity.getClass(), id) != null; } + @Override + public T getReference(final Class entityClass, final Object primaryKey) { + return find(entityClass, primaryKey); + } + @Override public T find(final Class entityClass, final Object primaryKey) { - final var self = this; if (entityClasses.containsKey(entityClass)) { final var entities = entityClasses.get(entityClass); //noinspection unchecked @@ -87,5 +91,4 @@ public class EntityManagerWrapperFake extends EntityManagerWrapper { } throw new IllegalArgumentException("No @Id field found in entity class: " + entity.getClass().getName()); } - }