hosting-asset-validation-beyond-property-validators (#65)

Co-authored-by: Michael Hoennig <michael@hoennig.de>
Reviewed-on: #65
Reviewed-by: Timotheus Pokorra <timotheus.pokorra@hostsharing.net>
This commit is contained in:
Michael Hoennig 2024-06-24 12:33:14 +02:00
parent 9418303b7c
commit de88f1d842
20 changed files with 639 additions and 55 deletions

View File

@ -34,6 +34,7 @@ import jakarta.persistence.OneToOne;
import jakarta.persistence.Table;
import jakarta.persistence.Transient;
import jakarta.persistence.Version;
import jakarta.validation.constraints.NotNull;
import java.io.IOException;
import java.time.LocalDate;
import java.util.HashMap;
@ -60,8 +61,8 @@ import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.SQL.directlyFetche
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.rbacViewFor;
import static net.hostsharing.hsadminng.stringify.Stringify.stringify;
@Builder
@Entity
@Builder(toBuilder = true)
@Table(name = "hs_booking_item_rv")
@Getter
@Setter
@ -92,6 +93,7 @@ public class HsBookingItemEntity implements Stringifyable, RbacObject {
@JoinColumn(name = "parentitemuuid")
private HsBookingItemEntity parentItem;
@NotNull
@Column(name = "type")
@Enumerated(EnumType.STRING)
private HsBookingItemType type;

View File

@ -29,6 +29,7 @@ import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.OneToMany;
import jakarta.persistence.OneToOne;
import jakarta.persistence.PostLoad;
import jakarta.persistence.Table;
import jakarta.persistence.Transient;
import jakarta.persistence.Version;
@ -120,12 +121,20 @@ public class HsHostingAssetEntity implements Stringifyable, RbacObject {
@Transient
private PatchableMapWrapper<Object> configWrapper;
@Transient
private boolean isLoaded = false;
@PostLoad
public void markAsLoaded() {
this.isLoaded = true;
}
public PatchableMapWrapper<Object> getConfig() {
return PatchableMapWrapper.of(configWrapper, (newWrapper) -> {configWrapper = newWrapper; }, config );
}
public void putConfig(Map<String, Object> newConfg) {
PatchableMapWrapper.of(configWrapper, (newWrapper) -> {configWrapper = newWrapper; }, config).assign(newConfg);
public void putConfig(Map<String, Object> newConfig) {
PatchableMapWrapper.of(configWrapper, (newWrapper) -> {configWrapper = newWrapper; }, config).assign(newConfig);
}
@Override

View File

@ -0,0 +1,23 @@
package net.hostsharing.hsadminng.hs.hosting.asset.validators;
import net.hostsharing.hsadminng.hs.booking.item.HsBookingItemType;
import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetEntity;
import java.util.regex.Pattern;
class HsCloudServerHostingAssetValidator extends HsHostingAssetEntityValidator {
HsCloudServerHostingAssetValidator() {
super(
BookingItem.mustBeOfType(HsBookingItemType.CLOUD_SERVER),
ParentAsset.mustBeNull(),
AssignedToAsset.mustBeNull(),
AlarmContact.isOptional(),
NO_EXTRA_PROPERTIES);
}
@Override
protected Pattern identifierPattern(final HsHostingAssetEntity assetEntity) {
return Pattern.compile("^vm[0-9][0-9][0-9][0-9]$");
}
}

View File

@ -1,35 +1,80 @@
package net.hostsharing.hsadminng.hs.hosting.asset.validators;
import net.hostsharing.hsadminng.hs.booking.item.HsBookingItemEntity;
import net.hostsharing.hsadminng.hs.booking.item.HsBookingItemType;
import net.hostsharing.hsadminng.hs.booking.item.validators.HsBookingItemEntityValidatorRegistry;
import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetEntity;
import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType;
import net.hostsharing.hsadminng.hs.office.contact.HsOfficeContactEntity;
import net.hostsharing.hsadminng.hs.validation.HsEntityValidator;
import net.hostsharing.hsadminng.hs.validation.ValidatableProperty;
import jakarta.validation.constraints.NotNull;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.function.BiFunction;
import java.util.function.Function;
import java.util.regex.Pattern;
import java.util.stream.Stream;
import static java.util.Arrays.stream;
import static java.util.Collections.emptyList;
import static java.util.Optional.ofNullable;
public class HsHostingAssetEntityValidator extends HsEntityValidator<HsHostingAssetEntity> {
public abstract class HsHostingAssetEntityValidator extends HsEntityValidator<HsHostingAssetEntity> {
public HsHostingAssetEntityValidator(final ValidatableProperty<?>... properties) {
static final ValidatableProperty<?>[] NO_EXTRA_PROPERTIES = new ValidatableProperty<?>[0];
private final HsHostingAssetEntityValidator.BookingItem bookingItemValidation;
private final HsHostingAssetEntityValidator.ParentAsset parentAssetValidation;
private final HsHostingAssetEntityValidator.AssignedToAsset assignedToAssetValidation;
private final HsHostingAssetEntityValidator.AlarmContact alarmContactValidation;
HsHostingAssetEntityValidator(
@NotNull final BookingItem bookingItemValidation,
@NotNull final ParentAsset parentAssetValidation,
@NotNull final AssignedToAsset assignedToAssetValidation,
@NotNull final AlarmContact alarmContactValidation,
final ValidatableProperty<?>... properties) {
super(properties);
this.bookingItemValidation = bookingItemValidation;
this.parentAssetValidation = parentAssetValidation;
this.assignedToAssetValidation = assignedToAssetValidation;
this.alarmContactValidation = alarmContactValidation;
}
@Override
public List<String> validate(final HsHostingAssetEntity assetEntity) {
return sequentiallyValidate(
() -> validateProperties(assetEntity),
() -> validateEntityReferences(assetEntity),
() -> validateIdentifierPattern(assetEntity), // might need proper parentAsset or billingItem
() -> optionallyValidate(assetEntity.getBookingItem()),
() -> optionallyValidate(assetEntity.getParentAsset()),
() -> validateAgainstSubEntities(assetEntity)
);
}
private List<String> validateEntityReferences(final HsHostingAssetEntity assetEntity) {
return Stream.of(
validateReferencedEntity(assetEntity, "bookingItem", bookingItemValidation::validate),
validateReferencedEntity(assetEntity, "parentAsset", parentAssetValidation::validate),
validateReferencedEntity(assetEntity, "assignedToAsset", assignedToAssetValidation::validate),
validateReferencedEntity(assetEntity, "alarmContact", alarmContactValidation::validate),
validateProperties(assetEntity))
.filter(Objects::nonNull)
.flatMap(List::stream)
.filter(Objects::nonNull)
.toList();
}
private List<String> validateReferencedEntity(
final HsHostingAssetEntity assetEntity,
final String referenceFieldName,
final BiFunction<HsHostingAssetEntity, String, List<String>> validator) {
return enrich(prefix(assetEntity.toShortString()), validator.apply(assetEntity, referenceFieldName));
}
private List<String> validateProperties(final HsHostingAssetEntity assetEntity) {
return enrich(prefix(assetEntity.toShortString(), "config"), validateProperties(assetEntity.getConfig()));
}
@ -57,6 +102,7 @@ public class HsHostingAssetEntityValidator extends HsEntityValidator<HsHostingAs
.toList());
}
// TODO.test: check, if there are any hosting assets which need this validation at all
private String validateMaxTotalValue(
final HsHostingAssetEntity hostingAsset,
final ValidatableProperty<?> propDef) {
@ -73,4 +119,120 @@ public class HsHostingAssetEntityValidator extends HsEntityValidator<HsHostingAs
propName, maxValue, propUnit, propName, totalValue, propUnit)
: null;
}
private List<String> validateIdentifierPattern(final HsHostingAssetEntity assetEntity) {
final var expectedIdentifierPattern = identifierPattern(assetEntity);
if (assetEntity.getIdentifier() == null ||
!expectedIdentifierPattern.matcher(assetEntity.getIdentifier()).matches()) {
return List.of("'identifier' expected to match '"+expectedIdentifierPattern+"', but is '" + assetEntity.getIdentifier() + "'");
}
return Collections.emptyList();
}
protected abstract Pattern identifierPattern(HsHostingAssetEntity assetEntity);
static abstract class ReferenceValidator<S, T> {
private final Policy policy;
private final T subEntityType;
private final Function<HsHostingAssetEntity, S> subEntityGetter;
private final Function<S,T> subEntityTypeGetter;
public ReferenceValidator(
final Policy policy,
final T subEntityType,
final Function<HsHostingAssetEntity, S> subEntityGetter,
final Function<S, T> subEntityTypeGetter) {
this.policy = policy;
this.subEntityType = subEntityType;
this.subEntityGetter = subEntityGetter;
this.subEntityTypeGetter = subEntityTypeGetter;
}
public ReferenceValidator(
final Policy policy,
final Function<HsHostingAssetEntity, S> subEntityGetter) {
this.policy = policy;
this.subEntityType = null;
this.subEntityGetter = subEntityGetter;
this.subEntityTypeGetter = e -> null;
}
enum Policy {
OPTIONAL, FORBIDDEN, REQUIRED
}
List<String> validate(final HsHostingAssetEntity assetEntity, final String referenceFieldName) {
final var subEntity = subEntityGetter.apply(assetEntity);
if (policy == Policy.REQUIRED && subEntity == null) {
return List.of(referenceFieldName + "' must not be null but is null");
}
if (policy == Policy.FORBIDDEN && subEntity != null) {
return List.of(referenceFieldName + "' must be null but is set to "+ assetEntity.getBookingItem().toShortString());
}
final var subItemType = subEntity != null ? subEntityTypeGetter.apply(subEntity) : null;
if (subEntityType != null && subItemType != subEntityType) {
return List.of(referenceFieldName + "' must be of type " + subEntityType + " but is of type " + subItemType);
}
return emptyList();
}
}
static class BookingItem extends ReferenceValidator<HsBookingItemEntity, HsBookingItemType> {
BookingItem(final Policy policy, final HsBookingItemType bookingItemType) {
super(policy, bookingItemType, HsHostingAssetEntity::getBookingItem, HsBookingItemEntity::getType);
}
static BookingItem mustBeNull() {
return new BookingItem(Policy.FORBIDDEN, null);
}
static BookingItem mustBeOfType(final HsBookingItemType hsBookingItemType) {
return new BookingItem(Policy.REQUIRED, hsBookingItemType);
}
}
static class ParentAsset extends ReferenceValidator<HsHostingAssetEntity, HsHostingAssetType> {
ParentAsset(final ReferenceValidator.Policy policy, final HsHostingAssetType parentAssetType) {
super(policy, parentAssetType, HsHostingAssetEntity::getParentAsset, HsHostingAssetEntity::getType);
}
static ParentAsset mustBeNull() {
return new ParentAsset(Policy.FORBIDDEN, null);
}
static ParentAsset mustBeOfType(final HsHostingAssetType hostingAssetType) {
return new ParentAsset(Policy.REQUIRED, hostingAssetType);
}
static ParentAsset mustBeNullOrOfType(final HsHostingAssetType hostingAssetType) {
return new ParentAsset(Policy.OPTIONAL, hostingAssetType);
}
}
static class AssignedToAsset extends ReferenceValidator<HsHostingAssetEntity, HsHostingAssetType> {
AssignedToAsset(final ReferenceValidator.Policy policy, final HsHostingAssetType assignedToAssetType) {
super(policy, assignedToAssetType, HsHostingAssetEntity::getAssignedToAsset, HsHostingAssetEntity::getType);
}
static AssignedToAsset mustBeNull() {
return new AssignedToAsset(Policy.FORBIDDEN, null);
}
}
static class AlarmContact extends ReferenceValidator<HsOfficeContactEntity, Enum<?>> {
AlarmContact(final ReferenceValidator.Policy policy) {
super(policy, HsHostingAssetEntity::getAlarmContact);
}
static AlarmContact isOptional() {
return new AlarmContact(Policy.OPTIONAL);
}
}
}

View File

@ -14,10 +14,11 @@ public class HsHostingAssetEntityValidatorRegistry {
private static final Map<Enum<HsHostingAssetType>, HsEntityValidator<HsHostingAssetEntity>> validators = new HashMap<>();
static {
register(CLOUD_SERVER, new HsHostingAssetEntityValidator());
// HOWTO: add (register) new HsHostingAssetType-specific validators
register(CLOUD_SERVER, new HsCloudServerHostingAssetValidator());
register(MANAGED_SERVER, new HsManagedServerHostingAssetValidator());
register(MANAGED_WEBSPACE, new HsManagedWebspaceHostingAssetValidator());
register(UNIX_USER, new HsHostingAssetEntityValidator());
register(UNIX_USER, new HsUnixUserHostingAssetValidator());
}
private static void register(final Enum<HsHostingAssetType> type, final HsEntityValidator<HsHostingAssetEntity> validator) {

View File

@ -1,5 +1,10 @@
package net.hostsharing.hsadminng.hs.hosting.asset.validators;
import net.hostsharing.hsadminng.hs.booking.item.HsBookingItemType;
import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetEntity;
import java.util.regex.Pattern;
import static net.hostsharing.hsadminng.hs.validation.BooleanProperty.booleanProperty;
import static net.hostsharing.hsadminng.hs.validation.EnumerationProperty.enumerationProperty;
import static net.hostsharing.hsadminng.hs.validation.IntegerProperty.integerProperty;
@ -8,6 +13,11 @@ class HsManagedServerHostingAssetValidator extends HsHostingAssetEntityValidator
public HsManagedServerHostingAssetValidator() {
super(
BookingItem.mustBeOfType(HsBookingItemType.MANAGED_SERVER),
ParentAsset.mustBeNull(), // until we introduce a hosting asset for 'HOST'
AssignedToAsset.mustBeNull(),
AlarmContact.isOptional(), // hostmaster alert address is implicitly added
// monitoring
integerProperty("monit_max_cpu_usage").unit("%").min(10).max(100).withDefault(92),
integerProperty("monit_max_ram_usage").unit("%").min(10).max(100).withDefault(92),
@ -15,7 +25,6 @@ class HsManagedServerHostingAssetValidator extends HsHostingAssetEntityValidator
integerProperty("monit_min_free_ssd").min(1).max(1000).withDefault(5),
integerProperty("monit_max_hdd_usage").unit("%").min(10).max(100).withDefault(95),
integerProperty("monit_min_free_hdd").min(1).max(4000).withDefault(10),
// stringProperty("monit_alarm_email").unit("GB").optional() TODO.impl: via Contact?
// other settings
// booleanProperty("fastcgi_small").withDefault(false), TODO.spec: clarify Salt-Grains
@ -45,4 +54,9 @@ class HsManagedServerHostingAssetValidator extends HsHostingAssetEntityValidator
booleanProperty("software-imagemagick-ghostscript").withDefault(false)
);
}
@Override
protected Pattern identifierPattern(final HsHostingAssetEntity assetEntity) {
return Pattern.compile("^vm[0-9][0-9][0-9][0-9]$");
}
}

View File

@ -1,29 +1,26 @@
package net.hostsharing.hsadminng.hs.hosting.asset.validators;
import net.hostsharing.hsadminng.hs.booking.item.HsBookingItemType;
import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetEntity;
import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType;
import java.util.Collection;
import java.util.stream.Stream;
import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;
import java.util.regex.Pattern;
class HsManagedWebspaceHostingAssetValidator extends HsHostingAssetEntityValidator {
public HsManagedWebspaceHostingAssetValidator() {
super(BookingItem.mustBeOfType(HsBookingItemType.MANAGED_WEBSPACE),
ParentAsset.mustBeOfType(HsHostingAssetType.MANAGED_SERVER), // the (shared or private) ManagedServer
AssignedToAsset.mustBeNull(),
AlarmContact.isOptional(), // hostmaster alert address is implicitly added
NO_EXTRA_PROPERTIES);
}
@Override
public List<String> validate(final HsHostingAssetEntity assetEntity) {
return Stream.of(validateIdentifierPattern(assetEntity), super.validate(assetEntity))
.flatMap(Collection::stream)
.collect(Collectors.toList());
}
private static List<String> validateIdentifierPattern(final HsHostingAssetEntity assetEntity) {
final var expectedIdentifierPattern = "^" + assetEntity.getParentAsset().getBookingItem().getProject().getDebitor().getDefaultPrefix() + "[0-9][0-9]$";
if ( !assetEntity.getIdentifier().matches(expectedIdentifierPattern)) {
return List.of("'identifier' expected to match '"+expectedIdentifierPattern+"', but is '" + assetEntity.getIdentifier() + "'");
}
return Collections.emptyList();
protected Pattern identifierPattern(final HsHostingAssetEntity assetEntity) {
final var prefixPattern =
!assetEntity.isLoaded()
? assetEntity.getParentAsset().getBookingItem().getProject().getDebitor().getDefaultPrefix()
: "[a-z][a-z0-9][a-z0-9]";
return Pattern.compile("^" + prefixPattern + "[0-9][0-9]$");
}
}

View File

@ -0,0 +1,23 @@
package net.hostsharing.hsadminng.hs.hosting.asset.validators;
import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetEntity;
import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType;
import java.util.regex.Pattern;
class HsUnixUserHostingAssetValidator extends HsHostingAssetEntityValidator {
HsUnixUserHostingAssetValidator() {
super(BookingItem.mustBeNull(),
ParentAsset.mustBeOfType(HsHostingAssetType.MANAGED_WEBSPACE),
AssignedToAsset.mustBeNull(),
AlarmContact.isOptional(), // TODO.spec: for quota notifications
NO_EXTRA_PROPERTIES); // TODO.spec: yet to be specified
}
@Override
protected Pattern identifierPattern(final HsHostingAssetEntity assetEntity) {
final var webspaceIdentifier = assetEntity.getParentAsset().getIdentifier();
return Pattern.compile("^"+webspaceIdentifier+"$|^"+webspaceIdentifier+"-[a-z0-9]+$");
}
}

View File

@ -0,0 +1,40 @@
### HsHostingAssetEntity-Validation
There is just a single `HsHostingAssetEntity` class for all types of hosting assets like Managed-Server, Managed-Webspace, Unix-Users, Databases etc. These are distinguished by `HsHostingAssetType HsHostingAssetEntity.type`.
For each of these types, a distinct validator has to be
implemented as a subclass of `HsHostingAssetEntityValidator` which needs to be registered (see `HsHostingAssetEntityValidatorRegistry`) for the relevant type(s).
### Kinds of Validations
#### Identifier validation
The identifier of a Hosting-Asset is for example the Webspace-Name like "xyz00" or a Unix-User-Name like "xyz00-test".
To validate the identifier, vverride the method `identifierPattern(...)` and return a regular expression to validate the identifier against. The regular expression can depend on the actual entity instance.
#### Reference validation
References in this context are:
- the related Booking-Item,
- the parent-Hosting-Asset,
- the Assigned-To-Hosting-Asset and
- the Contact.
The first parameters of the `HsHostingAssetEntityValidator` superclass take rule descriptors for these references. These are all Subclasses fo
### Validation Order
The validations are called in a sensible order. E.g. if a property value is not numeric, it makes no sense to check the total sum of such values to be within certain numeric values. And if the related booking item is of wrong type, it makes no sense to validate limits against sub-entities.
Properties are validated all at once, though. Thus, if multiple properties fail validation, all error messages are returned at once.
In general, the validation es executed in this order:
1. the entity itself
1. its references
2. its properties
2. the limits of the parent entity (parent asset + booking item)
3. limits against the own own-sub-entities
This implementation can be found in `HsHostingAssetEntityValidator.validate`.

View File

@ -12,13 +12,21 @@ import static net.hostsharing.hsadminng.hs.booking.project.TestHsBookingProject.
@UtilityClass
public class TestHsBookingItem {
public static final HsBookingItemEntity TEST_BOOKING_ITEM = HsBookingItemEntity.builder()
public static final HsBookingItemEntity TEST_MANAGED_SERVER_BOOKING_ITEM = HsBookingItemEntity.builder()
.project(TEST_PROJECT)
.caption("test booking item")
.type(HsBookingItemType.MANAGED_SERVER)
.caption("test project booking item")
.resources(Map.ofEntries(
entry("someThing", 1),
entry("anotherThing", "blue")
))
.validity(Range.closedInfinite(LocalDate.of(2020, 1, 15)))
.build();
public static final HsBookingItemEntity TEST_CLOUD_SERVER_BOOKING_ITEM = HsBookingItemEntity.builder()
.project(TEST_PROJECT)
.type(HsBookingItemType.CLOUD_SERVER)
.caption("test cloud server booking item")
.validity(Range.closedInfinite(LocalDate.of(2020, 1, 15)))
.build();
}

View File

@ -22,6 +22,7 @@ import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.web.server.LocalServerPort;
import org.springframework.transaction.annotation.Transactional;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
@ -220,9 +221,7 @@ class HsHostingAssetControllerAcceptanceTest extends ContextBasedTestWithCleanup
void parentAssetAgent_canAddSubAsset() {
context.define("superuser-alex@hostsharing.net");
final var givenParentAsset = givenParentAsset(MANAGED_SERVER, "vm1011");
context.define("person-FirbySusan@example.com");
final var givenParentAsset = givenParentAsset(MANAGED_WEBSPACE, "fir01");
final var location = RestAssured // @formatter:off
.given()
@ -232,9 +231,9 @@ class HsHostingAssetControllerAcceptanceTest extends ContextBasedTestWithCleanup
.body("""
{
"parentAssetUuid": "%s",
"type": "MANAGED_WEBSPACE",
"identifier": "fir90",
"caption": "some new ManagedWebspace in client's ManagedServer",
"type": "UNIX_USER",
"identifier": "fir01-temp",
"caption": "some new UnixUser in client's ManagedWebspace",
"config": {}
}
""".formatted(givenParentAsset.getUuid()))
@ -246,9 +245,9 @@ class HsHostingAssetControllerAcceptanceTest extends ContextBasedTestWithCleanup
.contentType(ContentType.JSON)
.body("", lenientlyEquals("""
{
"type": "MANAGED_WEBSPACE",
"identifier": "fir90",
"caption": "some new ManagedWebspace in client's ManagedServer",
"type": "UNIX_USER",
"identifier": "fir01-temp",
"caption": "some new UnixUser in client's ManagedWebspace",
"config": {}
}
"""))
@ -265,7 +264,9 @@ class HsHostingAssetControllerAcceptanceTest extends ContextBasedTestWithCleanup
void propertyValidationsArePerformend_whenAddingAsset() {
context.define("superuser-alex@hostsharing.net");
final var givenBookingItem = givenBookingItem("D-1000111 default project", "some PrivateCloud");
final var givenBookingItem = givenSomeNewBookingItem("D-1000111 default project",
HsBookingItemType.MANAGED_SERVER,
"some PrivateCloud");
RestAssured // @formatter:off
.given()
@ -558,10 +559,22 @@ class HsHostingAssetControllerAcceptanceTest extends ContextBasedTestWithCleanup
}).assertSuccessful().returnedValue();
}
HsBookingItemEntity givenBookingItem(final String projectCaption, final String bookingItemCaption) {
return bookingItemRepo.findByCaption(bookingItemCaption).stream()
.filter(bi -> bi.getRelatedProject().getCaption().contains(projectCaption))
.findAny().orElseThrow();
HsBookingItemEntity givenSomeNewBookingItem(final String projectCaption, final HsBookingItemType bookingItemType, final String bookingItemCaption) {
return jpaAttempt.transacted(() -> {
context.define("superuser-alex@hostsharing.net");
final var project = projectRepo.findByCaption(projectCaption).getFirst();
final var resources = switch (bookingItemType) {
case MANAGED_SERVER -> Map.<String, Object>ofEntries(entry("CPUs", 1), entry("RAM", 20), entry("SSD", 25), entry("Traffic", 250));
default -> new HashMap<String, Object>();
};
final var newBookingItem = HsBookingItemEntity.builder()
.project(project)
.type(bookingItemType)
.caption(bookingItemCaption)
.resources(resources)
.build();
return toCleanup(bookingItemRepo.save(newBookingItem));
}).assertSuccessful().returnedValue();
}
HsHostingAssetEntity givenParentAsset(final HsHostingAssetType assetType, final String assetIdentifier) {
@ -574,16 +587,23 @@ class HsHostingAssetControllerAcceptanceTest extends ContextBasedTestWithCleanup
@SafeVarargs
private HsHostingAssetEntity givenSomeTemporaryHostingAsset(final String identifierSuffix,
final HsHostingAssetType hostingAssetType,
final Map.Entry<String, Object>... resources) {
final Map.Entry<String, Object>... config) {
return jpaAttempt.transacted(() -> {
context.define("superuser-alex@hostsharing.net");
final var bookingItemType = switch (hostingAssetType) {
case CLOUD_SERVER -> HsBookingItemType.CLOUD_SERVER;
case MANAGED_SERVER -> HsBookingItemType.MANAGED_SERVER;
case MANAGED_WEBSPACE -> HsBookingItemType.MANAGED_WEBSPACE;
default -> null;
};
final var newBookingItem = givenSomeNewBookingItem("D-1000111 default project", bookingItemType, "temp ManagedServer");
final var newAsset = HsHostingAssetEntity.builder()
.uuid(UUID.randomUUID())
.bookingItem(givenBookingItem("D-1000111 default project", "some ManagedServer"))
.bookingItem(newBookingItem)
.type(hostingAssetType)
.identifier("vm" + identifierSuffix)
.caption("some test-asset")
.config(Map.ofEntries(resources))
.config(Map.ofEntries(config))
.build();
return assetRepo.save(newAsset);

View File

@ -15,7 +15,7 @@ import java.util.Map;
import java.util.UUID;
import java.util.stream.Stream;
import static net.hostsharing.hsadminng.hs.booking.item.TestHsBookingItem.TEST_BOOKING_ITEM;
import static net.hostsharing.hsadminng.hs.booking.item.TestHsBookingItem.TEST_CLOUD_SERVER_BOOKING_ITEM;
import static net.hostsharing.hsadminng.mapper.PatchMap.entry;
import static net.hostsharing.hsadminng.mapper.PatchMap.patchMap;
import static org.junit.jupiter.api.TestInstance.Lifecycle.PER_CLASS;
@ -70,7 +70,7 @@ class HsHostingAssetEntityPatcherUnitTest extends PatchUnitTestBase<
protected HsHostingAssetEntity newInitialEntity() {
final var entity = new HsHostingAssetEntity();
entity.setUuid(INITIAL_BOOKING_ITEM_UUID);
entity.setBookingItem(TEST_BOOKING_ITEM);
entity.setBookingItem(TEST_CLOUD_SERVER_BOOKING_ITEM);
entity.getConfig().putAll(KeyValueMap.from(INITIAL_CONFIG));
entity.setCaption(INITIAL_CAPTION);
entity.setAlarmContact(givenInitialContact);

View File

@ -5,13 +5,13 @@ import org.junit.jupiter.api.Test;
import java.util.Map;
import static java.util.Map.entry;
import static net.hostsharing.hsadminng.hs.booking.item.TestHsBookingItem.TEST_BOOKING_ITEM;
import static net.hostsharing.hsadminng.hs.booking.item.TestHsBookingItem.TEST_CLOUD_SERVER_BOOKING_ITEM;
import static org.assertj.core.api.Assertions.assertThat;
class HsHostingAssetEntityUnitTest {
final HsHostingAssetEntity givenParentAsset = HsHostingAssetEntity.builder()
.bookingItem(TEST_BOOKING_ITEM)
.bookingItem(TEST_CLOUD_SERVER_BOOKING_ITEM)
.type(HsHostingAssetType.MANAGED_SERVER)
.identifier("vm1234")
.caption("some managed asset")
@ -21,7 +21,7 @@ class HsHostingAssetEntityUnitTest {
entry("HDD-storage", 2048)))
.build();
final HsHostingAssetEntity givenWebspace = HsHostingAssetEntity.builder()
.bookingItem(TEST_BOOKING_ITEM)
.bookingItem(TEST_CLOUD_SERVER_BOOKING_ITEM)
.type(HsHostingAssetType.MANAGED_WEBSPACE)
.parentAsset(givenParentAsset)
.identifier("xyz00")
@ -58,7 +58,7 @@ class HsHostingAssetEntityUnitTest {
void toStringContainsAllPropertiesAndResourcesSortedByKey() {
assertThat(givenWebspace.toString()).isEqualTo(
"HsHostingAssetEntity(MANAGED_WEBSPACE, xyz00, some managed webspace, MANAGED_SERVER:vm1234, D-1234500:test project:test booking item, { CPUs: 2, HDD-storage: 2048, SSD-storage: 512 })");
"HsHostingAssetEntity(MANAGED_WEBSPACE, xyz00, some managed webspace, MANAGED_SERVER:vm1234, D-1234500:test project:test cloud server booking item, { CPUs: 2, HDD-storage: 2048, SSD-storage: 512 })");
assertThat(givenUnixUser.toString()).isEqualTo(
"HsHostingAssetEntity(UNIX_USER, xyz00-web, some unix-user, MANAGED_WEBSPACE:xyz00, { HDD-hard-quota: 512, HDD-soft-quota: 256, SSD-hard-quota: 256, SSD-soft-quota: 128 })");

View File

@ -90,6 +90,7 @@ class HsHostingAssetRepositoryIntegrationTest extends ContextBasedTestWithCleanu
result.assertSuccessful();
assertThat(result.returnedValue()).isNotNull().extracting(HsHostingAssetEntity::getUuid).isNotNull();
assertThatAssetIsPersisted(result.returnedValue());
assertThat(result.returnedValue().isLoaded()).isFalse();
assertThat(assetRepo.count()).isEqualTo(count + 1);
}
@ -413,5 +414,6 @@ class HsHostingAssetRepositoryIntegrationTest extends ContextBasedTestWithCleanu
assertThat(actualResult)
.extracting(HsHostingAssetEntity::toString)
.contains(serverNames);
actualResult.forEach(loadedEntity -> assertThat(loadedEntity.isLoaded()).isTrue());
}
}

View File

@ -1,12 +1,16 @@
package net.hostsharing.hsadminng.hs.hosting.asset.validators;
import net.hostsharing.hsadminng.hs.booking.item.HsBookingItemEntity;
import net.hostsharing.hsadminng.hs.booking.item.HsBookingItemType;
import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetEntity;
import org.junit.jupiter.api.Test;
import java.util.Map;
import static java.util.Map.entry;
import static net.hostsharing.hsadminng.hs.booking.item.TestHsBookingItem.TEST_CLOUD_SERVER_BOOKING_ITEM;
import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.CLOUD_SERVER;
import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.MANAGED_SERVER;
import static org.assertj.core.api.Assertions.assertThat;
class HsCloudServerHostingAssetValidatorUnitTest {
@ -28,7 +32,28 @@ class HsCloudServerHostingAssetValidatorUnitTest {
final var result = validator.validate(cloudServerHostingAssetEntity);
// then
assertThat(result).containsExactly("'CLOUD_SERVER:vm1234.config.RAM' is not expected but is set to '2000'");
assertThat(result).containsExactlyInAnyOrder(
"'CLOUD_SERVER:vm1234.bookingItem' must not be null but is null",
"'CLOUD_SERVER:vm1234.config.RAM' is not expected but is set to '2000'");
}
@Test
void validatesInvalidIdentifier() {
// given
final var cloudServerHostingAssetEntity = HsHostingAssetEntity.builder()
.type(CLOUD_SERVER)
.identifier("xyz99")
.bookingItem(TEST_CLOUD_SERVER_BOOKING_ITEM)
.build();
final var validator = HsHostingAssetEntityValidatorRegistry.forType(cloudServerHostingAssetEntity.getType());
// when
final var result = validator.validate(cloudServerHostingAssetEntity);
// then
assertThat(result).containsExactlyInAnyOrder(
"'identifier' expected to match '^vm[0-9][0-9][0-9][0-9]$', but is 'xyz99'");
}
@Test
@ -39,4 +64,43 @@ class HsCloudServerHostingAssetValidatorUnitTest {
// then
assertThat(validator.properties()).map(Map::toString).isEmpty();
}
@Test
void validatesBookingItemType() {
// given
final var mangedServerHostingAssetEntity = HsHostingAssetEntity.builder()
.type(MANAGED_SERVER)
.identifier("xyz00")
.bookingItem(HsBookingItemEntity.builder().type(HsBookingItemType.CLOUD_SERVER).build())
.build();
final var validator = HsHostingAssetEntityValidatorRegistry.forType(mangedServerHostingAssetEntity.getType());
// when
final var result = validator.validate(mangedServerHostingAssetEntity);
// then
assertThat(result).containsExactlyInAnyOrder(
"'MANAGED_SERVER:xyz00.bookingItem' must be of type MANAGED_SERVER but is of type CLOUD_SERVER");
}
@Test
void validatesParentAndAssignedToAssetMustNotBeSet() {
// given
final var mangedServerHostingAssetEntity = HsHostingAssetEntity.builder()
.type(CLOUD_SERVER)
.identifier("xyz00")
.parentAsset(HsHostingAssetEntity.builder().build())
.assignedToAsset(HsHostingAssetEntity.builder().build())
.bookingItem(HsBookingItemEntity.builder().type(HsBookingItemType.CLOUD_SERVER).build())
.build();
final var validator = HsHostingAssetEntityValidatorRegistry.forType(mangedServerHostingAssetEntity.getType());
// when
final var result = validator.validate(mangedServerHostingAssetEntity);
// then
assertThat(result).containsExactlyInAnyOrder(
"'CLOUD_SERVER:xyz00.parentAsset' must be null but is set to D-???????-?:null",
"'CLOUD_SERVER:xyz00.assignedToAsset' must be null but is set to D-???????-?:null");
}
}

View File

@ -0,0 +1,64 @@
package net.hostsharing.hsadminng.hs.hosting.asset.validators;
import net.hostsharing.hsadminng.hs.booking.item.HsBookingItemEntity;
import net.hostsharing.hsadminng.hs.booking.item.HsBookingItemType;
import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetEntity;
import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType;
import org.junit.jupiter.api.Test;
import java.util.Map;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.catchThrowable;
import static org.assertj.core.api.Assertions.entry;
class HsHostingAssetEntityValidatorRegistryUnitTest {
@Test
void forTypeWithUnknownTypeThrowsException() {
// when
final var thrown = catchThrowable(() -> {
HsHostingAssetEntityValidatorRegistry.forType(null);
});
// then
assertThat(thrown).hasMessage("no validator found for type null");
}
@Test
void typesReturnsAllImplementedTypes() {
// when
final var types = HsHostingAssetEntityValidatorRegistry.types();
// then
// TODO.test: when all types are implemented, replace with set of all types:
// assertThat(types).isEqualTo(EnumSet.allOf(HsHostingAssetType.class));
// also remove "Implemented" from the test method name.
assertThat(types).containsExactlyInAnyOrder(
HsHostingAssetType.CLOUD_SERVER,
HsHostingAssetType.MANAGED_SERVER,
HsHostingAssetType.MANAGED_WEBSPACE,
HsHostingAssetType.UNIX_USER
);
}
@Test
void validatedDoesNotThrowAnExceptionForValidEntity() {
final var givenBookingItem = HsBookingItemEntity.builder()
.type(HsBookingItemType.CLOUD_SERVER)
.resources(Map.ofEntries(
entry("CPUs", 4),
entry("RAM", 20),
entry("SSD", 50),
entry("Traffic", 250)
))
.build();
final var validEntity = HsHostingAssetEntity.builder()
.type(HsHostingAssetType.CLOUD_SERVER)
.bookingItem(givenBookingItem)
.identifier("vm1234")
.caption("some valid cloud server")
.build();
HsHostingAssetEntityValidatorRegistry.validated(validEntity);
}
}

View File

@ -1,5 +1,7 @@
package net.hostsharing.hsadminng.hs.hosting.asset.validators;
import net.hostsharing.hsadminng.hs.booking.item.HsBookingItemEntity;
import net.hostsharing.hsadminng.hs.booking.item.HsBookingItemType;
import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetEntity;
import org.junit.jupiter.api.Test;
@ -16,12 +18,18 @@ class HsHostingAssetEntityValidatorUnitTest {
final var managedServerHostingAssetEntity = HsHostingAssetEntity.builder()
.type(MANAGED_SERVER)
.identifier("vm1234")
.bookingItem(HsBookingItemEntity.builder().type(HsBookingItemType.MANAGED_SERVER).build())
.parentAsset(HsHostingAssetEntity.builder().type(MANAGED_SERVER).build())
.assignedToAsset(HsHostingAssetEntity.builder().type(MANAGED_SERVER).build())
.build();
// when
final var result = catchThrowable( ()-> HsHostingAssetEntityValidatorRegistry.validated(managedServerHostingAssetEntity));
// then
assertThat(result).isNull(); // all required properties have defaults
assertThat(result.getMessage()).contains(
"'MANAGED_SERVER:vm1234.parentAsset' must be null but is set to D-???????-?:null",
"'MANAGED_SERVER:vm1234.assignedToAsset' must be null but is set to D-???????-?:null"
);
}
}

View File

@ -1,5 +1,7 @@
package net.hostsharing.hsadminng.hs.hosting.asset.validators;
import net.hostsharing.hsadminng.hs.booking.item.HsBookingItemEntity;
import net.hostsharing.hsadminng.hs.booking.item.HsBookingItemType;
import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetEntity;
import org.junit.jupiter.api.Test;
@ -17,6 +19,9 @@ class HsManagedServerHostingAssetValidatorUnitTest {
final var mangedWebspaceHostingAssetEntity = HsHostingAssetEntity.builder()
.type(MANAGED_SERVER)
.identifier("vm1234")
.bookingItem(HsBookingItemEntity.builder().type(HsBookingItemType.MANAGED_SERVER).build())
.parentAsset(HsHostingAssetEntity.builder().build())
.assignedToAsset(HsHostingAssetEntity.builder().build())
.config(Map.ofEntries(
entry("monit_max_hdd_usage", "90"),
entry("monit_max_cpu_usage", 2),
@ -30,8 +35,50 @@ class HsManagedServerHostingAssetValidatorUnitTest {
// then
assertThat(result).containsExactlyInAnyOrder(
"'MANAGED_SERVER:vm1234.parentAsset' must be null but is set to D-???????-?:null",
"'MANAGED_SERVER:vm1234.assignedToAsset' must be null but is set to D-???????-?:null",
"'MANAGED_SERVER:vm1234.config.monit_max_cpu_usage' is expected to be >= 10 but is 2",
"'MANAGED_SERVER:vm1234.config.monit_max_ram_usage' is expected to be <= 100 but is 101",
"'MANAGED_SERVER:vm1234.config.monit_max_hdd_usage' is expected to be of type class java.lang.Integer, but is of type 'String'");
}
@Test
void validatesInvalidIdentifier() {
// given
final var mangedServerHostingAssetEntity = HsHostingAssetEntity.builder()
.type(MANAGED_SERVER)
.identifier("xyz00")
.bookingItem(HsBookingItemEntity.builder().type(HsBookingItemType.MANAGED_SERVER).build())
.build();
final var validator = HsHostingAssetEntityValidatorRegistry.forType(mangedServerHostingAssetEntity.getType());
// when
final var result = validator.validate(mangedServerHostingAssetEntity);
// then
assertThat(result).containsExactlyInAnyOrder(
"'identifier' expected to match '^vm[0-9][0-9][0-9][0-9]$', but is 'xyz00'");
}
@Test
void validatesParentAndAssignedToAssetMustNotBeSet() {
// given
final var mangedServerHostingAssetEntity = HsHostingAssetEntity.builder()
.type(MANAGED_SERVER)
.identifier("xyz00")
.parentAsset(HsHostingAssetEntity.builder().build())
.assignedToAsset(HsHostingAssetEntity.builder().build())
.bookingItem(HsBookingItemEntity.builder().type(HsBookingItemType.CLOUD_SERVER).build())
.build();
final var validator = HsHostingAssetEntityValidatorRegistry.forType(mangedServerHostingAssetEntity.getType());
// when
final var result = validator.validate(mangedServerHostingAssetEntity);
// then
assertThat(result).containsExactlyInAnyOrder(
"'MANAGED_SERVER:xyz00.bookingItem' must be of type MANAGED_SERVER but is of type CLOUD_SERVER",
"'MANAGED_SERVER:xyz00.parentAsset' must be null but is set to D-???????-?:null",
"'MANAGED_SERVER:xyz00.assignedToAsset' must be null but is set to D-???????-?:null");
}
}

View File

@ -28,6 +28,11 @@ class HsManagedWebspaceHostingAssetValidatorUnitTest {
entry("SLA-EMail", true)
))
.build();
final HsBookingItemEntity cloudServerBookingItem = managedServerBookingItem.toBuilder()
.type(HsBookingItemType.CLOUD_SERVER)
.caption("Test Cloud-Server")
.build();
final HsHostingAssetEntity mangedServerAssetEntity = HsHostingAssetEntity.builder()
.type(HsHostingAssetType.MANAGED_SERVER)
.bookingItem(managedServerBookingItem)
@ -38,13 +43,46 @@ class HsManagedWebspaceHostingAssetValidatorUnitTest {
entry("monit_max_ram_usage", 90)
))
.build();
final HsHostingAssetEntity cloudServerAssetEntity = HsHostingAssetEntity.builder()
.type(HsHostingAssetType.CLOUD_SERVER)
.bookingItem(cloudServerBookingItem)
.identifier("vm1234")
.config(Map.ofEntries(
entry("monit_max_ssd_usage", 70),
entry("monit_max_cpu_usage", 80),
entry("monit_max_ram_usage", 90)
))
.build();
@Test
void validatesIdentifier() {
void acceptsAlienIdentifierPrefixForPreExistingEntity() {
// given
final var validator = HsHostingAssetEntityValidatorRegistry.forType(MANAGED_WEBSPACE);
final var mangedWebspaceHostingAssetEntity = HsHostingAssetEntity.builder()
.type(MANAGED_WEBSPACE)
.bookingItem(HsBookingItemEntity.builder()
.type(HsBookingItemType.MANAGED_WEBSPACE)
.resources(Map.ofEntries(entry("SSD", 25), entry("Traffic", 250)))
.build())
.parentAsset(mangedServerAssetEntity)
.identifier("xyz00")
.isLoaded(true)
.build();
// when
final var result = validator.validate(mangedWebspaceHostingAssetEntity);
// then
assertThat(result).isEmpty();
}
@Test
void validatesIdentifierAndReferencedEntities() {
// given
final var validator = HsHostingAssetEntityValidatorRegistry.forType(MANAGED_WEBSPACE);
final var mangedWebspaceHostingAssetEntity = HsHostingAssetEntity.builder()
.type(MANAGED_WEBSPACE)
.bookingItem(HsBookingItemEntity.builder().type(HsBookingItemType.MANAGED_WEBSPACE).build())
.parentAsset(mangedServerAssetEntity)
.identifier("xyz00")
.build();
@ -62,6 +100,7 @@ class HsManagedWebspaceHostingAssetValidatorUnitTest {
final var validator = HsHostingAssetEntityValidatorRegistry.forType(MANAGED_WEBSPACE);
final var mangedWebspaceHostingAssetEntity = HsHostingAssetEntity.builder()
.type(MANAGED_WEBSPACE)
.bookingItem(HsBookingItemEntity.builder().type(HsBookingItemType.MANAGED_WEBSPACE).build())
.parentAsset(mangedServerAssetEntity)
.identifier("abc00")
.config(Map.ofEntries(
@ -82,6 +121,11 @@ class HsManagedWebspaceHostingAssetValidatorUnitTest {
final var validator = HsHostingAssetEntityValidatorRegistry.forType(MANAGED_WEBSPACE);
final var mangedWebspaceHostingAssetEntity = HsHostingAssetEntity.builder()
.type(MANAGED_WEBSPACE)
.bookingItem(HsBookingItemEntity.builder()
.type(HsBookingItemType.MANAGED_WEBSPACE)
.caption("some ManagedWebspace")
.resources(Map.ofEntries(entry("SSD", 25), entry("Traffic", 250)))
.build())
.parentAsset(mangedServerAssetEntity)
.identifier("abc00")
.build();
@ -92,4 +136,30 @@ class HsManagedWebspaceHostingAssetValidatorUnitTest {
// then
assertThat(result).isEmpty();
}
@Test
void validatesEntityReferences() {
// given
final var validator = HsHostingAssetEntityValidatorRegistry.forType(MANAGED_WEBSPACE);
final var mangedWebspaceHostingAssetEntity = HsHostingAssetEntity.builder()
.type(MANAGED_WEBSPACE)
.bookingItem(HsBookingItemEntity.builder()
.type(HsBookingItemType.MANAGED_SERVER)
.caption("some ManagedServer")
.resources(Map.ofEntries(entry("SSD", 25), entry("Traffic", 250)))
.build())
.parentAsset(cloudServerAssetEntity)
.assignedToAsset(HsHostingAssetEntity.builder().build())
.identifier("abc00")
.build();
// when
final var result = validator.validate(mangedWebspaceHostingAssetEntity);
// then
assertThat(result).containsExactly(
"'MANAGED_WEBSPACE:abc00.bookingItem' must be of type MANAGED_WEBSPACE but is of type MANAGED_SERVER",
"'MANAGED_WEBSPACE:abc00.parentAsset' must be of type MANAGED_SERVER but is of type CLOUD_SERVER",
"'MANAGED_WEBSPACE:abc00.assignedToAsset' must be null but is set to D-???????-?:some ManagedServer");
}
}

View File

@ -0,0 +1,30 @@
package net.hostsharing.hsadminng.hs.hosting.asset.validators;
import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetEntity;
import org.junit.jupiter.api.Test;
import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.MANAGED_WEBSPACE;
import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.UNIX_USER;
import static org.assertj.core.api.Assertions.assertThat;
class HsUnixUserHostingAssetValidatorUnitTest {
@Test
void validatesInvalidIdentifier() {
// given
final var unixUserHostingAsset = HsHostingAssetEntity.builder()
.type(UNIX_USER)
.parentAsset(HsHostingAssetEntity.builder().type(MANAGED_WEBSPACE).identifier("abc00").build())
.identifier("xyz99-temp")
.build();
final var validator = HsHostingAssetEntityValidatorRegistry.forType(unixUserHostingAsset.getType());
// when
final var result = validator.validate(unixUserHostingAsset);
// then
assertThat(result).containsExactly(
"'identifier' expected to match '^abc00$|^abc00-[a-z0-9]+$', but is 'xyz99-temp'");
}
}