From 7e177adff36986e5ffafea909355fbb268d7b5f3 Mon Sep 17 00:00:00 2001 From: Michael Hoennig Date: Tue, 1 Oct 2024 17:46:58 +0200 Subject: [PATCH] add HsBookingItemCreatedListenerUnitTest with EntityManagerWrapperFake --- .../item/BookingItemCreatedAppEvent.java | 2 +- .../item/BookingItemCreatedEventEntity.java | 5 +- .../DomainSetupHostingAssetFactory.java | 3 +- .../HsBookingItemCreatedListener.java | 2 +- .../HsBookingItemCreatedListenerUnitTest.java | 137 ++++++++++++++++++ .../persistence/EntityManagerWrapperFake.java | 95 ++++++++++++ 6 files changed, 238 insertions(+), 6 deletions(-) create mode 100644 src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/factories/HsBookingItemCreatedListenerUnitTest.java create mode 100644 src/test/java/net/hostsharing/hsadminng/persistence/EntityManagerWrapperFake.java diff --git a/src/main/java/net/hostsharing/hsadminng/hs/booking/item/BookingItemCreatedAppEvent.java b/src/main/java/net/hostsharing/hsadminng/hs/booking/item/BookingItemCreatedAppEvent.java index 1e65d914..6960d626 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/booking/item/BookingItemCreatedAppEvent.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/booking/item/BookingItemCreatedAppEvent.java @@ -11,7 +11,7 @@ public class BookingItemCreatedAppEvent extends ApplicationEvent { private BookingItemCreatedEventEntity entity; public BookingItemCreatedAppEvent( - @NotNull final HsBookingItemController source, + @NotNull final Object source, @NotNull final HsBookingItemRealEntity newBookingItem, final String assetJson) { super(source); diff --git a/src/main/java/net/hostsharing/hsadminng/hs/booking/item/BookingItemCreatedEventEntity.java b/src/main/java/net/hostsharing/hsadminng/hs/booking/item/BookingItemCreatedEventEntity.java index 4a97dddf..245b056f 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/booking/item/BookingItemCreatedEventEntity.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/booking/item/BookingItemCreatedEventEntity.java @@ -4,6 +4,7 @@ import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; import lombok.experimental.SuperBuilder; +import net.hostsharing.hsadminng.rbac.object.BaseEntity; import jakarta.persistence.Column; import jakarta.persistence.Entity; @@ -23,10 +24,10 @@ import java.util.UUID; @SuperBuilder(toBuilder = true) @Getter @NoArgsConstructor -public class BookingItemCreatedEventEntity { +public class BookingItemCreatedEventEntity implements BaseEntity { @Id @Column(name="bookingitemuuid") - private UUID id; + private UUID uuid; @MapsId @ManyToOne(optional = false) diff --git a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/factories/DomainSetupHostingAssetFactory.java b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/factories/DomainSetupHostingAssetFactory.java index 37045f93..6421aa1d 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/factories/DomainSetupHostingAssetFactory.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/factories/DomainSetupHostingAssetFactory.java @@ -69,9 +69,8 @@ public class DomainSetupHostingAssetFactory extends HostingAssetFactory { .type(HsHostingAssetType.DOMAIN_SETUP) .identifier(domainName) .caption(asset.getCaption() != null ? asset.getCaption() : domainName) - .parentAsset(ref(HsHostingAssetRealEntity.class, asset.getParentAssetUuid())) .alarmContact(ref(HsOfficeContactRealEntity.class, asset.getAlarmContactUuid())) - .subHostingAssets( + .subHostingAssets( // FIXME: is this even used? standardMapper.mapList(getSubHostingAssetResources(), HsHostingAssetRealEntity.class) ) .build(); diff --git a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/factories/HsBookingItemCreatedListener.java b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/factories/HsBookingItemCreatedListener.java index 3f63c50f..1b031a05 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/factories/HsBookingItemCreatedListener.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/factories/HsBookingItemCreatedListener.java @@ -29,7 +29,7 @@ public class HsBookingItemCreatedListener implements ApplicationListener null; // for now, no automatic HostingAsset possible - case MANAGED_WEBSPACE -> null; // TODO.impl: implement ManagedWebspace HostingAsset creation, where possible + case MANAGED_WEBSPACE -> null; // FIXME: implement ManagedWebspace HostingAsset creation, where possible case DOMAIN_SETUP -> new DomainSetupHostingAssetFactory(emw, newBookingItemRealEntity, asset, standardMapper); }; if (factory != null) { diff --git a/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/factories/HsBookingItemCreatedListenerUnitTest.java b/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/factories/HsBookingItemCreatedListenerUnitTest.java new file mode 100644 index 00000000..8ae27c5a --- /dev/null +++ b/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/factories/HsBookingItemCreatedListenerUnitTest.java @@ -0,0 +1,137 @@ +package net.hostsharing.hsadminng.hs.hosting.asset.factories; + +import net.hostsharing.hsadminng.hs.booking.item.BookingItemCreatedAppEvent; +import net.hostsharing.hsadminng.hs.booking.item.BookingItemCreatedEventEntity; +import net.hostsharing.hsadminng.hs.booking.item.HsBookingItemRealEntity; +import net.hostsharing.hsadminng.hs.booking.item.HsBookingItemType; +import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetRealEntity; +import net.hostsharing.hsadminng.hs.hosting.asset.validators.Dns; +import net.hostsharing.hsadminng.persistence.EntityManagerWrapperFake; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import com.fasterxml.jackson.databind.ObjectMapper; +import net.hostsharing.hsadminng.config.JsonObjectMapperConfiguration; +import net.hostsharing.hsadminng.mapper.StandardMapper; +import net.hostsharing.hsadminng.persistence.EntityManagerWrapper; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Spy; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.util.Map; +import java.util.UUID; + +import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.MANAGED_WEBSPACE; +import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.UNIX_USER; +import static net.hostsharing.hsadminng.mapper.PatchableMapWrapper.entry; +import static org.assertj.core.api.Assertions.assertThat; + +@ExtendWith(MockitoExtension.class) +class HsBookingItemCreatedListenerUnitTest { + + private final HsHostingAssetRealEntity managedWebspaceHostingAsset = HsHostingAssetRealEntity.builder() + .uuid(UUID.randomUUID()) + .type(MANAGED_WEBSPACE) + .build(); + + private final HsHostingAssetRealEntity unixUserHostingAsset = HsHostingAssetRealEntity.builder() + .uuid(UUID.randomUUID()) + .type(UNIX_USER) + .parentAsset(managedWebspaceHostingAsset) + .build(); + + private EntityManagerWrapperFake emwFake = new EntityManagerWrapperFake(); + + @Spy + private EntityManagerWrapper emw = emwFake; + + @Spy + private ObjectMapper jsonMapper = new JsonObjectMapperConfiguration().customObjectMapper().build(); + + @Spy + private StandardMapper standardMapper = new StandardMapper(); + + @InjectMocks + private HsBookingItemCreatedListener listener; + + @BeforeEach + void initMocks() { + emwFake.persist(managedWebspaceHostingAsset); + emwFake.persist(unixUserHostingAsset); + } + + @Test + void doesNotPersistEventEntityWithoutValidationErrors() { + // given + final var givenBookingItem = HsBookingItemRealEntity.builder() + .type(HsBookingItemType.DOMAIN_SETUP) + .resources(Map.ofEntries( + entry("domainName", "example.org"), + entry("verificationCode", "just-a-fake-verification-code") + )) + .build(); + final var givenAssetJson = """ + { + "identifier": "example.org", // also as default for all subAssets + "subHostingAssets": [ + { + "type": "DOMAIN_HTTP_SETUP", + "assignedToAssetUuid": "{unixUserHostingAssetUuid}" + } + ] + } + """ + .replace("{unixUserHostingAssetUuid}", unixUserHostingAsset.getUuid().toString()); + Dns.fakeResultForDomain("example.org", + Dns.Result.fromRecords("Hostsharing-domain-setup-verification-code=just-a-fake-verification-code")); + + // when + listener.onApplicationEvent( + new BookingItemCreatedAppEvent(this, givenBookingItem, givenAssetJson) + ); + + // then + assertThat(emwFake.stream(BookingItemCreatedEventEntity.class).findAny().isEmpty()) + .as("the event should not have been persisted, but got persisted").isTrue(); + } + + @Test + void persistsEventEntityIfHostingAssetVerificationFails() { + // given + final var givenBookingItem = HsBookingItemRealEntity.builder() + .type(HsBookingItemType.DOMAIN_SETUP) + .resources(Map.ofEntries( + entry("domainName", "example.org") + )) + .build(); + final var givenAssetJson = """ + { + "identifier": "example.org", // also as default for all subAssets + "subHostingAssets": [ + { + "type": "DOMAIN_HTTP_SETUP", + "assignedToAssetUuid": "{unixUserHostingAssetUuid}" + } + ] + } + """ + .replace("{unixUserHostingAssetUuid}", unixUserHostingAsset.getUuid().toString()); + Dns.fakeResultForDomain("example.org", Dns.Result.fromRecords()); // without valid verificationCode + + // when + listener.onApplicationEvent( + new BookingItemCreatedAppEvent(this, givenBookingItem, givenAssetJson) + ); + + // then + emwFake.stream(BookingItemCreatedEventEntity.class) + .reduce(EntityManagerWrapperFake::toSingleElement) + .map(eventEntity -> { + assertThat(eventEntity.getBookingItem()).isSameAs(givenBookingItem); + assertThat(eventEntity.getAssetJson()).isEqualTo(givenAssetJson); + assertThat(eventEntity.getStatusMessage()).isEqualTo( + "[[DNS] no TXT record 'Hostsharing-domain-setup-verification-code=null' found for domain name 'example.org' (nor in its super-domain)]"); + return true; + }); + } +} diff --git a/src/test/java/net/hostsharing/hsadminng/persistence/EntityManagerWrapperFake.java b/src/test/java/net/hostsharing/hsadminng/persistence/EntityManagerWrapperFake.java new file mode 100644 index 00000000..fd05c371 --- /dev/null +++ b/src/test/java/net/hostsharing/hsadminng/persistence/EntityManagerWrapperFake.java @@ -0,0 +1,95 @@ +package net.hostsharing.hsadminng.persistence; + +import lombok.SneakyThrows; + +import jakarta.persistence.Id; +import java.lang.reflect.Field; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; +import java.util.UUID; +import java.util.stream.Stream; + + +public class EntityManagerWrapperFake extends EntityManagerWrapper { + + private Map, Map> entityClasses = new HashMap<>(); + + public static T toSingleElement(T last, T next) { + throw new AssertionError("only a single entity expected"); + } + + @Override + public boolean contains(final Object entity) { + final var id = getEntityId(entity); + return find(entity.getClass(), id) != null; + } + + @Override + public T find(final Class entityClass, final Object primaryKey) { + final var self = this; + if (entityClasses.containsKey(entityClass)) { + final var entities = entityClasses.get(entityClass); + //noinspection unchecked + return entities.keySet().stream() + .filter(key -> key.equals(primaryKey)) + .map(key -> (T) entities.get(key)) + .findAny() + .orElse(null); + } + return null; + } + + @Override + public void persist(final Object entity) { + if (!entityClasses.containsKey(entity.getClass())) { + entityClasses.put(entity.getClass(), new HashMap<>()); + } + final var id = getEntityId(entity).orElseGet(() -> setEntityId(entity, UUID.randomUUID())); + entityClasses.get(entity.getClass()).put(id, entity); + } + + @Override + public void flush() { + } + + public Stream stream() { + return entityClasses.values().stream().flatMap(entitiesPerClass -> entitiesPerClass.values().stream()); + } + + public Stream stream(final Class entityClass) { + if (entityClasses.containsKey(entityClass)) { + //noinspection unchecked + return (Stream) entityClasses.get(entityClass).values().stream(); + } + return Stream.empty(); + } + + @SneakyThrows + private static Optional getEntityId(final Object entity) { + for (Class currentClass = entity.getClass(); currentClass != null; currentClass = currentClass.getSuperclass()) { + for (Field field : currentClass.getDeclaredFields()) { + if (field.isAnnotationPresent(Id.class)) { + field.setAccessible(true); + return Optional.ofNullable(field.get(entity)); + } + } + } + throw new IllegalArgumentException("No @Id field found in entity class: " + entity.getClass().getName()); + } + + @SneakyThrows + private static Object setEntityId(final Object entity, final Object id) { + for (Class currentClass = entity.getClass(); currentClass != null; currentClass = currentClass.getSuperclass()) { + for (Field field : currentClass.getDeclaredFields()) { + if (field.isAnnotationPresent(Id.class)) { + field.setAccessible(true); + field.set(entity, id); + return id; + } + } + } + throw new IllegalArgumentException("No @Id field found in entity class: " + entity.getClass().getName()); + } + +}