tests for auto-creating hosting asset for managed webspace booking item

This commit is contained in:
Michael Hoennig 2024-10-07 09:56:43 +02:00
parent 41f5eabd32
commit f5b839cf52
12 changed files with 293 additions and 20 deletions

View File

@ -3,6 +3,7 @@ package net.hostsharing.hsadminng.hs.booking.item;
import lombok.Getter; import lombok.Getter;
import lombok.NoArgsConstructor; import lombok.NoArgsConstructor;
import lombok.Setter; import lombok.Setter;
import lombok.ToString;
import lombok.experimental.SuperBuilder; import lombok.experimental.SuperBuilder;
import net.hostsharing.hsadminng.rbac.object.BaseEntity; import net.hostsharing.hsadminng.rbac.object.BaseEntity;
@ -23,6 +24,7 @@ import java.util.UUID;
@Table(schema = "hs_booking", name = "item_created_event") @Table(schema = "hs_booking", name = "item_created_event")
@SuperBuilder(toBuilder = true) @SuperBuilder(toBuilder = true)
@Getter @Getter
@ToString
@NoArgsConstructor @NoArgsConstructor
public class BookingItemCreatedEventEntity implements BaseEntity { public class BookingItemCreatedEventEntity implements BaseEntity {
@Id @Id

View File

@ -89,10 +89,9 @@ public abstract class HsHostingAsset implements Stringifyable, BaseEntity<HsHost
@JoinColumn(name = "alarmcontactuuid") @JoinColumn(name = "alarmcontactuuid")
private HsOfficeContactRealEntity alarmContact; private HsOfficeContactRealEntity alarmContact;
@Builder.Default
@OneToMany(cascade = CascadeType.REFRESH, orphanRemoval = true, fetch = FetchType.LAZY) @OneToMany(cascade = CascadeType.REFRESH, orphanRemoval = true, fetch = FetchType.LAZY)
@JoinColumn(name = "parentassetuuid", referencedColumnName = "uuid") @JoinColumn(name = "parentassetuuid", referencedColumnName = "uuid")
private List<HsHostingAssetRealEntity> subHostingAssets = new ArrayList<>(); private List<HsHostingAssetRealEntity> subHostingAssets;
@Column(name = "identifier") @Column(name = "identifier")
private String identifier; // e.g. vm1234, xyz00, example.org, xyz00_abc private String identifier; // e.g. vm1234, xyz00, example.org, xyz00_abc
@ -125,6 +124,13 @@ public abstract class HsHostingAsset implements Stringifyable, BaseEntity<HsHost
PatchableMapWrapper.of(configWrapper, (newWrapper) -> {configWrapper = newWrapper;}, config).assign(newConfig); PatchableMapWrapper.of(configWrapper, (newWrapper) -> {configWrapper = newWrapper;}, config).assign(newConfig);
} }
public List<HsHostingAssetRealEntity> getSubHostingAssets() {
if (subHostingAssets == null) {
subHostingAssets = new ArrayList<>();
}
return subHostingAssets;
}
@Override @Override
public PatchableMapWrapper<Object> directProps() { public PatchableMapWrapper<Object> directProps() {
return getConfig(); return getConfig();

View File

@ -1,5 +1,6 @@
package net.hostsharing.hsadminng.hs.hosting.asset.factories; package net.hostsharing.hsadminng.hs.hosting.asset.factories;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.SneakyThrows; import lombok.SneakyThrows;
import net.hostsharing.hsadminng.hs.booking.generated.api.v1.model.HsHostingAssetAutoInsertResource; 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.context.ApplicationListener;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
@Component @Component
public class HsBookingItemCreatedListener implements ApplicationListener<BookingItemCreatedAppEvent> { public class HsBookingItemCreatedListener implements ApplicationListener<BookingItemCreatedAppEvent> {
@ -24,12 +26,22 @@ public class HsBookingItemCreatedListener implements ApplicationListener<Booking
@Override @Override
@SneakyThrows @SneakyThrows
public void onApplicationEvent(final BookingItemCreatedAppEvent event) { 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 newBookingItemRealEntity = event.getEntity().getBookingItem();
final var asset = jsonMapper.readValue(event.getEntity().getAssetJson(), HsHostingAssetAutoInsertResource.class); final var asset = jsonMapper.readValue(event.getEntity().getAssetJson(), HsHostingAssetAutoInsertResource.class);
final var factory = switch (newBookingItemRealEntity.getType()) { final var factory = switch (newBookingItemRealEntity.getType()) {
case PRIVATE_CLOUD, CLOUD_SERVER, MANAGED_SERVER -> null; // for now, no automatic HostingAsset possible case PRIVATE_CLOUD, CLOUD_SERVER, MANAGED_SERVER -> 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); case DOMAIN_SETUP -> new DomainSetupHostingAssetFactory(emw, newBookingItemRealEntity, asset, standardMapper);
}; };
if (factory != null) { if (factory != null) {

View File

@ -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);
}
}

View File

@ -1,11 +1,11 @@
package net.hostsharing.hsadminng.mapper; package net.hostsharing.hsadminng.mapper;
import net.hostsharing.hsadminng.persistence.EntityManagerWrapper;
import org.modelmapper.ModelMapper; import org.modelmapper.ModelMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.util.ReflectionUtils; import org.springframework.util.ReflectionUtils;
import jakarta.persistence.EntityManager;
import jakarta.persistence.ManyToOne; import jakarta.persistence.ManyToOne;
import jakarta.persistence.PersistenceContext;
import jakarta.validation.ValidationException; import jakarta.validation.ValidationException;
import java.lang.reflect.Field; import java.lang.reflect.Field;
import java.util.List; import java.util.List;
@ -21,10 +21,10 @@ import static net.hostsharing.hsadminng.errors.DisplayAs.DisplayName;
*/ */
abstract class Mapper extends ModelMapper { abstract class Mapper extends ModelMapper {
@PersistenceContext EntityManagerWrapper em;
EntityManager em;
Mapper() { Mapper(@Autowired final EntityManagerWrapper em) {
this.em = em;
getConfiguration().setAmbiguityIgnored(true); getConfiguration().setAmbiguityIgnored(true);
} }

View File

@ -1,5 +1,7 @@
package net.hostsharing.hsadminng.mapper; package net.hostsharing.hsadminng.mapper;
import net.hostsharing.hsadminng.persistence.EntityManagerWrapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
/** /**
@ -8,7 +10,8 @@ import org.springframework.stereotype.Component;
@Component @Component
public class StandardMapper extends Mapper { public class StandardMapper extends Mapper {
public StandardMapper() { public StandardMapper(@Autowired final EntityManagerWrapper em) {
super(em);
getConfiguration().setAmbiguityIgnored(true); getConfiguration().setAmbiguityIgnored(true);
} }
} }

View File

@ -1,5 +1,7 @@
package net.hostsharing.hsadminng.mapper; package net.hostsharing.hsadminng.mapper;
import net.hostsharing.hsadminng.persistence.EntityManagerWrapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import static org.modelmapper.convention.MatchingStrategies.STRICT; import static org.modelmapper.convention.MatchingStrategies.STRICT;
@ -13,7 +15,8 @@ import static org.modelmapper.convention.MatchingStrategies.STRICT;
@Component @Component
public class StrictMapper extends Mapper { public class StrictMapper extends Mapper {
public StrictMapper() { public StrictMapper(@Autowired final EntityManagerWrapper em) {
super(em);
getConfiguration().setMatchingStrategy(STRICT); getConfiguration().setMatchingStrategy(STRICT);
} }
} }

View File

@ -185,7 +185,69 @@ class HsBookingItemControllerAcceptanceTest extends ContextBasedTestWithCleanup
} }
@Test @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"); context.define("superuser-alex@hostsharing.net", "hs_booking.project#D-1000111-D-1000111defaultproject:AGENT");
final var givenProject = findDefaultProjectOfDebitorNumber(1000111); final var givenProject = findDefaultProjectOfDebitorNumber(1000111);

View File

@ -59,7 +59,7 @@ class DomainSetupHostingAssetFactoryUnitTest {
private ObjectMapper jsonMapper = new JsonObjectMapperConfiguration().customObjectMapper().build(); private ObjectMapper jsonMapper = new JsonObjectMapperConfiguration().customObjectMapper().build();
@Spy @Spy
private StandardMapper standardMapper = new StandardMapper(); private StandardMapper standardMapper = new StandardMapper(emw);
@InjectMocks @InjectMocks
private HsBookingItemCreatedListener listener; private HsBookingItemCreatedListener listener;
@ -107,8 +107,9 @@ class DomainSetupHostingAssetFactoryUnitTest {
); );
// then // then
assertThat(emwFake.stream(BookingItemCreatedEventEntity.class).findAny().isEmpty()) assertThat(emwFake.stream(BookingItemCreatedEventEntity.class))
.as("the event should not have been persisted, but got persisted").isTrue(); .as("the event should not have been persisted, but got persisted")
.isEmpty();
} }
@Test @Test

View File

@ -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<String, String>... 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;
});
}
}

View File

@ -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.HsOfficeMembershipPatchResource;
import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficeMembershipStatusResource; import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficeMembershipStatusResource;
import net.hostsharing.hsadminng.mapper.StandardMapper; import net.hostsharing.hsadminng.mapper.StandardMapper;
import net.hostsharing.hsadminng.persistence.EntityManagerWrapper;
import net.hostsharing.hsadminng.rbac.test.PatchUnitTestBase; import net.hostsharing.hsadminng.rbac.test.PatchUnitTestBase;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.TestInstance; import org.junit.jupiter.api.TestInstance;
@ -12,7 +13,6 @@ import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock; import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension; import org.mockito.junit.jupiter.MockitoExtension;
import jakarta.persistence.EntityManager;
import java.time.LocalDate; import java.time.LocalDate;
import java.util.UUID; import java.util.UUID;
import java.util.stream.Stream; import java.util.stream.Stream;
@ -38,9 +38,9 @@ class HsOfficeMembershipEntityPatcherUnitTest extends PatchUnitTestBase<
private static final Boolean PATCHED_MEMBERSHIP_FEE_BILLABLE = false; private static final Boolean PATCHED_MEMBERSHIP_FEE_BILLABLE = false;
@Mock @Mock
private EntityManager em; private EntityManagerWrapper em;
private StandardMapper mapper = new StandardMapper(); private StandardMapper mapper = new StandardMapper(em);
@BeforeEach @BeforeEach
void initMocks() { void initMocks() {

View File

@ -21,9 +21,13 @@ public class EntityManagerWrapperFake extends EntityManagerWrapper {
return find(entity.getClass(), id) != null; return find(entity.getClass(), id) != null;
} }
@Override
public <T> T getReference(final Class<T> entityClass, final Object primaryKey) {
return find(entityClass, primaryKey);
}
@Override @Override
public <T> T find(final Class<T> entityClass, final Object primaryKey) { public <T> T find(final Class<T> entityClass, final Object primaryKey) {
final var self = this;
if (entityClasses.containsKey(entityClass)) { if (entityClasses.containsKey(entityClass)) {
final var entities = entityClasses.get(entityClass); final var entities = entityClasses.get(entityClass);
//noinspection unchecked //noinspection unchecked
@ -87,5 +91,4 @@ public class EntityManagerWrapperFake extends EntityManagerWrapper {
} }
throw new IllegalArgumentException("No @Id field found in entity class: " + entity.getClass().getName()); throw new IllegalArgumentException("No @Id field found in entity class: " + entity.getClass().getName());
} }
} }