diff --git a/src/main/java/net/hostsharing/hsadminng/errors/CustomErrorResponse.java b/src/main/java/net/hostsharing/hsadminng/errors/CustomErrorResponse.java index 2714b817..9b182137 100644 --- a/src/main/java/net/hostsharing/hsadminng/errors/CustomErrorResponse.java +++ b/src/main/java/net/hostsharing/hsadminng/errors/CustomErrorResponse.java @@ -9,7 +9,7 @@ import org.springframework.web.context.request.WebRequest; import java.time.LocalDateTime; @Getter -class CustomErrorResponse { +public class CustomErrorResponse { static ResponseEntity errorResponse( final WebRequest request, diff --git a/src/main/java/net/hostsharing/hsadminng/errors/MultiValidationException.java b/src/main/java/net/hostsharing/hsadminng/errors/MultiValidationException.java new file mode 100644 index 00000000..9a6d459d --- /dev/null +++ b/src/main/java/net/hostsharing/hsadminng/errors/MultiValidationException.java @@ -0,0 +1,19 @@ +package net.hostsharing.hsadminng.errors; + +import jakarta.validation.ValidationException; +import java.util.List; + +import static java.lang.String.join; + +public class MultiValidationException extends ValidationException { + + private MultiValidationException(final List violations) { + super("[\n" + join(",\n", violations) + "\n]"); + } + + public static void throwInvalid(final List violations) { + if (!violations.isEmpty()) { + throw new MultiValidationException(violations); + } + } +} diff --git a/src/main/java/net/hostsharing/hsadminng/errors/RestResponseEntityExceptionHandler.java b/src/main/java/net/hostsharing/hsadminng/errors/RestResponseEntityExceptionHandler.java index 5d675484..d4d6e8bf 100644 --- a/src/main/java/net/hostsharing/hsadminng/errors/RestResponseEntityExceptionHandler.java +++ b/src/main/java/net/hostsharing/hsadminng/errors/RestResponseEntityExceptionHandler.java @@ -73,9 +73,10 @@ public class RestResponseEntityExceptionHandler } @ExceptionHandler({ Iban4jException.class, ValidationException.class }) - protected ResponseEntity handleIbanAndBicExceptions( + protected ResponseEntity handleValidationExceptions( final Throwable exc, final WebRequest request) { - final var message = line(NestedExceptionUtils.getMostSpecificCause(exc).getMessage(), 0); + final String fullMessage = NestedExceptionUtils.getMostSpecificCause(exc).getMessage(); + final var message = exc instanceof MultiValidationException ? fullMessage : line(fullMessage, 0); return errorResponse(request, HttpStatus.BAD_REQUEST, message); } diff --git a/src/main/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemController.java b/src/main/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemController.java index 2ada5e0c..1343378c 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemController.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemController.java @@ -5,6 +5,7 @@ import net.hostsharing.hsadminng.hs.booking.generated.api.v1.api.HsBookingItemsA import net.hostsharing.hsadminng.hs.booking.generated.api.v1.model.HsBookingItemInsertResource; import net.hostsharing.hsadminng.hs.booking.generated.api.v1.model.HsBookingItemPatchResource; import net.hostsharing.hsadminng.hs.booking.generated.api.v1.model.HsBookingItemResource; +import net.hostsharing.hsadminng.hs.booking.item.validators.HsBookingItemEntityValidatorRegistry; import net.hostsharing.hsadminng.mapper.KeyValueMap; import net.hostsharing.hsadminng.mapper.Mapper; import org.springframework.beans.factory.annotation.Autowired; @@ -13,11 +14,12 @@ import org.springframework.transaction.annotation.Transactional; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.servlet.mvc.method.annotation.MvcUriComponentsBuilder; +import jakarta.persistence.EntityManager; +import jakarta.persistence.PersistenceContext; import java.util.List; import java.util.UUID; import java.util.function.BiConsumer; -import static net.hostsharing.hsadminng.hs.booking.item.validators.HsBookingItemEntityValidators.valid; import static net.hostsharing.hsadminng.mapper.PostgresDateRange.toPostgresDateRange; @RestController @@ -32,6 +34,9 @@ public class HsBookingItemController implements HsBookingItemsApi { @Autowired private HsBookingItemRepository bookingItemRepo; + @PersistenceContext + private EntityManager em; + @Override @Transactional(readOnly = true) public ResponseEntity> listBookingItemsByProjectUuid( @@ -57,7 +62,7 @@ public class HsBookingItemController implements HsBookingItemsApi { final var entityToSave = mapper.map(body, HsBookingItemEntity.class, RESOURCE_TO_ENTITY_POSTMAPPER); - final var saved = bookingItemRepo.save(valid(entityToSave)); + final var saved = HsBookingItemEntityValidatorRegistry.validated(bookingItemRepo.save(entityToSave)); final var uri = MvcUriComponentsBuilder.fromController(getClass()) @@ -78,6 +83,7 @@ public class HsBookingItemController implements HsBookingItemsApi { context.define(currentUser, assumedRoles); final var result = bookingItemRepo.findByUuid(bookingItemUuid); + result.ifPresent(entity -> em.detach(entity)); // prevent further LAZY-loading return result .map(bookingItemEntity -> ResponseEntity.ok( mapper.map(bookingItemEntity, HsBookingItemResource.class, ENTITY_TO_RESOURCE_POSTMAPPER))) @@ -112,7 +118,7 @@ public class HsBookingItemController implements HsBookingItemsApi { new HsBookingItemEntityPatcher(current).apply(body); - final var saved = bookingItemRepo.save(valid(current)); + final var saved = bookingItemRepo.save(HsBookingItemEntityValidatorRegistry.validated(current)); final var mapped = mapper.map(saved, HsBookingItemResource.class, ENTITY_TO_RESOURCE_POSTMAPPER); return ResponseEntity.ok(mapped); } diff --git a/src/main/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemEntity.java b/src/main/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemEntity.java index 1c5040e7..b820c243 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemEntity.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemEntity.java @@ -10,7 +10,7 @@ import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; import net.hostsharing.hsadminng.hs.booking.project.HsBookingProjectEntity; -import net.hostsharing.hsadminng.hs.validation.Validatable; +import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetEntity; import net.hostsharing.hsadminng.mapper.PatchableMapWrapper; import net.hostsharing.hsadminng.rbac.rbacdef.RbacView; import net.hostsharing.hsadminng.rbac.rbacdef.RbacView.SQL; @@ -19,6 +19,7 @@ import net.hostsharing.hsadminng.stringify.Stringify; import net.hostsharing.hsadminng.stringify.Stringifyable; import org.hibernate.annotations.Type; +import jakarta.persistence.CascadeType; import jakarta.persistence.Column; import jakarta.persistence.Entity; import jakarta.persistence.EnumType; @@ -27,12 +28,14 @@ import jakarta.persistence.GeneratedValue; import jakarta.persistence.Id; import jakarta.persistence.JoinColumn; import jakarta.persistence.ManyToOne; +import jakarta.persistence.OneToMany; import jakarta.persistence.Table; import jakarta.persistence.Transient; import jakarta.persistence.Version; import java.io.IOException; import java.time.LocalDate; import java.util.HashMap; +import java.util.List; import java.util.Map; import java.util.UUID; @@ -62,7 +65,7 @@ import static net.hostsharing.hsadminng.stringify.Stringify.stringify; @Setter @NoArgsConstructor @AllArgsConstructor -public class HsBookingItemEntity implements Stringifyable, RbacObject, Validatable { +public class HsBookingItemEntity implements Stringifyable, RbacObject { private static Stringify stringify = stringify(HsBookingItemEntity.class) .withProp(HsBookingItemEntity::getProject) @@ -105,6 +108,14 @@ public class HsBookingItemEntity implements Stringifyable, RbacObject, Validatab @Column(columnDefinition = "resources") private Map resources = new HashMap<>(); + @OneToMany(cascade = CascadeType.REFRESH, orphanRemoval = true) + @JoinColumn(name="parentitemuuid", referencedColumnName="uuid") + private List subBookingItems; + + @OneToMany(cascade = CascadeType.REFRESH, orphanRemoval = true) + @JoinColumn(name="bookingitemuuid", referencedColumnName="uuid") + private List subHostingAssets; + @Transient private PatchableMapWrapper resourcesWrapper; @@ -150,16 +161,6 @@ public class HsBookingItemEntity implements Stringifyable, RbacObject, Validatab return parentItem == null ? null : parentItem.relatedProject(); } - @Override - public String getPropertiesName() { - return "resources"; - } - - @Override - public Map getProperties() { - return resources; - } - public HsBookingProjectEntity getRelatedProject() { return project != null ? project : parentItem.getRelatedProject(); } diff --git a/src/main/java/net/hostsharing/hsadminng/hs/booking/item/validators/HsBookingItemEntityValidator.java b/src/main/java/net/hostsharing/hsadminng/hs/booking/item/validators/HsBookingItemEntityValidator.java new file mode 100644 index 00000000..7d002bac --- /dev/null +++ b/src/main/java/net/hostsharing/hsadminng/hs/booking/item/validators/HsBookingItemEntityValidator.java @@ -0,0 +1,77 @@ +package net.hostsharing.hsadminng.hs.booking.item.validators; + +import net.hostsharing.hsadminng.hs.booking.item.HsBookingItemEntity; +import net.hostsharing.hsadminng.hs.validation.HsEntityValidator; +import net.hostsharing.hsadminng.hs.validation.ValidatableProperty; + +import java.util.Collection; +import java.util.List; +import java.util.Objects; +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 HsBookingItemEntityValidator extends HsEntityValidator { + + public HsBookingItemEntityValidator(final ValidatableProperty... properties) { + super(properties); + } + + public List validate(final HsBookingItemEntity bookingItem) { + return sequentiallyValidate( + () -> validateProperties(bookingItem), + () -> optionallyValidate(bookingItem.getParentItem()), + () -> validateAgainstSubEntities(bookingItem) + ); + } + + private List validateProperties(final HsBookingItemEntity bookingItem) { + return enrich(prefix(bookingItem.toShortString(), "resources"), validateProperties(bookingItem.getResources())); + } + + private static List optionallyValidate(final HsBookingItemEntity bookingItem) { + return bookingItem != null + ? enrich(prefix(bookingItem.toShortString(), ""), + HsBookingItemEntityValidatorRegistry.doValidate(bookingItem)) + : emptyList(); + } + + protected List validateAgainstSubEntities(final HsBookingItemEntity bookingItem) { + return enrich(prefix(bookingItem.toShortString(), "resources"), + Stream.concat( + stream(propertyValidators) + .map(propDef -> propDef.validateTotals(bookingItem)) + .flatMap(Collection::stream), + stream(propertyValidators) + .filter(ValidatableProperty::isTotalsValidator) + .map(prop -> validateMaxTotalValue(bookingItem, prop)) + ).filter(Objects::nonNull).toList()); + } + + // TODO.refa: convert into generic shape like multi-options validator + private static String validateMaxTotalValue( + final HsBookingItemEntity bookingItem, + final ValidatableProperty propDef) { + final var propName = propDef.propertyName(); + final var propUnit = ofNullable(propDef.unit()).map(u -> " " + u).orElse(""); + final var totalValue = ofNullable(bookingItem.getSubBookingItems()).orElse(emptyList()) + .stream() + .map(subItem -> propDef.getValue(subItem.getResources())) + .map(HsBookingItemEntityValidator::toNonNullInteger) + .reduce(0, Integer::sum); + final var maxValue = getNonNullIntegerValue(propDef, bookingItem.getResources()); + if (propDef.thresholdPercentage() != null ) { + return totalValue > (maxValue * propDef.thresholdPercentage() / 100) + ? "%s' maximum total is %d%s, but actual total %s %d%s, which exceeds threshold of %d%%" + .formatted(propName, maxValue, propUnit, propName, totalValue, propUnit, propDef.thresholdPercentage()) + : null; + } else { + return totalValue > maxValue + ? "%s' maximum total is %d%s, but actual total %s %d%s" + .formatted(propName, maxValue, propUnit, propName, totalValue, propUnit) + : null; + } + } +} diff --git a/src/main/java/net/hostsharing/hsadminng/hs/booking/item/validators/HsBookingItemEntityValidators.java b/src/main/java/net/hostsharing/hsadminng/hs/booking/item/validators/HsBookingItemEntityValidatorRegistry.java similarity index 56% rename from src/main/java/net/hostsharing/hsadminng/hs/booking/item/validators/HsBookingItemEntityValidators.java rename to src/main/java/net/hostsharing/hsadminng/hs/booking/item/validators/HsBookingItemEntityValidatorRegistry.java index 1f4493e2..e067781e 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/booking/item/validators/HsBookingItemEntityValidators.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/booking/item/validators/HsBookingItemEntityValidatorRegistry.java @@ -1,12 +1,12 @@ package net.hostsharing.hsadminng.hs.booking.item.validators; -import lombok.experimental.UtilityClass; import net.hostsharing.hsadminng.hs.booking.item.HsBookingItemEntity; import net.hostsharing.hsadminng.hs.booking.item.HsBookingItemType; import net.hostsharing.hsadminng.hs.validation.HsEntityValidator; +import net.hostsharing.hsadminng.errors.MultiValidationException; -import jakarta.validation.ValidationException; import java.util.HashMap; +import java.util.List; import java.util.Map; import java.util.Set; @@ -14,37 +14,42 @@ import static java.util.Arrays.stream; import static net.hostsharing.hsadminng.hs.booking.item.HsBookingItemType.CLOUD_SERVER; import static net.hostsharing.hsadminng.hs.booking.item.HsBookingItemType.MANAGED_SERVER; import static net.hostsharing.hsadminng.hs.booking.item.HsBookingItemType.MANAGED_WEBSPACE; +import static net.hostsharing.hsadminng.hs.booking.item.HsBookingItemType.PRIVATE_CLOUD; -@UtilityClass -public class HsBookingItemEntityValidators { +public class HsBookingItemEntityValidatorRegistry { - private static final Map, HsEntityValidator> validators = new HashMap<>(); + private static final Map, HsEntityValidator> validators = new HashMap<>(); static { + register(PRIVATE_CLOUD, new HsPrivateCloudBookingItemValidator()); register(CLOUD_SERVER, new HsCloudServerBookingItemValidator()); register(MANAGED_SERVER, new HsManagedServerBookingItemValidator()); register(MANAGED_WEBSPACE, new HsManagedWebspaceBookingItemValidator()); } - private static void register(final Enum type, final HsEntityValidator validator) { + private static void register(final Enum type, final HsEntityValidator validator) { stream(validator.propertyValidators).forEach( entry -> { entry.verifyConsistency(Map.entry(type, validator)); }); validators.put(type, validator); } - public static HsEntityValidator forType(final Enum type) { - return validators.get(type); + public static HsEntityValidator forType(final Enum type) { + if ( validators.containsKey(type)) { + return validators.get(type); + } + throw new IllegalArgumentException("no validator found for type " + type); } public static Set> types() { return validators.keySet(); } - public static HsBookingItemEntity valid(final HsBookingItemEntity entityToSave) { - final var violations = HsBookingItemEntityValidators.forType(entityToSave.getType()).validate(entityToSave); - if (!violations.isEmpty()) { - throw new ValidationException(violations.toString()); - } + public static List doValidate(final HsBookingItemEntity bookingItem) { + return HsBookingItemEntityValidatorRegistry.forType(bookingItem.getType()).validate(bookingItem); + } + + public static HsBookingItemEntity validated(final HsBookingItemEntity entityToSave) { + MultiValidationException.throwInvalid(doValidate(entityToSave)); return entityToSave; } } diff --git a/src/main/java/net/hostsharing/hsadminng/hs/booking/item/validators/HsCloudServerBookingItemValidator.java b/src/main/java/net/hostsharing/hsadminng/hs/booking/item/validators/HsCloudServerBookingItemValidator.java index fa09f2c3..07bb80da 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/booking/item/validators/HsCloudServerBookingItemValidator.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/booking/item/validators/HsCloudServerBookingItemValidator.java @@ -1,20 +1,18 @@ package net.hostsharing.hsadminng.hs.booking.item.validators; -import net.hostsharing.hsadminng.hs.booking.item.HsBookingItemEntity; -import net.hostsharing.hsadminng.hs.booking.item.HsBookingItemType; -import net.hostsharing.hsadminng.hs.validation.HsEntityValidator; -import static net.hostsharing.hsadminng.hs.validation.EnumerationPropertyValidator.enumerationProperty; -import static net.hostsharing.hsadminng.hs.validation.IntegerPropertyValidator.integerProperty; -class HsCloudServerBookingItemValidator extends HsEntityValidator { +import static net.hostsharing.hsadminng.hs.validation.EnumerationProperty.enumerationProperty; +import static net.hostsharing.hsadminng.hs.validation.IntegerProperty.integerProperty; + +class HsCloudServerBookingItemValidator extends HsBookingItemEntityValidator { HsCloudServerBookingItemValidator() { super( integerProperty("CPUs").min(1).max(32).required(), integerProperty("RAM").unit("GB").min(1).max(128).required(), integerProperty("SSD").unit("GB").min(25).max(1000).step(25).required(), - integerProperty("HDD").unit("GB").min(0).max(4000).step(250).optional(), + integerProperty("HDD").unit("GB").min(0).max(4000).step(250).withDefault(0), integerProperty("Traffic").unit("GB").min(250).max(10000).step(250).required(), enumerationProperty("SLA-Infrastructure").values("BASIC", "EXT8H", "EXT4H", "EXT2H").optional() ); diff --git a/src/main/java/net/hostsharing/hsadminng/hs/booking/item/validators/HsManagedServerBookingItemValidator.java b/src/main/java/net/hostsharing/hsadminng/hs/booking/item/validators/HsManagedServerBookingItemValidator.java index 79c41070..a267b104 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/booking/item/validators/HsManagedServerBookingItemValidator.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/booking/item/validators/HsManagedServerBookingItemValidator.java @@ -1,24 +1,22 @@ package net.hostsharing.hsadminng.hs.booking.item.validators; -import net.hostsharing.hsadminng.hs.booking.item.HsBookingItemEntity; -import net.hostsharing.hsadminng.hs.booking.item.HsBookingItemType; -import net.hostsharing.hsadminng.hs.validation.HsEntityValidator; -import static net.hostsharing.hsadminng.hs.validation.BooleanPropertyValidator.booleanProperty; -import static net.hostsharing.hsadminng.hs.validation.EnumerationPropertyValidator.enumerationProperty; -import static net.hostsharing.hsadminng.hs.validation.IntegerPropertyValidator.integerProperty; -class HsManagedServerBookingItemValidator extends HsEntityValidator { +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; + +class HsManagedServerBookingItemValidator extends HsBookingItemEntityValidator { HsManagedServerBookingItemValidator() { super( integerProperty("CPUs").min(1).max(32).required(), integerProperty("RAM").unit("GB").min(1).max(128).required(), - integerProperty("SSD").unit("GB").min(25).max(1000).step(25).required(), - integerProperty("HDD").unit("GB").min(0).max(4000).step(250).optional(), - integerProperty("Traffic").unit("GB").min(250).max(10000).step(250).required(), - enumerationProperty("SLA-Platform").values("BASIC", "EXT8H", "EXT4H", "EXT2H").optional(), - booleanProperty("SLA-EMail").falseIf("SLA-Platform", "BASIC").optional(), + integerProperty("SSD").unit("GB").min(25).max(1000).step(25).required().asTotalLimit().withThreshold(200), + integerProperty("HDD").unit("GB").min(0).max(4000).step(250).withDefault(0).asTotalLimit().withThreshold(200), + integerProperty("Traffic").unit("GB").min(250).max(10000).step(250).required().asTotalLimit().withThreshold(200), + enumerationProperty("SLA-Platform").values("BASIC", "EXT8H", "EXT4H", "EXT2H").withDefault("BASIC"), + booleanProperty("SLA-EMail").falseIf("SLA-Platform", "BASIC").withDefault(false), booleanProperty("SLA-Maria").falseIf("SLA-Platform", "BASIC").optional(), booleanProperty("SLA-PgSQL").falseIf("SLA-Platform", "BASIC").optional(), booleanProperty("SLA-Office").falseIf("SLA-Platform", "BASIC").optional(), diff --git a/src/main/java/net/hostsharing/hsadminng/hs/booking/item/validators/HsManagedWebspaceBookingItemValidator.java b/src/main/java/net/hostsharing/hsadminng/hs/booking/item/validators/HsManagedWebspaceBookingItemValidator.java index 482d0900..bf637f15 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/booking/item/validators/HsManagedWebspaceBookingItemValidator.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/booking/item/validators/HsManagedWebspaceBookingItemValidator.java @@ -1,24 +1,98 @@ package net.hostsharing.hsadminng.hs.booking.item.validators; import net.hostsharing.hsadminng.hs.booking.item.HsBookingItemEntity; -import net.hostsharing.hsadminng.hs.booking.item.HsBookingItemType; -import net.hostsharing.hsadminng.hs.validation.HsEntityValidator; +import net.hostsharing.hsadminng.hs.validation.IntegerProperty; +import org.apache.commons.lang3.function.TriFunction; +import java.util.List; -import static net.hostsharing.hsadminng.hs.validation.BooleanPropertyValidator.booleanProperty; -import static net.hostsharing.hsadminng.hs.validation.EnumerationPropertyValidator.enumerationProperty; -import static net.hostsharing.hsadminng.hs.validation.IntegerPropertyValidator.integerProperty; +import static java.util.Collections.emptyList; +import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.DOMAIN_EMAIL_SETUP; +import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.EMAIL_ADDRESS; +import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.MARIADB_DATABASE; +import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.MARIADB_USER; +import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.PGSQL_DATABASE; +import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.PGSQL_USER; +import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.UNIX_USER; +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; -class HsManagedWebspaceBookingItemValidator extends HsEntityValidator { +class HsManagedWebspaceBookingItemValidator extends HsBookingItemEntityValidator { public HsManagedWebspaceBookingItemValidator() { super( integerProperty("SSD").unit("GB").min(1).max(100).step(1).required(), integerProperty("HDD").unit("GB").min(0).max(250).step(10).optional(), integerProperty("Traffic").unit("GB").min(10).max(1000).step(10).required(), - enumerationProperty("SLA-Platform").values("BASIC", "EXT24H").optional(), - integerProperty("Daemons").min(0).max(10).optional(), - booleanProperty("Online Office Server").optional() + integerProperty("Multi").min(1).max(100).step(1).withDefault(1) + .eachComprising( 25, unixUsers()) + .eachComprising( 5, databaseUsers()) + .eachComprising( 5, databases()) + .eachComprising(250, eMailAddresses()), + integerProperty("Daemons").min(0).max(10).withDefault(0), + booleanProperty("Online Office Server").optional(), + enumerationProperty("SLA-Platform").values("BASIC", "EXT24H").withDefault("BASIC") ); } + + private static TriFunction> unixUsers() { + return (final HsBookingItemEntity entity, final IntegerProperty prop, final Integer factor) -> { + final var unixUserCount = entity.getSubHostingAssets().stream() + .flatMap(ha -> ha.getSubHostingAssets().stream()) + .filter(ha -> ha.getType() == UNIX_USER) + .count(); + final long limitingValue = prop.getValue(entity.getResources()); + if (unixUserCount > factor*limitingValue) { + return List.of(prop.propertyName() + "=" + limitingValue + " allows at maximum " + limitingValue*factor + " unix users, but " + unixUserCount + " found"); + } + return emptyList(); + }; + } + + private static TriFunction> databaseUsers() { + return (final HsBookingItemEntity entity, final IntegerProperty prop, final Integer factor) -> { + final var unixUserCount = entity.getSubHostingAssets().stream() + .flatMap(ha -> ha.getSubHostingAssets().stream()) + .filter(bi -> bi.getType() == PGSQL_USER || bi.getType() == MARIADB_USER ) + .count(); + final long limitingValue = prop.getValue(entity.getResources()); + if (unixUserCount > factor*limitingValue) { + return List.of(prop.propertyName() + "=" + limitingValue + " allows at maximum " + limitingValue*factor + " database users, but " + unixUserCount + " found"); + } + return emptyList(); + }; + } + + private static TriFunction> databases() { + return (final HsBookingItemEntity entity, final IntegerProperty prop, final Integer factor) -> { + final var unixUserCount = entity.getSubHostingAssets().stream() + .flatMap(ha -> ha.getSubHostingAssets().stream()) + .filter(bi -> bi.getType()==PGSQL_USER || bi.getType()==MARIADB_USER ) + .flatMap(domainEMailSetup -> domainEMailSetup.getSubHostingAssets().stream() + .filter(ha -> ha.getType()==PGSQL_DATABASE || ha.getType()==MARIADB_DATABASE)) + .count(); + final long limitingValue = prop.getValue(entity.getResources()); + if (unixUserCount > factor*limitingValue) { + return List.of(prop.propertyName() + "=" + limitingValue + " allows at maximum " + limitingValue*factor + " databases, but " + unixUserCount + " found"); + } + return emptyList(); + }; + } + + private static TriFunction> eMailAddresses() { + return (final HsBookingItemEntity entity, final IntegerProperty prop, final Integer factor) -> { + final var unixUserCount = entity.getSubHostingAssets().stream() + .flatMap(ha -> ha.getSubHostingAssets().stream()) + .filter(bi -> bi.getType() == DOMAIN_EMAIL_SETUP) + .flatMap(domainEMailSetup -> domainEMailSetup.getSubHostingAssets().stream() + .filter(ha -> ha.getType()==EMAIL_ADDRESS)) + .count(); + final long limitingValue = prop.getValue(entity.getResources()); + if (unixUserCount > factor*limitingValue) { + return List.of(prop.propertyName() + "=" + limitingValue + " allows at maximum " + limitingValue*factor + " databases, but " + unixUserCount + " found"); + } + return emptyList(); + }; + } } diff --git a/src/main/java/net/hostsharing/hsadminng/hs/booking/item/validators/HsPrivateCloudBookingItemValidator.java b/src/main/java/net/hostsharing/hsadminng/hs/booking/item/validators/HsPrivateCloudBookingItemValidator.java new file mode 100644 index 00000000..317f2f0c --- /dev/null +++ b/src/main/java/net/hostsharing/hsadminng/hs/booking/item/validators/HsPrivateCloudBookingItemValidator.java @@ -0,0 +1,18 @@ +package net.hostsharing.hsadminng.hs.booking.item.validators; + +import static net.hostsharing.hsadminng.hs.validation.EnumerationProperty.enumerationProperty; +import static net.hostsharing.hsadminng.hs.validation.IntegerProperty.integerProperty; + +class HsPrivateCloudBookingItemValidator extends HsBookingItemEntityValidator { + + HsPrivateCloudBookingItemValidator() { + super( + integerProperty("CPUs").min(4).max(128).required().asTotalLimit(), + integerProperty("RAM").unit("GB").min(4).max(512).required().asTotalLimit(), + integerProperty("SSD").unit("GB").min(100).max(4000).step(25).required().asTotalLimit(), + integerProperty("HDD").unit("GB").min(0).max(16000).step(25).withDefault(0).asTotalLimit(), + integerProperty("Traffic").unit("GB").min(1000).max(40000).step(250).required().asTotalLimit(), + enumerationProperty("SLA-Infrastructure").values("BASIC", "EXT8H", "EXT4H", "EXT2H").withDefault("BASIC") + ); + } +} diff --git a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetController.java b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetController.java index a645bb78..76003671 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetController.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetController.java @@ -20,7 +20,7 @@ import java.util.List; import java.util.UUID; import java.util.function.BiConsumer; -import static net.hostsharing.hsadminng.hs.hosting.asset.validators.HsHostingAssetEntityValidators.valid; +import static net.hostsharing.hsadminng.hs.hosting.asset.validators.HsHostingAssetEntityValidatorRegistry.validated; @RestController public class HsHostingAssetController implements HsHostingAssetsApi { @@ -62,7 +62,7 @@ public class HsHostingAssetController implements HsHostingAssetsApi { final var entityToSave = mapper.map(body, HsHostingAssetEntity.class, RESOURCE_TO_ENTITY_POSTMAPPER); - final var saved = assetRepo.save(valid(entityToSave)); + final var saved = validated(assetRepo.save(entityToSave)); final var uri = MvcUriComponentsBuilder.fromController(getClass()) @@ -117,7 +117,7 @@ public class HsHostingAssetController implements HsHostingAssetsApi { new HsHostingAssetEntityPatcher(current).apply(body); - final var saved = assetRepo.save(valid(current)); + final var saved = validated(assetRepo.save(current)); final var mapped = mapper.map(saved, HsHostingAssetResource.class); return ResponseEntity.ok(mapped); } diff --git a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetEntity.java b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetEntity.java index 8d573c48..3f8202ef 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetEntity.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetEntity.java @@ -8,7 +8,6 @@ import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; import net.hostsharing.hsadminng.hs.booking.item.HsBookingItemEntity; -import net.hostsharing.hsadminng.hs.validation.Validatable; import net.hostsharing.hsadminng.mapper.PatchableMapWrapper; import net.hostsharing.hsadminng.rbac.rbacdef.RbacView; import net.hostsharing.hsadminng.rbac.rbacdef.RbacView.SQL; @@ -17,6 +16,7 @@ import net.hostsharing.hsadminng.stringify.Stringify; import net.hostsharing.hsadminng.stringify.Stringifyable; import org.hibernate.annotations.Type; +import jakarta.persistence.CascadeType; import jakarta.persistence.Column; import jakarta.persistence.Entity; import jakarta.persistence.EnumType; @@ -25,11 +25,13 @@ import jakarta.persistence.GeneratedValue; import jakarta.persistence.Id; import jakarta.persistence.JoinColumn; import jakarta.persistence.ManyToOne; +import jakarta.persistence.OneToMany; import jakarta.persistence.Table; import jakarta.persistence.Transient; import jakarta.persistence.Version; import java.io.IOException; import java.util.HashMap; +import java.util.List; import java.util.Map; import java.util.UUID; @@ -56,7 +58,7 @@ import static net.hostsharing.hsadminng.stringify.Stringify.stringify; @Setter @NoArgsConstructor @AllArgsConstructor -public class HsHostingAssetEntity implements Stringifyable, RbacObject, Validatable { +public class HsHostingAssetEntity implements Stringifyable, RbacObject { private static Stringify stringify = stringify(HsHostingAssetEntity.class) .withProp(HsHostingAssetEntity::getType) @@ -91,6 +93,10 @@ public class HsHostingAssetEntity implements Stringifyable, RbacObject, Validata @Enumerated(EnumType.STRING) private HsHostingAssetType type; + @OneToMany(cascade = CascadeType.REFRESH, orphanRemoval = true) + @JoinColumn(name="parentassetuuid", referencedColumnName="uuid") + private List subHostingAssets; + @Column(name = "identifier") private String identifier; // vm1234, xyz00, example.org, xyz00_abc @@ -114,16 +120,6 @@ public class HsHostingAssetEntity implements Stringifyable, RbacObject, Validata PatchableMapWrapper.of(configWrapper, (newWrapper) -> {configWrapper = newWrapper; }, config).assign(newConfg); } - @Override - public String getPropertiesName() { - return "config"; - } - - @Override - public Map getProperties() { - return config; - } - @Override public String toString() { return stringify.apply(this); diff --git a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetPropsController.java b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetPropsController.java index 47852310..0da530bd 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetPropsController.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetPropsController.java @@ -1,6 +1,6 @@ package net.hostsharing.hsadminng.hs.hosting.asset; -import net.hostsharing.hsadminng.hs.hosting.asset.validators.HsHostingAssetEntityValidators; +import net.hostsharing.hsadminng.hs.hosting.asset.validators.HsHostingAssetEntityValidatorRegistry; import net.hostsharing.hsadminng.hs.hosting.generated.api.v1.api.HsHostingAssetPropsApi; import net.hostsharing.hsadminng.hs.hosting.generated.api.v1.model.HsHostingAssetTypeResource; import org.springframework.http.ResponseEntity; @@ -15,7 +15,7 @@ public class HsHostingAssetPropsController implements HsHostingAssetPropsApi { @Override public ResponseEntity> listAssetTypes() { - final var resource = HsHostingAssetEntityValidators.types().stream() + final var resource = HsHostingAssetEntityValidatorRegistry.types().stream() .map(Enum::name) .toList(); return ResponseEntity.ok(resource); @@ -25,7 +25,8 @@ public class HsHostingAssetPropsController implements HsHostingAssetPropsApi { public ResponseEntity> listAssetTypeProps( final HsHostingAssetTypeResource assetType) { - final var propValidators = HsHostingAssetEntityValidators.forType(HsHostingAssetType.of(assetType)); + final Enum type = HsHostingAssetType.of(assetType); + final var propValidators = HsHostingAssetEntityValidatorRegistry.forType(type); final List> resource = propValidators.properties(); return ResponseEntity.ok(toListOfObjects(resource)); } diff --git a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsHostingAssetEntityValidator.java b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsHostingAssetEntityValidator.java new file mode 100644 index 00000000..3a0438ee --- /dev/null +++ b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsHostingAssetEntityValidator.java @@ -0,0 +1,76 @@ +package net.hostsharing.hsadminng.hs.hosting.asset.validators; + +import net.hostsharing.hsadminng.hs.booking.item.HsBookingItemEntity; +import net.hostsharing.hsadminng.hs.booking.item.validators.HsBookingItemEntityValidatorRegistry; +import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetEntity; +import net.hostsharing.hsadminng.hs.validation.HsEntityValidator; +import net.hostsharing.hsadminng.hs.validation.ValidatableProperty; + +import java.util.List; +import java.util.Objects; + +import static java.util.Arrays.stream; +import static java.util.Collections.emptyList; +import static java.util.Optional.ofNullable; + +public class HsHostingAssetEntityValidator extends HsEntityValidator { + + public HsHostingAssetEntityValidator(final ValidatableProperty... properties) { + super(properties); + } + + + @Override + public List validate(final HsHostingAssetEntity assetEntity) { + return sequentiallyValidate( + () -> validateProperties(assetEntity), + () -> optionallyValidate(assetEntity.getBookingItem()), + () -> optionallyValidate(assetEntity.getParentAsset()), + () -> validateAgainstSubEntities(assetEntity) + ); + } + + private List validateProperties(final HsHostingAssetEntity assetEntity) { + return enrich(prefix(assetEntity.toShortString(), "config"), validateProperties(assetEntity.getConfig())); + } + + private static List optionallyValidate(final HsHostingAssetEntity assetEntity) { + return assetEntity != null + ? enrich(prefix(assetEntity.toShortString(), "parentAsset"), + HsHostingAssetEntityValidatorRegistry.forType(assetEntity.getType()).validate(assetEntity)) + : emptyList(); + } + + private static List optionallyValidate(final HsBookingItemEntity bookingItem) { + return bookingItem != null + ? enrich(prefix(bookingItem.toShortString(), "bookingItem"), + HsBookingItemEntityValidatorRegistry.doValidate(bookingItem)) + : emptyList(); + } + + protected List validateAgainstSubEntities(final HsHostingAssetEntity assetEntity) { + return enrich(prefix(assetEntity.toShortString(), "config"), + stream(propertyValidators) + .filter(ValidatableProperty::isTotalsValidator) + .map(prop -> validateMaxTotalValue(assetEntity, prop)) + .filter(Objects::nonNull) + .toList()); + } + + private String validateMaxTotalValue( + final HsHostingAssetEntity hostingAsset, + final ValidatableProperty propDef) { + final var propName = propDef.propertyName(); + final var propUnit = ofNullable(propDef.unit()).map(u -> " " + u).orElse(""); + final var totalValue = ofNullable(hostingAsset.getSubHostingAssets()).orElse(emptyList()) + .stream() + .map(subItem -> propDef.getValue(subItem.getConfig())) + .map(HsEntityValidator::toNonNullInteger) + .reduce(0, Integer::sum); + final var maxValue = getNonNullIntegerValue(propDef, hostingAsset.getConfig()); + return totalValue > maxValue + ? "%s' maximum total is %d%s, but actual total is %s %d%s".formatted( + propName, maxValue, propUnit, propName, totalValue, propUnit) + : null; + } +} diff --git a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsHostingAssetEntityValidatorRegistry.java b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsHostingAssetEntityValidatorRegistry.java new file mode 100644 index 00000000..a1cac8e0 --- /dev/null +++ b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsHostingAssetEntityValidatorRegistry.java @@ -0,0 +1,50 @@ +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 net.hostsharing.hsadminng.hs.validation.HsEntityValidator; +import net.hostsharing.hsadminng.errors.MultiValidationException; + +import java.util.*; + +import static java.util.Arrays.stream; +import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.*; + +public class HsHostingAssetEntityValidatorRegistry { + + private static final Map, HsEntityValidator> validators = new HashMap<>(); + static { + register(CLOUD_SERVER, new HsHostingAssetEntityValidator()); + register(MANAGED_SERVER, new HsManagedServerHostingAssetValidator()); + register(MANAGED_WEBSPACE, new HsManagedWebspaceHostingAssetValidator()); + register(UNIX_USER, new HsHostingAssetEntityValidator()); + } + + private static void register(final Enum type, final HsEntityValidator validator) { + stream(validator.propertyValidators).forEach( entry -> { + entry.verifyConsistency(Map.entry(type, validator)); + }); + validators.put(type, validator); + } + + public static HsEntityValidator forType(final Enum type) { + if ( validators.containsKey(type)) { + return validators.get(type); + } + throw new IllegalArgumentException("no validator found for type " + type); + } + + public static Set> types() { + return validators.keySet(); + } + + public static List doValidate(final HsHostingAssetEntity hostingAsset) { + return HsHostingAssetEntityValidatorRegistry.forType(hostingAsset.getType()).validate(hostingAsset); + } + + public static HsHostingAssetEntity validated(final HsHostingAssetEntity entityToSave) { + MultiValidationException.throwInvalid(doValidate(entityToSave)); + return entityToSave; + } + +} diff --git a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsHostingAssetEntityValidators.java b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsHostingAssetEntityValidators.java deleted file mode 100644 index 11df9a84..00000000 --- a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsHostingAssetEntityValidators.java +++ /dev/null @@ -1,51 +0,0 @@ -package net.hostsharing.hsadminng.hs.hosting.asset.validators; - -import lombok.experimental.UtilityClass; -import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetEntity; -import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType; -import net.hostsharing.hsadminng.hs.validation.HsEntityValidator; - -import jakarta.validation.ValidationException; -import java.util.HashMap; -import java.util.Map; -import java.util.Set; - -import static java.util.Arrays.stream; -import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.CLOUD_SERVER; -import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.MANAGED_SERVER; -import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.MANAGED_WEBSPACE; - -@UtilityClass -public class HsHostingAssetEntityValidators { - - private static final Map, HsEntityValidator> validators = new HashMap<>(); - static { - register(CLOUD_SERVER, new HsEntityValidator<>()); - register(MANAGED_SERVER, new HsManagedServerHostingAssetValidator()); - register(MANAGED_WEBSPACE, new HsManagedWebspaceHostingAssetValidator()); - } - - private static void register(final Enum type, final HsEntityValidator validator) { - stream(validator.propertyValidators).forEach( entry -> { - entry.verifyConsistency(Map.entry(type, validator)); - }); - validators.put(type, validator); - } - - public static HsEntityValidator forType(final Enum type) { - return validators.get(type); - } - - public static Set> types() { - return validators.keySet(); - } - - - public static HsHostingAssetEntity valid(final HsHostingAssetEntity entityToSave) { - final var violations = HsHostingAssetEntityValidators.forType(entityToSave.getType()).validate(entityToSave); - if (!violations.isEmpty()) { - throw new ValidationException(violations.toString()); - } - return entityToSave; - } -} diff --git a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsManagedServerHostingAssetValidator.java b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsManagedServerHostingAssetValidator.java index 35f3b81d..b2107866 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsManagedServerHostingAssetValidator.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsManagedServerHostingAssetValidator.java @@ -1,12 +1,8 @@ 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 net.hostsharing.hsadminng.hs.validation.HsEntityValidator; +import static net.hostsharing.hsadminng.hs.validation.IntegerProperty.integerProperty; -import static net.hostsharing.hsadminng.hs.validation.IntegerPropertyValidator.integerProperty; - -class HsManagedServerHostingAssetValidator extends HsEntityValidator { +class HsManagedServerHostingAssetValidator extends HsHostingAssetEntityValidator { public HsManagedServerHostingAssetValidator() { super( diff --git a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsManagedWebspaceHostingAssetValidator.java b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsManagedWebspaceHostingAssetValidator.java index ffef39d7..19c9dc24 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsManagedWebspaceHostingAssetValidator.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsManagedWebspaceHostingAssetValidator.java @@ -1,28 +1,29 @@ 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 net.hostsharing.hsadminng.hs.validation.HsEntityValidator; +import java.util.Collection; +import java.util.stream.Stream; +import java.util.Collections; import java.util.List; +import java.util.stream.Collectors; - -class HsManagedWebspaceHostingAssetValidator extends HsEntityValidator { +class HsManagedWebspaceHostingAssetValidator extends HsHostingAssetEntityValidator { public HsManagedWebspaceHostingAssetValidator() { } @Override public List validate(final HsHostingAssetEntity assetEntity) { - final var result = super.validate(assetEntity); - validateIdentifierPattern(result, assetEntity); - - return result; + return Stream.of(validateIdentifierPattern(assetEntity), super.validate(assetEntity)) + .flatMap(Collection::stream) + .collect(Collectors.toList()); } - private static void validateIdentifierPattern(final List result, final HsHostingAssetEntity assetEntity) { + private static List validateIdentifierPattern(final HsHostingAssetEntity assetEntity) { final var expectedIdentifierPattern = "^" + assetEntity.getParentAsset().getBookingItem().getProject().getDebitor().getDefaultPrefix() + "[0-9][0-9]$"; if ( !assetEntity.getIdentifier().matches(expectedIdentifierPattern)) { - result.add("'identifier' expected to match '"+expectedIdentifierPattern+"', but is '" + assetEntity.getIdentifier() + "'"); + return List.of("'identifier' expected to match '"+expectedIdentifierPattern+"', but is '" + assetEntity.getIdentifier() + "'"); } + return Collections.emptyList(); } } diff --git a/src/main/java/net/hostsharing/hsadminng/hs/office/coopassets/HsOfficeCoopAssetsTransactionController.java b/src/main/java/net/hostsharing/hsadminng/hs/office/coopassets/HsOfficeCoopAssetsTransactionController.java index a22065c0..6279ad05 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/office/coopassets/HsOfficeCoopAssetsTransactionController.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/office/coopassets/HsOfficeCoopAssetsTransactionController.java @@ -3,6 +3,7 @@ package net.hostsharing.hsadminng.hs.office.coopassets; import net.hostsharing.hsadminng.context.Context; import net.hostsharing.hsadminng.hs.office.generated.api.v1.api.HsOfficeCoopAssetsApi; import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.*; +import net.hostsharing.hsadminng.errors.MultiValidationException; import net.hostsharing.hsadminng.mapper.Mapper; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.format.annotation.DateTimeFormat; @@ -13,14 +14,12 @@ import org.springframework.web.bind.annotation.RestController; import org.springframework.web.servlet.mvc.method.annotation.MvcUriComponentsBuilder; import jakarta.persistence.EntityNotFoundException; -import jakarta.validation.ValidationException; import java.time.LocalDate; import java.util.ArrayList; import java.util.List; import java.util.UUID; import java.util.function.BiConsumer; -import static java.lang.String.join; import static net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficeCoopAssetsTransactionTypeResource.*; @RestController @@ -97,9 +96,7 @@ public class HsOfficeCoopAssetsTransactionController implements HsOfficeCoopAsse validateDebitTransaction(requestBody, violations); validateCreditTransaction(requestBody, violations); validateAssetValue(requestBody, violations); - if (violations.size() > 0) { - throw new ValidationException("[" + join(", ", violations) + "]"); - } + MultiValidationException.throwInvalid(violations); } private static void validateDebitTransaction( diff --git a/src/main/java/net/hostsharing/hsadminng/hs/office/coopshares/HsOfficeCoopSharesTransactionController.java b/src/main/java/net/hostsharing/hsadminng/hs/office/coopshares/HsOfficeCoopSharesTransactionController.java index 9a3295a2..f90d5276 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/office/coopshares/HsOfficeCoopSharesTransactionController.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/office/coopshares/HsOfficeCoopSharesTransactionController.java @@ -5,6 +5,7 @@ import net.hostsharing.hsadminng.context.Context; import net.hostsharing.hsadminng.hs.office.generated.api.v1.api.HsOfficeCoopSharesApi; import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficeCoopSharesTransactionInsertResource; import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficeCoopSharesTransactionResource; +import net.hostsharing.hsadminng.errors.MultiValidationException; import net.hostsharing.hsadminng.mapper.Mapper; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.format.annotation.DateTimeFormat; @@ -14,14 +15,12 @@ import org.springframework.transaction.annotation.Transactional; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.servlet.mvc.method.annotation.MvcUriComponentsBuilder; -import jakarta.validation.ValidationException; import java.time.LocalDate; import java.util.ArrayList; import java.util.List; import java.util.UUID; import java.util.function.BiConsumer; -import static java.lang.String.join; import static net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficeCoopSharesTransactionTypeResource.CANCELLATION; import static net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficeCoopSharesTransactionTypeResource.SUBSCRIPTION; @@ -99,9 +98,7 @@ public class HsOfficeCoopSharesTransactionController implements HsOfficeCoopShar validateSubscriptionTransaction(requestBody, violations); validateCancellationTransaction(requestBody, violations); validateshareCount(requestBody, violations); - if (violations.size() > 0) { - throw new ValidationException("[" + join(", ", violations) + "]"); - } + MultiValidationException.throwInvalid(violations); } private static void validateSubscriptionTransaction( diff --git a/src/main/java/net/hostsharing/hsadminng/hs/validation/BooleanProperty.java b/src/main/java/net/hostsharing/hsadminng/hs/validation/BooleanProperty.java new file mode 100644 index 00000000..9d664683 --- /dev/null +++ b/src/main/java/net/hostsharing/hsadminng/hs/validation/BooleanProperty.java @@ -0,0 +1,46 @@ +package net.hostsharing.hsadminng.hs.validation; + +import lombok.Setter; +import net.hostsharing.hsadminng.mapper.Array; + +import java.util.AbstractMap; +import java.util.ArrayList; +import java.util.Map; +import java.util.Objects; + +@Setter +public class BooleanProperty extends ValidatableProperty { + + private static final String[] KEY_ORDER = Array.join(ValidatableProperty.KEY_ORDER_HEAD, ValidatableProperty.KEY_ORDER_TAIL); + + private Map.Entry falseIf; + + private BooleanProperty(final String propertyName) { + super(Boolean.class, propertyName, KEY_ORDER); + } + + public static BooleanProperty booleanProperty(final String propertyName) { + return new BooleanProperty(propertyName); + } + + public ValidatableProperty falseIf(final String refPropertyName, final String refPropertyValue) { + this.falseIf = new AbstractMap.SimpleImmutableEntry<>(refPropertyName, refPropertyValue); + return this; + } + + @Override + protected void validate(final ArrayList result, final Boolean propValue, final Map props) { + if (falseIf != null && propValue) { + final Object referencedValue = props.get(falseIf.getKey()); + if (Objects.equals(referencedValue, falseIf.getValue())) { + result.add(propertyName + "' is expected to be false because " + + falseIf.getKey() + "=" + referencedValue + " but is " + propValue); + } + } + } + + @Override + protected String simpleTypeName() { + return "boolean"; + } +} diff --git a/src/main/java/net/hostsharing/hsadminng/hs/validation/BooleanPropertyValidator.java b/src/main/java/net/hostsharing/hsadminng/hs/validation/BooleanPropertyValidator.java deleted file mode 100644 index 2838e0f5..00000000 --- a/src/main/java/net/hostsharing/hsadminng/hs/validation/BooleanPropertyValidator.java +++ /dev/null @@ -1,42 +0,0 @@ -package net.hostsharing.hsadminng.hs.validation; - -import lombok.Setter; - -import java.util.AbstractMap; -import java.util.ArrayList; -import java.util.Map; -import java.util.Objects; - -@Setter -public class BooleanPropertyValidator extends HsPropertyValidator { - - private Map.Entry falseIf; - - private BooleanPropertyValidator(final String propertyName) { - super(Boolean.class, propertyName); - } - - public static BooleanPropertyValidator booleanProperty(final String propertyName) { - return new BooleanPropertyValidator(propertyName); - } - - public HsPropertyValidator falseIf(final String refPropertyName, final String refPropertyValue) { - this.falseIf = new AbstractMap.SimpleImmutableEntry<>(refPropertyName, refPropertyValue); - return this; - } - - @Override - protected void validate(final ArrayList result, final String propertiesName, final Boolean propValue, final Map props) { - if (falseIf != null && !Objects.equals(props.get(falseIf.getKey()), falseIf.getValue())) { - if (propValue) { - result.add("'"+propertiesName+"." + propertyName + "' is expected to be false because " + - propertiesName+"." + falseIf.getKey()+ "=" + falseIf.getValue() + " but is " + propValue); - } - } - } - - @Override - protected String simpleTypeName() { - return "boolean"; - } -} diff --git a/src/main/java/net/hostsharing/hsadminng/hs/validation/EnumerationProperty.java b/src/main/java/net/hostsharing/hsadminng/hs/validation/EnumerationProperty.java new file mode 100644 index 00000000..23e5ef61 --- /dev/null +++ b/src/main/java/net/hostsharing/hsadminng/hs/validation/EnumerationProperty.java @@ -0,0 +1,44 @@ +package net.hostsharing.hsadminng.hs.validation; + +import lombok.Setter; +import net.hostsharing.hsadminng.mapper.Array; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Map; + +@Setter +public class EnumerationProperty extends ValidatableProperty { + + private static final String[] KEY_ORDER = Array.join( + ValidatableProperty.KEY_ORDER_HEAD, + Array.of("values"), + ValidatableProperty.KEY_ORDER_TAIL); + + private String[] values; + + private EnumerationProperty(final String propertyName) { + super(String.class, propertyName, KEY_ORDER); + } + + public static EnumerationProperty enumerationProperty(final String propertyName) { + return new EnumerationProperty(propertyName); + } + + public ValidatableProperty values(final String... values) { + this.values = values; + return this; + } + + @Override + protected void validate(final ArrayList result, final String propValue, final Map props) { + if (Arrays.stream(values).noneMatch(v -> v.equals(propValue))) { + result.add(propertyName + "' is expected to be one of " + Arrays.toString(values) + " but is '" + propValue + "'"); + } + } + + @Override + protected String simpleTypeName() { + return "enumeration"; + } +} diff --git a/src/main/java/net/hostsharing/hsadminng/hs/validation/EnumerationPropertyValidator.java b/src/main/java/net/hostsharing/hsadminng/hs/validation/EnumerationPropertyValidator.java deleted file mode 100644 index 329feb74..00000000 --- a/src/main/java/net/hostsharing/hsadminng/hs/validation/EnumerationPropertyValidator.java +++ /dev/null @@ -1,38 +0,0 @@ -package net.hostsharing.hsadminng.hs.validation; - -import lombok.Setter; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Map; - -@Setter -public class EnumerationPropertyValidator extends HsPropertyValidator { - - private String[] values; - - private EnumerationPropertyValidator(final String propertyName) { - super(String.class, propertyName); - } - - public static EnumerationPropertyValidator enumerationProperty(final String propertyName) { - return new EnumerationPropertyValidator(propertyName); - } - - public HsPropertyValidator values(final String... values) { - this.values = values; - return this; - } - - @Override - protected void validate(final ArrayList result, final String propertiesName, final String propValue, final Map props) { - if (Arrays.stream(values).noneMatch(v -> v.equals(propValue))) { - result.add("'"+propertiesName+"." + propertyName + "' is expected to be one of " + Arrays.toString(values) + " but is '" + propValue + "'"); - } - } - - @Override - protected String simpleTypeName() { - return "enumeration"; - } -} diff --git a/src/main/java/net/hostsharing/hsadminng/hs/validation/HsEntityValidator.java b/src/main/java/net/hostsharing/hsadminng/hs/validation/HsEntityValidator.java index 43be4d10..c06ed140 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/validation/HsEntityValidator.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/validation/HsEntityValidator.java @@ -1,49 +1,76 @@ package net.hostsharing.hsadminng.hs.validation; -import com.fasterxml.jackson.annotation.JsonAutoDetect; -import com.fasterxml.jackson.annotation.PropertyAccessor; -import com.fasterxml.jackson.databind.ObjectMapper; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.Map; +import java.util.function.Supplier; import static java.util.Arrays.stream; +import static java.util.Collections.emptyList; -public class HsEntityValidator, T extends Enum> { +public abstract class HsEntityValidator { - public final HsPropertyValidator[] propertyValidators; + public final ValidatableProperty[] propertyValidators; - public HsEntityValidator(final HsPropertyValidator... validators) { + public HsEntityValidator(final ValidatableProperty... validators) { propertyValidators = validators; } - public List validate(final E assetEntity) { + protected static List enrich(final String prefix, final List messages) { + return messages.stream() + // TODO:refa: this is a bit hacky, I need to find the right place to add the prefix + .map(message -> message.startsWith("'") ? message : ("'" + prefix + "." + message)) + .toList(); + } + + protected static String prefix(final String... parts) { + return String.join(".", parts); + } + + public abstract List validate(final E entity); + + public final List> properties() { + return Arrays.stream(propertyValidators) + .map(ValidatableProperty::toOrderedMap) + .toList(); + } + + protected ArrayList validateProperties(final Map properties) { final var result = new ArrayList(); - assetEntity.getProperties().keySet().forEach( givenPropName -> { + properties.keySet().forEach( givenPropName -> { if (stream(propertyValidators).map(pv -> pv.propertyName).noneMatch(propName -> propName.equals(givenPropName))) { - result.add("'"+assetEntity.getPropertiesName()+"." + givenPropName + "' is not expected but is set to '" +assetEntity.getProperties().get(givenPropName) + "'"); + result.add(givenPropName + "' is not expected but is set to '" + properties.get(givenPropName) + "'"); } }); stream(propertyValidators).forEach(pv -> { - result.addAll(pv.validate(assetEntity.getPropertiesName(), assetEntity.getProperties())); + result.addAll(pv.validate(properties)); }); return result; } - public List> properties() { - final var mapper = new ObjectMapper(); - mapper.setVisibility(PropertyAccessor.FIELD, JsonAutoDetect.Visibility.ANY); - return Arrays.stream(propertyValidators) - .map(propertyValidator -> propertyValidator.toMap(mapper)) - .map(HsEntityValidator::asKeyValueMap) - .toList(); + @SafeVarargs + protected static List sequentiallyValidate(final Supplier>... validators) { + return new ArrayList<>(stream(validators) + .map(Supplier::get) + .filter(violations -> !violations.isEmpty()) + .findFirst() + .orElse(emptyList())); } - @SuppressWarnings({ "unchecked", "rawtypes" }) - private static Map asKeyValueMap(final Map map) { - return (Map) map; + protected static Integer getNonNullIntegerValue(final ValidatableProperty prop, final Map propValues) { + final var value = prop.getValue(propValues); + if (value instanceof Integer) { + return (Integer) value; + } + throw new IllegalArgumentException(prop.propertyName + " Integer value expected, but got " + value); } + protected static Integer toNonNullInteger(final Object value) { + if (value instanceof Integer) { + return (Integer) value; + } + throw new IllegalArgumentException("Integer value expected, but got " + value); + } } diff --git a/src/main/java/net/hostsharing/hsadminng/hs/validation/HsPropertyValidator.java b/src/main/java/net/hostsharing/hsadminng/hs/validation/HsPropertyValidator.java deleted file mode 100644 index 891c8a7a..00000000 --- a/src/main/java/net/hostsharing/hsadminng/hs/validation/HsPropertyValidator.java +++ /dev/null @@ -1,67 +0,0 @@ -package net.hostsharing.hsadminng.hs.validation; - -import com.fasterxml.jackson.databind.ObjectMapper; -import lombok.RequiredArgsConstructor; - -import java.util.AbstractMap.SimpleImmutableEntry; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; - -@RequiredArgsConstructor -public abstract class HsPropertyValidator { - - final Class type; - final String propertyName; - private Boolean required; - - public static Map.Entry defType(K k, V v) { - return new SimpleImmutableEntry<>(k, v); - } - - public HsPropertyValidator required() { - required = Boolean.TRUE; - return this; - } - - public HsPropertyValidator optional() { - required = Boolean.FALSE; - return this; - } - - public final List validate(final String propertiesName, final Map props) { - final var result = new ArrayList(); - final var propValue = props.get(propertyName); - if (propValue == null) { - if (required) { - result.add("'"+propertiesName+"." + propertyName + "' is required but missing"); - } - } - if (propValue != null){ - if ( type.isInstance(propValue)) { - //noinspection unchecked - validate(result, propertiesName, (T) propValue, props); - } else { - result.add("'"+propertiesName+"." + propertyName + "' is expected to be of type " + type + ", " + - "but is of type '" + propValue.getClass().getSimpleName() + "'"); - } - } - return result; - } - - protected abstract void validate(final ArrayList result, final String propertiesName, final T propValue, final Map props); - - public void verifyConsistency(final Map.Entry, ?> typeDef) { - if (required == null ) { - throw new IllegalStateException(typeDef.getKey() + "[" + propertyName + "] not fully initialized, please call either .required() or .optional()" ); - } - } - - public Map toMap(final ObjectMapper mapper) { - final Map map = mapper.convertValue(this, Map.class); - map.put("type", simpleTypeName()); - return map; - } - - protected abstract String simpleTypeName(); -} diff --git a/src/main/java/net/hostsharing/hsadminng/hs/validation/IntegerProperty.java b/src/main/java/net/hostsharing/hsadminng/hs/validation/IntegerProperty.java new file mode 100644 index 00000000..a1658ff9 --- /dev/null +++ b/src/main/java/net/hostsharing/hsadminng/hs/validation/IntegerProperty.java @@ -0,0 +1,56 @@ +package net.hostsharing.hsadminng.hs.validation; + +import lombok.Setter; +import net.hostsharing.hsadminng.mapper.Array; + +import java.util.ArrayList; +import java.util.Map; + +@Setter +public class IntegerProperty extends ValidatableProperty { + + private final static String[] KEY_ORDER = Array.join( + ValidatableProperty.KEY_ORDER_HEAD, + Array.of("unit", "min", "max", "step"), + ValidatableProperty.KEY_ORDER_TAIL); + + private String unit; + private Integer min; + private Integer max; + private Integer step; + + public static IntegerProperty integerProperty(final String propertyName) { + return new IntegerProperty(propertyName); + } + + private IntegerProperty(final String propertyName) { + super(Integer.class, propertyName, KEY_ORDER); + } + + @Override + public String unit() { + return unit; + } + + public Integer max() { + return max; + } + + @Override + protected void validate(final ArrayList result, final Integer propValue, final Map props) { + if (min != null && propValue < min) { + result.add(propertyName + "' is expected to be >= " + min + " but is " + propValue); + } + if (max != null && propValue > max) { + result.add(propertyName + "' is expected to be <= " + max + " but is " + propValue); + } + if (step != null && propValue % step != 0) { + result.add(propertyName + "' is expected to be multiple of " + step + " but is " + propValue); + } + } + + @Override + protected String simpleTypeName() { + return "integer"; + } +} diff --git a/src/main/java/net/hostsharing/hsadminng/hs/validation/IntegerPropertyValidator.java b/src/main/java/net/hostsharing/hsadminng/hs/validation/IntegerPropertyValidator.java deleted file mode 100644 index d6fb85f5..00000000 --- a/src/main/java/net/hostsharing/hsadminng/hs/validation/IntegerPropertyValidator.java +++ /dev/null @@ -1,42 +0,0 @@ -package net.hostsharing.hsadminng.hs.validation; - -import lombok.Setter; - -import java.util.ArrayList; -import java.util.Map; - -@Setter -public class IntegerPropertyValidator extends HsPropertyValidator { - - private String unit; - private Integer min; - private Integer max; - private Integer step; - - public static IntegerPropertyValidator integerProperty(final String propertyName) { - return new IntegerPropertyValidator(propertyName); - } - - private IntegerPropertyValidator(final String propertyName) { - super(Integer.class, propertyName); - } - - - @Override - protected void validate(final ArrayList result, final String propertiesName, final Integer propValue, final Map props) { - if (min != null && propValue < min) { - result.add("'"+propertiesName+"." + propertyName + "' is expected to be >= " + min + " but is " + propValue); - } - if (max != null && propValue > max) { - result.add("'"+propertiesName+"." + propertyName + "' is expected to be <= " + max + " but is " + propValue); - } - if (step != null && propValue % step != 0) { - result.add("'"+propertiesName+"." + propertyName + "' is expected to be multiple of " + step + " but is " + propValue); - } - } - - @Override - protected String simpleTypeName() { - return "integer"; - } -} diff --git a/src/main/java/net/hostsharing/hsadminng/hs/validation/Validatable.java b/src/main/java/net/hostsharing/hsadminng/hs/validation/Validatable.java deleted file mode 100644 index 6f214b04..00000000 --- a/src/main/java/net/hostsharing/hsadminng/hs/validation/Validatable.java +++ /dev/null @@ -1,13 +0,0 @@ -package net.hostsharing.hsadminng.hs.validation; - - -import java.util.Map; - -public interface Validatable> { - - - Enum getType(); - - String getPropertiesName(); - Map getProperties(); -} diff --git a/src/main/java/net/hostsharing/hsadminng/hs/validation/ValidatableProperty.java b/src/main/java/net/hostsharing/hsadminng/hs/validation/ValidatableProperty.java new file mode 100644 index 00000000..7795d47d --- /dev/null +++ b/src/main/java/net/hostsharing/hsadminng/hs/validation/ValidatableProperty.java @@ -0,0 +1,172 @@ +package net.hostsharing.hsadminng.hs.validation; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import lombok.RequiredArgsConstructor; +import lombok.SneakyThrows; +import net.hostsharing.hsadminng.hs.booking.item.HsBookingItemEntity; +import net.hostsharing.hsadminng.mapper.Array; +import org.apache.commons.lang3.function.TriFunction; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.function.Function; + +import static java.lang.Boolean.FALSE; +import static java.lang.Boolean.TRUE; +import static java.util.Collections.emptyList; + +@RequiredArgsConstructor +public abstract class ValidatableProperty { + + protected static final String[] KEY_ORDER_HEAD = Array.of("propertyName"); + protected static final String[] KEY_ORDER_TAIL = Array.of("required", "defaultValue", "isTotalsValidator", "thresholdPercentage"); + + final Class type; + final String propertyName; + private final String[] keyOrder; + private Boolean required; + private T defaultValue; + private boolean isTotalsValidator = false; + @JsonIgnore + private List>> asTotalLimitValidators; // TODO.impl: move to BookingItemIntegerProperty + + private Integer thresholdPercentage; // TODO.impl: move to IntegerProperty + + public String unit() { + return null; + } + + public ValidatableProperty required() { + required = TRUE; + return this; + } + + public ValidatableProperty optional() { + required = FALSE; + return this; + } + + public ValidatableProperty withDefault(final T value) { + defaultValue = value; + required = FALSE; + return this; + } + + public ValidatableProperty asTotalLimit() { + isTotalsValidator = true; + return this; + } + + public String propertyName() { + return propertyName; + } + + public boolean isTotalsValidator() { + return isTotalsValidator || asTotalLimitValidators != null; + } + + public Integer thresholdPercentage() { + return thresholdPercentage; + } + + public ValidatableProperty eachComprising(final int factor, final TriFunction> validator) { + if (asTotalLimitValidators == null) { + asTotalLimitValidators = new ArrayList<>(); + } + asTotalLimitValidators.add((final HsBookingItemEntity entity) -> validator.apply(entity, (IntegerProperty)this, factor)); + return this; + } + + public ValidatableProperty withThreshold(final Integer percentage) { + this.thresholdPercentage = percentage; + return this; + } + + public final List validate(final Map props) { + final var result = new ArrayList(); + final var propValue = props.get(propertyName); + if (propValue == null) { + if (required) { + result.add(propertyName + "' is required but missing"); + } + } + if (propValue != null){ + if ( type.isInstance(propValue)) { + //noinspection unchecked + validate(result, (T) propValue, props); + } else { + result.add(propertyName + "' is expected to be of type " + type + ", " + + "but is of type '" + propValue.getClass().getSimpleName() + "'"); + } + } + return result; + } + + protected abstract void validate(final ArrayList result, final T propValue, final Map props); + + public void verifyConsistency(final Map.Entry, ?> typeDef) { + if (required == null ) { + throw new IllegalStateException(typeDef.getKey() + "[" + propertyName + "] not fully initialized, please call either .required() or .optional()" ); + } + } + + @SuppressWarnings("unchecked") + public T getValue(final Map propValues) { + return (T) Optional.ofNullable(propValues.get(propertyName)).orElse(defaultValue); + } + + protected abstract String simpleTypeName(); + + public Map toOrderedMap() { + Map sortedMap = new LinkedHashMap<>(); + sortedMap.put("type", simpleTypeName()); + + // Add entries according to the given order + for (String key : keyOrder) { + final Optional propValue = getPropertyValue(key); + propValue.ifPresent(o -> sortedMap.put(key, o)); + } + + return sortedMap; + } + + @SneakyThrows + private Optional getPropertyValue(final String key) { + try { + final var field = getClass().getDeclaredField(key); + field.setAccessible(true); + return Optional.ofNullable(arrayToList(field.get(this))); + } catch (final NoSuchFieldException e1) { + try { + final var field = getClass().getSuperclass().getDeclaredField(key); + field.setAccessible(true); + return Optional.ofNullable(arrayToList(field.get(this))); + } catch (final NoSuchFieldException e2) { + return Optional.empty(); + } + } + } + + private Object arrayToList(final Object value) { + if ( value instanceof String[]) { + return List.of((String[])value); + } + return value; + } + + public List validateTotals(final HsBookingItemEntity bookingItem) { + if (asTotalLimitValidators==null) { + return emptyList(); + } + return asTotalLimitValidators.stream() + .map(v -> v.apply(bookingItem)) + .filter(Objects::nonNull) + .flatMap(Collection::stream) + .toList(); + } +} diff --git a/src/test/java/net/hostsharing/hsadminng/rbac/test/Array.java b/src/main/java/net/hostsharing/hsadminng/mapper/Array.java similarity index 83% rename from src/test/java/net/hostsharing/hsadminng/rbac/test/Array.java rename to src/main/java/net/hostsharing/hsadminng/mapper/Array.java index c51a69bb..39588f11 100644 --- a/src/test/java/net/hostsharing/hsadminng/rbac/test/Array.java +++ b/src/main/java/net/hostsharing/hsadminng/mapper/Array.java @@ -1,4 +1,4 @@ -package net.hostsharing.hsadminng.rbac.test; +package net.hostsharing.hsadminng.mapper; import java.util.ArrayList; import java.util.Arrays; @@ -37,4 +37,10 @@ public class Array { return resultList.toArray(String[]::new); } + public static String[] join(final String[]... parts) { + final String[] joined = Arrays.stream(parts) + .flatMap(Arrays::stream) + .toArray(String[]::new); + return joined; + } } diff --git a/src/main/java/net/hostsharing/hsadminng/rbac/rbacgrant/RbacGrantsDiagramService.java b/src/main/java/net/hostsharing/hsadminng/rbac/rbacgrant/RbacGrantsDiagramService.java index 2290c948..fd33f358 100644 --- a/src/main/java/net/hostsharing/hsadminng/rbac/rbacgrant/RbacGrantsDiagramService.java +++ b/src/main/java/net/hostsharing/hsadminng/rbac/rbacgrant/RbacGrantsDiagramService.java @@ -62,6 +62,8 @@ public class RbacGrantsDiagramService { @PersistenceContext private EntityManager em; + private Map> descendantsByUuid = new HashMap<>(); + public String allGrantsToCurrentUser(final EnumSet includes) { final var graph = new LimitedHashSet(); for ( UUID subjectUuid: context.currentSubjectsUuids() ) { @@ -102,7 +104,7 @@ public class RbacGrantsDiagramService { } private void traverseGrantsFrom(final Set graph, final UUID refUuid, final EnumSet option) { - final var grants = rawGrantRepo.findByDescendantUuid(refUuid); + final var grants = findDescendantsByUuid(refUuid); grants.forEach(g -> { if (!option.contains(USERS) && g.getAscendantIdName().startsWith("user:")) { return; @@ -114,6 +116,11 @@ public class RbacGrantsDiagramService { }); } + private List findDescendantsByUuid(final UUID refUuid) { + // TODO.impl: if that UUID already got processed, do we need to return anything at all? + return descendantsByUuid.computeIfAbsent(refUuid, uuid -> rawGrantRepo.findByDescendantUuid(uuid)); + } + private String toMermaidFlowchart(final HashSet graph, final EnumSet includes) { final var entities = includes.contains(DETAILS) diff --git a/src/main/resources/db/changelog/6-hs-booking/630-booking-item/6208-hs-booking-item-test-data.sql b/src/main/resources/db/changelog/6-hs-booking/630-booking-item/6208-hs-booking-item-test-data.sql index bc3a9e51..3f007ab8 100644 --- a/src/main/resources/db/changelog/6-hs-booking/630-booking-item/6208-hs-booking-item-test-data.sql +++ b/src/main/resources/db/changelog/6-hs-booking/630-booking-item/6208-hs-booking-item-test-data.sql @@ -33,13 +33,13 @@ begin managedServerUuid := uuid_generate_v4(); insert into hs_booking_item (uuid, projectuuid, type, parentitemuuid, caption, validity, resources) - values (privateCloudUuid, relatedProject.uuid, 'PRIVATE_CLOUD', null, 'some PrivateCloud', daterange('20240401', null, '[]'), '{ "CPUs": 10, "SDD": 10240, "HDD": 10240, "Traffic": 42 }'::jsonb), - (uuid_generate_v4(), null, 'MANAGED_SERVER', privateCloudUuid, 'some ManagedServer', daterange('20230115', '20240415', '[)'), '{ "CPUs": 2, "RAM": 4, "HDD": 1024, "Traffic": 42 }'::jsonb), - (uuid_generate_v4(), null, 'CLOUD_SERVER', privateCloudUuid, 'test CloudServer', daterange('20230115', '20240415', '[)'), '{ "CPUs": 2, "RAM": 4, "HDD": 1024, "Traffic": 42 }'::jsonb), - (uuid_generate_v4(), null, 'CLOUD_SERVER', privateCloudUuid, 'prod CloudServer', daterange('20230115', '20240415', '[)'), '{ "CPUs": 4, "RAM": 16, "HDD": 2924, "Traffic": 420 }'::jsonb), - (managedServerUuid, relatedProject.uuid, 'MANAGED_SERVER', null, 'separate ManagedServer', daterange('20221001', null, '[]'), '{ "CPUs": 2, "RAM": 8, "SDD": 512, "Traffic": 42 }'::jsonb), - (uuid_generate_v4(), null, 'MANAGED_WEBSPACE', managedServerUuid, 'some ManagedWebspace', daterange('20221001', null, '[]'), '{ "SDD": 512, "Traffic": 12, "Daemons": 2, "Multi": 4 }'::jsonb), - (uuid_generate_v4(), relatedProject.uuid, 'MANAGED_WEBSPACE', null, 'some ManagedWebspace', daterange('20221001', null, '[]'), '{ "SDD": 512, "Traffic": 12, "Daemons": 2, "Multi": 4 }'::jsonb); + values (privateCloudUuid, relatedProject.uuid, 'PRIVATE_CLOUD', null, 'some PrivateCloud', daterange('20240401', null, '[]'), '{ "CPUs": 10, "RAM": 32, "SSD": 4000, "HDD": 10000, "Traffic": 2000 }'::jsonb), + (uuid_generate_v4(), null, 'MANAGED_SERVER', privateCloudUuid, 'some ManagedServer', daterange('20230115', '20240415', '[)'), '{ "CPUs": 2, "RAM": 4, "SSD": 500, "Traffic": 500 }'::jsonb), + (uuid_generate_v4(), null, 'CLOUD_SERVER', privateCloudUuid, 'test CloudServer', daterange('20230115', '20240415', '[)'), '{ "CPUs": 2, "RAM": 4, "SSD": 750, "Traffic": 500 }'::jsonb), + (uuid_generate_v4(), null, 'CLOUD_SERVER', privateCloudUuid, 'prod CloudServer', daterange('20230115', '20240415', '[)'), '{ "CPUs": 4, "RAM": 16, "SSD": 1000, "Traffic": 500 }'::jsonb), + (managedServerUuid, relatedProject.uuid, 'MANAGED_SERVER', null, 'separate ManagedServer', daterange('20221001', null, '[]'), '{ "CPUs": 2, "RAM": 8, "SSD": 500, "Traffic": 500 }'::jsonb), + (uuid_generate_v4(), null, 'MANAGED_WEBSPACE', managedServerUuid, 'some ManagedWebspace', daterange('20221001', null, '[]'), '{ "SSD": 50, "Traffic": 20, "Daemons": 2, "Multi": 4 }'::jsonb), + (uuid_generate_v4(), relatedProject.uuid, 'MANAGED_WEBSPACE', null, 'separate ManagedWebspace', daterange('20221001', null, '[]'), '{ "SSD": 100, "Traffic": 50, "Daemons": 0, "Multi": 1 }'::jsonb); end; $$; --// diff --git a/src/main/resources/db/changelog/7-hs-hosting/701-hosting-asset/7010-hs-hosting-asset.sql b/src/main/resources/db/changelog/7-hs-hosting/701-hosting-asset/7010-hs-hosting-asset.sql index c6fedb72..7e96a3fd 100644 --- a/src/main/resources/db/changelog/7-hs-hosting/701-hosting-asset/7010-hs-hosting-asset.sql +++ b/src/main/resources/db/changelog/7-hs-hosting/701-hosting-asset/7010-hs-hosting-asset.sql @@ -75,10 +75,10 @@ begin end); if expectedParentType is not null and actualParentType is null then - raise exception '[400] % must have % as parent, but got ', + raise exception '[400] HostingAsset % must have % as parent, but got ', NEW.type, expectedParentType; elsif expectedParentType is not null and actualParentType <> expectedParentType then - raise exception '[400] % must have % as parent, but got %s', + raise exception '[400] HostingAsset % must have % as parent, but got %s', NEW.type, expectedParentType, actualParentType; end if; return NEW; @@ -100,27 +100,23 @@ create or replace function hs_hosting_asset_booking_item_hierarchy_check_tf() language plpgsql as $$ declare actualBookingItemType HsBookingItemType; - expectedBookingItemTypes HsBookingItemType[]; + expectedBookingItemType HsBookingItemType; begin actualBookingItemType := (select type from hs_booking_item where NEW.bookingItemUuid = uuid); if NEW.type = 'CLOUD_SERVER' then - expectedBookingItemTypes := ARRAY['PRIVATE_CLOUD', 'CLOUD_SERVER']; + expectedBookingItemType := 'CLOUD_SERVER'; elsif NEW.type = 'MANAGED_SERVER' then - expectedBookingItemTypes := ARRAY['PRIVATE_CLOUD', 'MANAGED_SERVER']; + expectedBookingItemType := 'MANAGED_SERVER'; elsif NEW.type = 'MANAGED_WEBSPACE' then - if NEW.parentAssetUuid is null then - expectedBookingItemTypes := ARRAY['MANAGED_WEBSPACE']; - else - expectedBookingItemTypes := ARRAY['PRIVATE_CLOUD', 'MANAGED_SERVER']; - end if; + expectedBookingItemType := 'MANAGED_WEBSPACE'; end if; - if not actualBookingItemType = any(expectedBookingItemTypes) then - raise exception '[400] % % must have any of % as booking-item, but got %', - NEW.type, NEW.identifier, expectedBookingItemTypes, actualBookingItemType; + if not actualBookingItemType = expectedBookingItemType then + raise exception '[400] HostingAsset % % must have % as booking-item, but got %', + NEW.type, NEW.identifier, expectedBookingItemType, actualBookingItemType; end if; return NEW; end; $$; diff --git a/src/main/resources/db/changelog/7-hs-hosting/701-hosting-asset/7018-hs-hosting-asset-test-data.sql b/src/main/resources/db/changelog/7-hs-hosting/701-hosting-asset/7018-hs-hosting-asset-test-data.sql index 964acdec..c82bd768 100644 --- a/src/main/resources/db/changelog/7-hs-hosting/701-hosting-asset/7018-hs-hosting-asset-test-data.sql +++ b/src/main/resources/db/changelog/7-hs-hosting/701-hosting-asset/7018-hs-hosting-asset-test-data.sql @@ -11,16 +11,18 @@ create or replace procedure createHsHostingAssetTestData(givenProjectCaption varchar) language plpgsql as $$ declare - currentTask varchar; - relatedProject hs_booking_project; - relatedDebitor hs_office_debitor; - relatedPrivateCloudBookingItem hs_booking_item; - relatedManagedServerBookingItem hs_booking_item; - debitorNumberSuffix varchar; - defaultPrefix varchar; - managedServerUuid uuid; - managedWebspaceUuid uuid; - webUnixUserUuid uuid; + currentTask varchar; + relatedProject hs_booking_project; + relatedDebitor hs_office_debitor; + relatedPrivateCloudBookingItem hs_booking_item; + relatedManagedServerBookingItem hs_booking_item; + relatedCloudServerBookingItem hs_booking_item; + relatedManagedWebspaceBookingItem hs_booking_item; + debitorNumberSuffix varchar; + defaultPrefix varchar; + managedServerUuid uuid; + managedWebspaceUuid uuid; + webUnixUserUuid uuid; begin currentTask := 'creating hosting-asset test-data ' || givenProjectCaption; call defineContext(currentTask, null, 'superuser-alex@hostsharing.net', 'global#global:ADMIN'); @@ -38,7 +40,7 @@ begin select item.* into relatedPrivateCloudBookingItem from hs_booking_item item - where item.projectUuid = relatedProject.uuid + where item.projectUuid = relatedProject.uuid and item.type = 'PRIVATE_CLOUD'; assert relatedPrivateCloudBookingItem.uuid is not null, 'relatedPrivateCloudBookingItem for "' || givenProjectCaption|| '" must not be null'; @@ -48,6 +50,18 @@ begin and item.type = 'MANAGED_SERVER'; assert relatedManagedServerBookingItem.uuid is not null, 'relatedManagedServerBookingItem for "' || givenProjectCaption|| '" must not be null'; + select item.* into relatedCloudServerBookingItem + from hs_booking_item item + where item.parentItemuuid = relatedPrivateCloudBookingItem.uuid + and item.type = 'CLOUD_SERVER'; + assert relatedCloudServerBookingItem.uuid is not null, 'relatedCloudServerBookingItem for "' || givenProjectCaption|| '" must not be null'; + + select item.* into relatedManagedWebspaceBookingItem + from hs_booking_item item + where item.projectUuid = relatedProject.uuid + and item.type = 'MANAGED_WEBSPACE'; + assert relatedManagedWebspaceBookingItem.uuid is not null, 'relatedManagedWebspaceBookingItem for "' || givenProjectCaption|| '" must not be null'; + select uuid_generate_v4() into managedServerUuid; select uuid_generate_v4() into managedWebspaceUuid; select uuid_generate_v4() into webUnixUserUuid; @@ -55,12 +69,12 @@ begin defaultPrefix := relatedDebitor.defaultPrefix; insert into hs_hosting_asset - (uuid, bookingitemuuid, type, parentAssetUuid, assignedToAssetUuid, identifier, caption, config) - values (managedServerUuid, relatedPrivateCloudBookingItem.uuid, 'MANAGED_SERVER', null, null, 'vm10' || debitorNumberSuffix, 'some ManagedServer', '{ "extra": 42 }'::jsonb), - (uuid_generate_v4(), relatedPrivateCloudBookingItem.uuid, 'CLOUD_SERVER', null, null, 'vm20' || debitorNumberSuffix, 'another CloudServer', '{ "extra": 42 }'::jsonb), - (managedWebspaceUuid, relatedManagedServerBookingItem.uuid, 'MANAGED_WEBSPACE', managedServerUuid, null, defaultPrefix || '01', 'some Webspace', '{ "extra": 42 }'::jsonb), - (webUnixUserUuid, null, 'UNIX_USER', managedWebspaceUuid, null, defaultPrefix || '01-web', 'some UnixUser for Website', '{ "SSD-soft-quota": "128", "SSD-hard-quota": "256", "HDD-soft-quota": "512", "HDD-hard-quota": "1024", "extra": 42 }'::jsonb), - (uuid_generate_v4(), null, 'DOMAIN_HTTP_SETUP', managedWebspaceUuid, webUnixUserUuid, defaultPrefix || '.example.org', 'some Domain-HTTP-Setup', '{ "option-htdocsfallback": true, "use-fcgiphpbin": "/usr/lib/cgi-bin/php", "validsubdomainnames": "*", "extra": 42 }'::jsonb); + (uuid, bookingitemuuid, type, parentAssetUuid, assignedToAssetUuid, identifier, caption, config) + values (managedServerUuid, relatedManagedServerBookingItem.uuid, 'MANAGED_SERVER', null, null, 'vm10' || debitorNumberSuffix, 'some ManagedServer', '{ "monit_max_cpu_usage": 90, "monit_max_ram_usage": 80, "monit_max_ssd_usage": 70 }'::jsonb), + (uuid_generate_v4(), relatedCloudServerBookingItem.uuid, 'CLOUD_SERVER', null, null, 'vm20' || debitorNumberSuffix, 'another CloudServer', '{}'::jsonb), + (managedWebspaceUuid, relatedManagedWebspaceBookingItem.uuid, 'MANAGED_WEBSPACE', managedServerUuid, null, defaultPrefix || '01', 'some Webspace', '{}'::jsonb), + (webUnixUserUuid, null, 'UNIX_USER', managedWebspaceUuid, null, defaultPrefix || '01-web', 'some UnixUser for Website', '{ "SSD-soft-quota": "128", "SSD-hard-quota": "256", "HDD-soft-quota": "512", "HDD-hard-quota": "1024"}'::jsonb), + (uuid_generate_v4(), null, 'DOMAIN_HTTP_SETUP', managedWebspaceUuid, webUnixUserUuid, defaultPrefix || '.example.org', 'some Domain-HTTP-Setup', '{ "option-htdocsfallback": true, "use-fcgiphpbin": "/usr/lib/cgi-bin/php", "validsubdomainnames": "*"}'::jsonb); end; $$; --// diff --git a/src/test/java/net/hostsharing/hsadminng/arch/ArchitectureTest.java b/src/test/java/net/hostsharing/hsadminng/arch/ArchitectureTest.java index 2c2f9f3d..df26279d 100644 --- a/src/test/java/net/hostsharing/hsadminng/arch/ArchitectureTest.java +++ b/src/test/java/net/hostsharing/hsadminng/arch/ArchitectureTest.java @@ -8,7 +8,10 @@ import com.tngtech.archunit.lang.ArchRule; import com.tngtech.archunit.lang.ConditionEvents; import com.tngtech.archunit.lang.SimpleConditionEvent; import net.hostsharing.hsadminng.HsadminNgApplication; +import net.hostsharing.hsadminng.hs.booking.item.HsBookingItemEntity; +import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetEntity; import net.hostsharing.hsadminng.rbac.context.ContextBasedTest; +import net.hostsharing.hsadminng.rbac.rbacgrant.RbacGrantsDiagramService; import net.hostsharing.hsadminng.rbac.rbacobject.RbacObject; import org.springframework.data.repository.Repository; import org.springframework.web.bind.annotation.RestController; @@ -51,6 +54,7 @@ public class ArchitectureTest { "..hs.office.person", "..hs.office.relation", "..hs.office.sepamandate", + "..hs.booking.debitor", "..hs.booking.project", "..hs.booking.item", "..hs.booking.item.validators", @@ -155,7 +159,8 @@ public class ArchitectureTest { .that().resideInAPackage("..hs.hosting.(*)..") .should().onlyBeAccessed().byClassesThat() .resideInAnyPackage( - "..hs.hosting.(*).." + "..hs.hosting.(*)..", + "..hs.booking.(*).." // TODO.impl: fix this cyclic dependency ); @ArchTest @@ -295,9 +300,13 @@ public class ArchitectureTest { static final ArchRule everythingShouldBeFreeOfCycles = slices().matching("net.hostsharing.hsadminng.(*)..") .should().beFreeOfCycles() + // TODO.refa: would be great if we could get rid of these cyclic dependencies .ignoreDependency( ContextBasedTest.class, - net.hostsharing.hsadminng.rbac.rbacgrant.RbacGrantsDiagramService.class); + RbacGrantsDiagramService.class) + .ignoreDependency( + HsBookingItemEntity.class, + HsHostingAssetEntity.class); @ArchTest diff --git a/src/test/java/net/hostsharing/hsadminng/errors/RestResponseEntityExceptionHandlerUnitTest.java b/src/test/java/net/hostsharing/hsadminng/errors/RestResponseEntityExceptionHandlerUnitTest.java index ad3cdfa0..9b25fed4 100644 --- a/src/test/java/net/hostsharing/hsadminng/errors/RestResponseEntityExceptionHandlerUnitTest.java +++ b/src/test/java/net/hostsharing/hsadminng/errors/RestResponseEntityExceptionHandlerUnitTest.java @@ -187,7 +187,7 @@ class RestResponseEntityExceptionHandlerUnitTest { final var givenWebRequest = mock(WebRequest.class); // when - final var errorResponse = exceptionHandler.handleIbanAndBicExceptions(givenException, givenWebRequest); + final var errorResponse = exceptionHandler.handleValidationExceptions(givenException, givenWebRequest); // then assertThat(errorResponse.getBody().getStatusCode()).isEqualTo(400); diff --git a/src/test/java/net/hostsharing/hsadminng/hs/booking/debitor/HsBookingDebitorEntityTest.java b/src/test/java/net/hostsharing/hsadminng/hs/booking/debitor/HsBookingDebitorEntityUnitTest.java similarity index 95% rename from src/test/java/net/hostsharing/hsadminng/hs/booking/debitor/HsBookingDebitorEntityTest.java rename to src/test/java/net/hostsharing/hsadminng/hs/booking/debitor/HsBookingDebitorEntityUnitTest.java index 4275c56c..154e2b89 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/booking/debitor/HsBookingDebitorEntityTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/booking/debitor/HsBookingDebitorEntityUnitTest.java @@ -4,7 +4,7 @@ import org.junit.jupiter.api.Test; import static org.assertj.core.api.Assertions.assertThat; -class HsBookingDebitorEntityTest { +class HsBookingDebitorEntityUnitTest { @Test void toStringContainsDebitorNumberAndDefaultPrefix() { 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 a0054b4f..2804a758 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 @@ -77,14 +77,14 @@ class HsBookingItemControllerAcceptanceTest extends ContextBasedTestWithCleanup [ { "type": "MANAGED_WEBSPACE", - "caption": "some ManagedWebspace", + "caption": "separate ManagedWebspace", "validFrom": "2022-10-01", "validTo": null, "resources": { - "SDD": 512, - "Multi": 4, - "Daemons": 2, - "Traffic": 12 + "SSD": 100, + "Multi": 1, + "Daemons": 0, + "Traffic": 50 } }, { @@ -94,9 +94,9 @@ class HsBookingItemControllerAcceptanceTest extends ContextBasedTestWithCleanup "validTo": null, "resources": { "RAM": 8, - "SDD": 512, + "SSD": 500, "CPUs": 2, - "Traffic": 42 + "Traffic": 500 } }, { @@ -105,10 +105,11 @@ class HsBookingItemControllerAcceptanceTest extends ContextBasedTestWithCleanup "validFrom": "2024-04-01", "validTo": null, "resources": { - "HDD": 10240, - "SDD": 10240, + "HDD": 10000, + "RAM": 32, + "SSD": 4000, "CPUs": 10, - "Traffic": 42 + "Traffic": 2000 } } ] @@ -174,7 +175,7 @@ class HsBookingItemControllerAcceptanceTest extends ContextBasedTestWithCleanup @Test void globalAdmin_canGetArbitraryBookingItem() { context.define("superuser-alex@hostsharing.net"); - final var givenBookingItemUuid = bookingItemRepo.findByCaption("some ManagedWebspace").stream() + final var givenBookingItemUuid = bookingItemRepo.findByCaption("separate ManagedWebspace").stream() .filter(bi -> belongsToDebitorWithDefaultPrefix(bi, "fir")) .map(HsBookingItemEntity::getUuid) .findAny().orElseThrow(); @@ -191,14 +192,14 @@ class HsBookingItemControllerAcceptanceTest extends ContextBasedTestWithCleanup .body("", lenientlyEquals(""" { "type": "MANAGED_WEBSPACE", - "caption": "some ManagedWebspace", + "caption": "separate ManagedWebspace", "validFrom": "2022-10-01", "validTo": null, "resources": { - "SDD": 512, - "Multi": 4, - "Daemons": 2, - "Traffic": 12 + "SSD": 100, + "Multi": 1, + "Daemons": 0, + "Traffic": 50 } } """)); // @formatter:on @@ -227,14 +228,16 @@ class HsBookingItemControllerAcceptanceTest extends ContextBasedTestWithCleanup void projectAdmin_canGetRelatedBookingItem() { context.define("superuser-alex@hostsharing.net"); final var givenBookingItemUuid = bookingItemRepo.findByCaption("separate ManagedServer").stream() - .filter(bi -> belongsToDebitorWithDefaultPrefix(bi, "thi")) + .filter(bi -> belongsToDebitorWithDefaultPrefix(bi, "sec")) .map(HsBookingItemEntity::getUuid) .findAny().orElseThrow(); + generateRbacDiagramForObjectPermission(givenBookingItemUuid, "SELECT", "select"); + RestAssured // @formatter:off .given() .header("current-user", "superuser-alex@hostsharing.net") - .header("assumed-roles", "hs_booking_project#D-1000313-D-1000313defaultproject:ADMIN") + .header("assumed-roles", "hs_booking_project#D-1000212-D-1000212defaultproject:ADMIN") .port(port) .when() .get("http://localhost/api/hs/booking/items/" + givenBookingItemUuid) @@ -249,9 +252,9 @@ class HsBookingItemControllerAcceptanceTest extends ContextBasedTestWithCleanup "validTo": null, "resources": { "RAM": 8, - "SDD": 512, + "SSD": 500, "CPUs": 2, - "Traffic": 42 + "Traffic": 500 } } """)); // @formatter:on @@ -261,7 +264,7 @@ class HsBookingItemControllerAcceptanceTest extends ContextBasedTestWithCleanup return ofNullable(bi) .map(HsBookingItemEntity::getProject) .map(HsBookingProjectEntity::getDebitor) - .map(bd -> bd.getDefaultPrefix().equals(defaultPrefix)) + .filter(bd -> bd.getDefaultPrefix().equals(defaultPrefix)) .isPresent(); } } diff --git a/src/test/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemEntityPatcherUnitTest.java b/src/test/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemEntityPatcherUnitTest.java index 7e312fbc..ca179fc3 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemEntityPatcherUnitTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemEntityPatcherUnitTest.java @@ -44,11 +44,11 @@ class HsBookingItemEntityPatcherUnitTest extends PatchUnitTestBase< private static final Map PATCH_RESOURCES = patchMap( entry("CPU", 2), entry("HDD", null), - entry("SDD", 256) + entry("SSD", 256) ); private static final Map PATCHED_RESOURCES = patchMap( entry("CPU", 2), - entry("SDD", 256), + entry("SSD", 256), entry("MEM", 64) ); diff --git a/src/test/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemRepositoryIntegrationTest.java b/src/test/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemRepositoryIntegrationTest.java index 0d1e22ac..028971ee 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemRepositoryIntegrationTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemRepositoryIntegrationTest.java @@ -6,7 +6,7 @@ import net.hostsharing.hsadminng.hs.booking.project.HsBookingProjectRepository; import net.hostsharing.hsadminng.hs.office.debitor.HsOfficeDebitorRepository; import net.hostsharing.hsadminng.rbac.rbacgrant.RawRbacGrantRepository; import net.hostsharing.hsadminng.rbac.rbacrole.RawRbacRoleRepository; -import net.hostsharing.hsadminng.rbac.test.Array; +import net.hostsharing.hsadminng.mapper.Array; import net.hostsharing.hsadminng.rbac.test.ContextBasedTestWithCleanup; import net.hostsharing.hsadminng.rbac.test.JpaAttempt; import org.junit.jupiter.api.Nested; @@ -30,7 +30,7 @@ import static net.hostsharing.hsadminng.hs.booking.item.HsBookingItemType.MANAGE import static net.hostsharing.hsadminng.hs.booking.item.HsBookingItemType.MANAGED_WEBSPACE; import static net.hostsharing.hsadminng.rbac.rbacgrant.RawRbacGrantEntity.distinctGrantDisplaysOf; import static net.hostsharing.hsadminng.rbac.rbacrole.RawRbacRoleEntity.distinctRoleNamesOf; -import static net.hostsharing.hsadminng.rbac.test.Array.fromFormatted; +import static net.hostsharing.hsadminng.mapper.Array.fromFormatted; import static net.hostsharing.hsadminng.rbac.test.JpaAttempt.attempt; import static org.assertj.core.api.Assertions.assertThat; @@ -174,9 +174,9 @@ class HsBookingItemRepositoryIntegrationTest extends ContextBasedTestWithCleanup // then allTheseBookingItemsAreReturned( result, - "HsBookingItemEntity(D-1000212:D-1000212 default project, MANAGED_WEBSPACE, [2022-10-01,), some ManagedWebspace, { Daemons: 2, Multi: 4, SDD: 512, Traffic: 12 })", - "HsBookingItemEntity(D-1000212:D-1000212 default project, MANAGED_SERVER, [2022-10-01,), separate ManagedServer, { CPUs: 2, RAM: 8, SDD: 512, Traffic: 42 })", - "HsBookingItemEntity(D-1000212:D-1000212 default project, PRIVATE_CLOUD, [2024-04-01,), some PrivateCloud, { CPUs: 10, HDD: 10240, SDD: 10240, Traffic: 42 })"); + "HsBookingItemEntity(D-1000212:D-1000212 default project, MANAGED_WEBSPACE, [2022-10-01,), separate ManagedWebspace, { Daemons: 0, Multi: 1, SSD: 100, Traffic: 50 })", + "HsBookingItemEntity(D-1000212:D-1000212 default project, MANAGED_SERVER, [2022-10-01,), separate ManagedServer, { CPUs: 2, RAM: 8, SSD: 500, Traffic: 500 })", + "HsBookingItemEntity(D-1000212:D-1000212 default project, PRIVATE_CLOUD, [2024-04-01,), some PrivateCloud, { CPUs: 10, HDD: 10000, RAM: 32, SSD: 4000, Traffic: 2000 })"); } @Test @@ -194,9 +194,9 @@ class HsBookingItemRepositoryIntegrationTest extends ContextBasedTestWithCleanup // then: exactlyTheseBookingItemsAreReturned( result, - "HsBookingItemEntity(D-1000111:D-1000111 default project, MANAGED_SERVER, [2022-10-01,), separate ManagedServer, { CPUs: 2, RAM: 8, SDD: 512, Traffic: 42 })", - "HsBookingItemEntity(D-1000111:D-1000111 default project, MANAGED_WEBSPACE, [2022-10-01,), some ManagedWebspace, { Daemons: 2, Multi: 4, SDD: 512, Traffic: 12 })", - "HsBookingItemEntity(D-1000111:D-1000111 default project, PRIVATE_CLOUD, [2024-04-01,), some PrivateCloud, { CPUs: 10, HDD: 10240, SDD: 10240, Traffic: 42 })"); + "HsBookingItemEntity(D-1000111:D-1000111 default project, MANAGED_SERVER, [2022-10-01,), separate ManagedServer, { CPUs: 2, RAM: 8, SSD: 500, Traffic: 500 })", + "HsBookingItemEntity(D-1000111:D-1000111 default project, MANAGED_WEBSPACE, [2022-10-01,), separate ManagedWebspace, { Daemons: 0, Multi: 1, SSD: 100, Traffic: 50 })", + "HsBookingItemEntity(D-1000111:D-1000111 default project, PRIVATE_CLOUD, [2024-04-01,), some PrivateCloud, { CPUs: 10, HDD: 10000, RAM: 32, SSD: 4000, Traffic: 2000 })"); } } diff --git a/src/test/java/net/hostsharing/hsadminng/hs/booking/item/validators/HsBookingItemEntityValidatorUnitTest.java b/src/test/java/net/hostsharing/hsadminng/hs/booking/item/validators/HsBookingItemEntityValidatorUnitTest.java new file mode 100644 index 00000000..e784edec --- /dev/null +++ b/src/test/java/net/hostsharing/hsadminng/hs/booking/item/validators/HsBookingItemEntityValidatorUnitTest.java @@ -0,0 +1,55 @@ +package net.hostsharing.hsadminng.hs.booking.item.validators; + +import net.hostsharing.hsadminng.hs.booking.debitor.HsBookingDebitorEntity; +import net.hostsharing.hsadminng.hs.booking.item.HsBookingItemEntity; +import net.hostsharing.hsadminng.hs.booking.project.HsBookingProjectEntity; +import org.junit.jupiter.api.Test; + +import jakarta.validation.ValidationException; + +import static net.hostsharing.hsadminng.hs.booking.item.HsBookingItemType.PRIVATE_CLOUD; +import static net.hostsharing.hsadminng.hs.booking.item.HsBookingItemType.CLOUD_SERVER; +import static net.hostsharing.hsadminng.hs.booking.item.HsBookingItemType.MANAGED_SERVER; +import static net.hostsharing.hsadminng.hs.booking.item.HsBookingItemType.MANAGED_WEBSPACE; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.catchThrowable; + +class HsBookingItemEntityValidatorUnitTest { + final HsBookingDebitorEntity debitor = HsBookingDebitorEntity.builder() + .debitorNumber(12345) + .build(); + final HsBookingProjectEntity project = HsBookingProjectEntity.builder() + .debitor(debitor) + .caption("test project") + .build(); + + @Test + void validThrowsException() { + // given + final var cloudServerBookingItemEntity = HsBookingItemEntity.builder() + .type(CLOUD_SERVER) + .project(project) + .caption("Test-Server") + .build(); + + // when + final var result = catchThrowable( ()-> HsBookingItemEntityValidatorRegistry.validated(cloudServerBookingItemEntity)); + + // then + assertThat(result).isInstanceOf(ValidationException.class) + .hasMessageContaining( + "'D-12345:test project:Test-Server.resources.CPUs' is required but missing", + "'D-12345:test project:Test-Server.resources.RAM' is required but missing", + "'D-12345:test project:Test-Server.resources.SSD' is required but missing", + "'D-12345:test project:Test-Server.resources.Traffic' is required but missing"); + } + + @Test + void listsTypes() { + // when + final var result = HsBookingItemEntityValidatorRegistry.types(); + + // then + assertThat(result).containsExactlyInAnyOrder(PRIVATE_CLOUD, CLOUD_SERVER, MANAGED_SERVER, MANAGED_WEBSPACE); + } +} diff --git a/src/test/java/net/hostsharing/hsadminng/hs/booking/item/validators/HsBookingItemEntityValidatorsUnitTest.java b/src/test/java/net/hostsharing/hsadminng/hs/booking/item/validators/HsBookingItemEntityValidatorsUnitTest.java deleted file mode 100644 index 741d7c1e..00000000 --- a/src/test/java/net/hostsharing/hsadminng/hs/booking/item/validators/HsBookingItemEntityValidatorsUnitTest.java +++ /dev/null @@ -1,44 +0,0 @@ -package net.hostsharing.hsadminng.hs.booking.item.validators; - -import net.hostsharing.hsadminng.hs.booking.item.HsBookingItemEntity; -import org.junit.jupiter.api.Test; - -import jakarta.validation.ValidationException; - -import static net.hostsharing.hsadminng.hs.booking.item.HsBookingItemType.MANAGED_SERVER; -import static net.hostsharing.hsadminng.hs.booking.item.HsBookingItemType.MANAGED_WEBSPACE; -import static net.hostsharing.hsadminng.hs.booking.item.validators.HsBookingItemEntityValidators.valid; -import static net.hostsharing.hsadminng.hs.booking.item.HsBookingItemType.CLOUD_SERVER; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.catchThrowable; - -class HsBookingItemEntityValidatorsUnitTest { - - @Test - void validThrowsException() { - // given - final var cloudServerBookingItemEntity = HsBookingItemEntity.builder() - .type(CLOUD_SERVER) - .build(); - - // when - final var result = catchThrowable( ()-> valid(cloudServerBookingItemEntity) ); - - // then - assertThat(result).isInstanceOf(ValidationException.class) - .hasMessageContaining( - "'resources.CPUs' is required but missing", - "'resources.RAM' is required but missing", - "'resources.SSD' is required but missing", - "'resources.Traffic' is required but missing"); - } - - @Test - void listsTypes() { - // when - final var result = HsBookingItemEntityValidators.types(); - - // then - assertThat(result).containsExactlyInAnyOrder(CLOUD_SERVER, MANAGED_SERVER, MANAGED_WEBSPACE); - } -} diff --git a/src/test/java/net/hostsharing/hsadminng/hs/booking/item/validators/HsCloudServerBookingItemValidatorUnitTest.java b/src/test/java/net/hostsharing/hsadminng/hs/booking/item/validators/HsCloudServerBookingItemValidatorUnitTest.java index e15b95d7..787b4c08 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/booking/item/validators/HsCloudServerBookingItemValidatorUnitTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/booking/item/validators/HsCloudServerBookingItemValidatorUnitTest.java @@ -1,23 +1,37 @@ package net.hostsharing.hsadminng.hs.booking.item.validators; +import net.hostsharing.hsadminng.hs.booking.debitor.HsBookingDebitorEntity; import net.hostsharing.hsadminng.hs.booking.item.HsBookingItemEntity; +import net.hostsharing.hsadminng.hs.booking.project.HsBookingProjectEntity; import org.junit.jupiter.api.Test; import java.util.Map; +import static java.util.List.of; import static java.util.Map.entry; +import static java.util.Map.ofEntries; import static net.hostsharing.hsadminng.hs.booking.item.HsBookingItemType.CLOUD_SERVER; -import static net.hostsharing.hsadminng.hs.booking.item.validators.HsBookingItemEntityValidators.forType; +import static net.hostsharing.hsadminng.hs.booking.item.HsBookingItemType.MANAGED_SERVER; +import static net.hostsharing.hsadminng.hs.booking.item.HsBookingItemType.PRIVATE_CLOUD; import static org.assertj.core.api.Assertions.assertThat; class HsCloudServerBookingItemValidatorUnitTest { + final HsBookingDebitorEntity debitor = HsBookingDebitorEntity.builder() + .debitorNumber(12345) + .build(); + final HsBookingProjectEntity project = HsBookingProjectEntity.builder() + .debitor(debitor) + .caption("Test-Project") + .build(); + @Test void validatesProperties() { // given - final var validator = HsBookingItemEntityValidators.forType(CLOUD_SERVER); final var cloudServerBookingItemEntity = HsBookingItemEntity.builder() .type(CLOUD_SERVER) + .project(project) + .caption("Test-Server") .resources(Map.ofEntries( entry("CPUs", 2), entry("RAM", 25), @@ -28,24 +42,77 @@ class HsCloudServerBookingItemValidatorUnitTest { .build(); // when - final var result = validator.validate(cloudServerBookingItemEntity); + final var result = HsBookingItemEntityValidatorRegistry.doValidate(cloudServerBookingItemEntity); // then - assertThat(result).containsExactly("'resources.SLA-EMail' is not expected but is set to 'true'"); + assertThat(result).containsExactly("'D-12345:Test-Project:Test-Server.resources.SLA-EMail' is not expected but is set to 'true'"); } @Test void containsAllValidations() { // when - final var validator = forType(CLOUD_SERVER); + final var validator = HsBookingItemEntityValidatorRegistry.forType(CLOUD_SERVER); // then assertThat(validator.properties()).map(Map::toString).containsExactlyInAnyOrder( - "{type=integer, propertyName=CPUs, required=true, unit=null, min=1, max=32, step=null}", - "{type=integer, propertyName=RAM, required=true, unit=GB, min=1, max=128, step=null}", - "{type=integer, propertyName=SSD, required=true, unit=GB, min=25, max=1000, step=25}", - "{type=integer, propertyName=HDD, required=false, unit=GB, min=0, max=4000, step=250}", - "{type=integer, propertyName=Traffic, required=true, unit=GB, min=250, max=10000, step=250}", - "{type=enumeration, propertyName=SLA-Infrastructure, required=false, values=[BASIC, EXT8H, EXT4H, EXT2H]}"); + "{type=integer, propertyName=CPUs, min=1, max=32, required=true, isTotalsValidator=false}", + "{type=integer, propertyName=RAM, unit=GB, min=1, max=128, required=true, isTotalsValidator=false}", + "{type=integer, propertyName=SSD, unit=GB, min=25, max=1000, step=25, required=true, isTotalsValidator=false}", + "{type=integer, propertyName=HDD, unit=GB, min=0, max=4000, step=250, required=false, defaultValue=0, isTotalsValidator=false}", + "{type=integer, propertyName=Traffic, unit=GB, min=250, max=10000, step=250, required=true, isTotalsValidator=false}", + "{type=enumeration, propertyName=SLA-Infrastructure, values=[BASIC, EXT8H, EXT4H, EXT2H], required=false, isTotalsValidator=false}"); + } + + @Test + void validatesExceedingPropertyTotals() { + // given + final var subCloudServerBookingItemEntity = HsBookingItemEntity.builder() + .type(CLOUD_SERVER) + .caption("Test Cloud-Server") + .resources(ofEntries( + entry("CPUs", 2), + entry("RAM", 10), + entry("SSD", 50), + entry("Traffic", 2500) + )) + .build(); + final HsBookingItemEntity subManagedServerBookingItemEntity = HsBookingItemEntity.builder() + .type(MANAGED_SERVER) + .caption("Test Managed-Server") + .resources(ofEntries( + entry("CPUs", 3), + entry("RAM", 20), + entry("SSD", 100), + entry("Traffic", 3000) + )) + .build(); + final var privateCloudBookingItemEntity = HsBookingItemEntity.builder() + .type(PRIVATE_CLOUD) + .project(project) + .caption("Test Cloud") + .resources(ofEntries( + entry("CPUs", 4), + entry("RAM", 20), + entry("SSD", 100), + entry("Traffic", 5000) + )) + .subBookingItems(of( + subManagedServerBookingItemEntity, + subCloudServerBookingItemEntity + )) + .build(); + subManagedServerBookingItemEntity.setParentItem(privateCloudBookingItemEntity); + subCloudServerBookingItemEntity.setParentItem(privateCloudBookingItemEntity); + + // when + final var result = HsBookingItemEntityValidatorRegistry.doValidate(subCloudServerBookingItemEntity); + + // then + assertThat(result).containsExactlyInAnyOrder( + "'D-12345:Test-Project:Test Cloud.resources.CPUs' maximum total is 4, but actual total CPUs 5", + "'D-12345:Test-Project:Test Cloud.resources.RAM' maximum total is 20 GB, but actual total RAM 30 GB", + "'D-12345:Test-Project:Test Cloud.resources.SSD' maximum total is 100 GB, but actual total SSD 150 GB", + "'D-12345:Test-Project:Test Cloud.resources.Traffic' maximum total is 5000 GB, but actual total Traffic 5500 GB" + ); } } diff --git a/src/test/java/net/hostsharing/hsadminng/hs/booking/item/validators/HsManagedServerBookingItemValidatorUnitTest.java b/src/test/java/net/hostsharing/hsadminng/hs/booking/item/validators/HsManagedServerBookingItemValidatorUnitTest.java index 5f2bdfc3..1fe54a82 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/booking/item/validators/HsManagedServerBookingItemValidatorUnitTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/booking/item/validators/HsManagedServerBookingItemValidatorUnitTest.java @@ -1,56 +1,228 @@ package net.hostsharing.hsadminng.hs.booking.item.validators; +import net.hostsharing.hsadminng.hs.booking.debitor.HsBookingDebitorEntity; import net.hostsharing.hsadminng.hs.booking.item.HsBookingItemEntity; +import net.hostsharing.hsadminng.hs.booking.project.HsBookingProjectEntity; +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.Collection; +import java.util.List; import java.util.Map; +import java.util.stream.Collectors; +import java.util.stream.IntStream; +import static java.util.Arrays.stream; +import static java.util.List.of; import static java.util.Map.entry; +import static java.util.Map.ofEntries; +import static net.hostsharing.hsadminng.hs.booking.item.HsBookingItemType.CLOUD_SERVER; import static net.hostsharing.hsadminng.hs.booking.item.HsBookingItemType.MANAGED_SERVER; -import static net.hostsharing.hsadminng.hs.booking.item.validators.HsBookingItemEntityValidators.forType; +import static net.hostsharing.hsadminng.hs.booking.item.HsBookingItemType.MANAGED_WEBSPACE; +import static net.hostsharing.hsadminng.hs.booking.item.HsBookingItemType.PRIVATE_CLOUD; import static org.assertj.core.api.Assertions.assertThat; class HsManagedServerBookingItemValidatorUnitTest { + final HsBookingDebitorEntity debitor = HsBookingDebitorEntity.builder() + .debitorNumber(12345) + .build(); + final HsBookingProjectEntity project = HsBookingProjectEntity.builder() + .debitor(debitor) + .caption("Test-Project") + .build(); + @Test void validatesProperties() { // given - final var validator = HsBookingItemEntityValidators.forType(MANAGED_SERVER); final var mangedServerBookingItemEntity = HsBookingItemEntity.builder() .type(MANAGED_SERVER) + .project(project) .resources(Map.ofEntries( entry("CPUs", 2), entry("RAM", 25), entry("SSD", 25), entry("Traffic", 250), + entry("SLA-Platform", "BASIC"), entry("SLA-EMail", true) )) .build(); // when - final var result = validator.validate(mangedServerBookingItemEntity); + final var result = HsBookingItemEntityValidatorRegistry.doValidate(mangedServerBookingItemEntity); // then - assertThat(result).containsExactly("'resources.SLA-EMail' is expected to be false because resources.SLA-Platform=BASIC but is true"); + assertThat(result).containsExactly("'D-12345:Test-Project:null.resources.SLA-EMail' is expected to be false because SLA-Platform=BASIC but is true"); } @Test void containsAllValidations() { // when - final var validator = forType(MANAGED_SERVER); + final var validator = HsBookingItemEntityValidatorRegistry.forType(MANAGED_SERVER); // then assertThat(validator.properties()).map(Map::toString).containsExactlyInAnyOrder( - "{type=integer, propertyName=CPUs, required=true, unit=null, min=1, max=32, step=null}", - "{type=integer, propertyName=RAM, required=true, unit=GB, min=1, max=128, step=null}", - "{type=integer, propertyName=SSD, required=true, unit=GB, min=25, max=1000, step=25}", - "{type=integer, propertyName=HDD, required=false, unit=GB, min=0, max=4000, step=250}", - "{type=integer, propertyName=Traffic, required=true, unit=GB, min=250, max=10000, step=250}", - "{type=enumeration, propertyName=SLA-Platform, required=false, values=[BASIC, EXT8H, EXT4H, EXT2H]}", - "{type=boolean, propertyName=SLA-EMail, required=false, falseIf={SLA-Platform=BASIC}}", - "{type=boolean, propertyName=SLA-Maria, required=false, falseIf={SLA-Platform=BASIC}}", - "{type=boolean, propertyName=SLA-PgSQL, required=false, falseIf={SLA-Platform=BASIC}}", - "{type=boolean, propertyName=SLA-Office, required=false, falseIf={SLA-Platform=BASIC}}", - "{type=boolean, propertyName=SLA-Web, required=false, falseIf={SLA-Platform=BASIC}}"); + "{type=integer, propertyName=CPUs, min=1, max=32, required=true, isTotalsValidator=false}", + "{type=integer, propertyName=RAM, unit=GB, min=1, max=128, required=true, isTotalsValidator=false}", + "{type=integer, propertyName=SSD, unit=GB, min=25, max=1000, step=25, required=true, isTotalsValidator=true, thresholdPercentage=200}", + "{type=integer, propertyName=HDD, unit=GB, min=0, max=4000, step=250, required=false, defaultValue=0, isTotalsValidator=true, thresholdPercentage=200}", + "{type=integer, propertyName=Traffic, unit=GB, min=250, max=10000, step=250, required=true, isTotalsValidator=true, thresholdPercentage=200}", + "{type=enumeration, propertyName=SLA-Platform, values=[BASIC, EXT8H, EXT4H, EXT2H], required=false, defaultValue=BASIC, isTotalsValidator=false}", + "{type=boolean, propertyName=SLA-EMail, required=false, defaultValue=false, isTotalsValidator=false}", + "{type=boolean, propertyName=SLA-Maria, required=false, isTotalsValidator=false}", + "{type=boolean, propertyName=SLA-PgSQL, required=false, isTotalsValidator=false}", + "{type=boolean, propertyName=SLA-Office, required=false, isTotalsValidator=false}", + "{type=boolean, propertyName=SLA-Web, required=false, isTotalsValidator=false}"); } + + @Test + void validatesExceedingPropertyTotals() { + // given + final var subCloudServerBookingItemEntity = HsBookingItemEntity.builder() + .type(CLOUD_SERVER) + .resources(ofEntries( + entry("CPUs", 2), + entry("RAM", 10), + entry("SSD", 50), + entry("Traffic", 2500) + )) + .build(); + final HsBookingItemEntity subManagedServerBookingItemEntity = HsBookingItemEntity.builder() + .type(MANAGED_SERVER) + .resources(ofEntries( + entry("CPUs", 3), + entry("RAM", 20), + entry("SSD", 100), + entry("Traffic", 3000) + )) + .build(); + final var privateCloudBookingItemEntity = HsBookingItemEntity.builder() + .type(PRIVATE_CLOUD) + .project(project) + .resources(ofEntries( + entry("CPUs", 4), + entry("RAM", 20), + entry("SSD", 100), + entry("Traffic", 5000) + )) + .subBookingItems(of( + subManagedServerBookingItemEntity, + subCloudServerBookingItemEntity + )) + .build(); + + subManagedServerBookingItemEntity.setParentItem(privateCloudBookingItemEntity); + subCloudServerBookingItemEntity.setParentItem(privateCloudBookingItemEntity); + + // when + final var result = HsBookingItemEntityValidatorRegistry.doValidate(subManagedServerBookingItemEntity); + + // then + assertThat(result).containsExactlyInAnyOrder( + "'D-12345:Test-Project:null.resources.CPUs' maximum total is 4, but actual total CPUs 5", + "'D-12345:Test-Project:null.resources.RAM' maximum total is 20 GB, but actual total RAM 30 GB", + "'D-12345:Test-Project:null.resources.SSD' maximum total is 100 GB, but actual total SSD 150 GB", + "'D-12345:Test-Project:null.resources.Traffic' maximum total is 5000 GB, but actual total Traffic 5500 GB" + ); + } + + @Test + void validatesExceedingTotals() { + // given + final var managedWebspaceBookingItem = HsBookingItemEntity.builder() + .type(MANAGED_WEBSPACE) + .project(project) + .caption("test Managed-Webspace") + .resources(ofEntries( + entry("SSD", 100), + entry("Traffic", 1000), + entry("Multi", 1) + )) + .subHostingAssets(of( + HsHostingAssetEntity.builder() + .type(HsHostingAssetType.MANAGED_WEBSPACE) + .identifier("abc00") + .subHostingAssets(concat( + generate(26, HsHostingAssetType.UNIX_USER, "xyz00-%c%c"), + generateDbUsersWithDatabases(3, HsHostingAssetType.PGSQL_USER, + "xyz00_%c%c", + 1, HsHostingAssetType.PGSQL_DATABASE + ), + generateDbUsersWithDatabases(3, HsHostingAssetType.MARIADB_USER, + "xyz00_%c%c", + 2, HsHostingAssetType.MARIADB_DATABASE + ), + generateDomainEmailSetupsWithEMailAddresses(26, HsHostingAssetType.DOMAIN_EMAIL_SETUP, + "%c%c.example.com", + 10, HsHostingAssetType.EMAIL_ADDRESS + ) + )) + .build() + )) + .build(); + + // when + final var result = HsBookingItemEntityValidatorRegistry.doValidate(managedWebspaceBookingItem); + + // then + assertThat(result).containsExactlyInAnyOrder( + "'D-12345:Test-Project:test Managed-Webspace.resources.Multi=1 allows at maximum 25 unix users, but 26 found", + "'D-12345:Test-Project:test Managed-Webspace.resources.Multi=1 allows at maximum 5 database users, but 6 found", + "'D-12345:Test-Project:test Managed-Webspace.resources.Multi=1 allows at maximum 5 databases, but 9 found", + "'D-12345:Test-Project:test Managed-Webspace.resources.Multi=1 allows at maximum 250 databases, but 260 found" + ); + } + + @SafeVarargs + private List concat(final List... hostingAssets) { + return stream(hostingAssets) + .flatMap(Collection::stream) + .collect(Collectors.toList()); + } + + private List generate(final int count, final HsHostingAssetType hostingAssetType, + final String identifierPattern) { + return IntStream.range(0, count) + .mapToObj(number -> HsHostingAssetEntity.builder() + .type(hostingAssetType) + .identifier(identifierPattern.formatted((number/'a')+'a', (number%'a')+'a')) + .build()) + .toList(); + } + + private List generateDbUsersWithDatabases( + final int userCount, + final HsHostingAssetType directAssetType, + final String directAssetIdentifierFormat, + final int dbCount, + final HsHostingAssetType subAssetType) { + return IntStream.range(0, userCount) + .mapToObj(n -> HsHostingAssetEntity.builder() + .type(directAssetType) + .identifier(directAssetIdentifierFormat.formatted((n/'a')+'a', (n%'a')+'a')) + .subHostingAssets( + generate(dbCount, subAssetType, "%c%c.example.com".formatted((n/'a')+'a', (n%'a')+'a')) + ) + .build()) + .toList(); + } + + private List generateDomainEmailSetupsWithEMailAddresses( + final int domainCount, + final HsHostingAssetType directAssetType, + final String directAssetIdentifierFormat, + final int emailAddressCount, + final HsHostingAssetType subAssetType) { + return IntStream.range(0, domainCount) + .mapToObj(n -> HsHostingAssetEntity.builder() + .type(directAssetType) + .identifier(directAssetIdentifierFormat.formatted((n/'a')+'a', (n%'a')+'a')) + .subHostingAssets( + generate(emailAddressCount, subAssetType, "xyz00_%c%c%%c%%c".formatted((n/'a')+'a', (n%'a')+'a')) + ) + .build()) + .toList(); + } + } diff --git a/src/test/java/net/hostsharing/hsadminng/hs/booking/item/validators/HsManagedWebspaceBookingItemValidatorUnitTest.java b/src/test/java/net/hostsharing/hsadminng/hs/booking/item/validators/HsManagedWebspaceBookingItemValidatorUnitTest.java index 8a278850..dd9081ee 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/booking/item/validators/HsManagedWebspaceBookingItemValidatorUnitTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/booking/item/validators/HsManagedWebspaceBookingItemValidatorUnitTest.java @@ -1,54 +1,66 @@ package net.hostsharing.hsadminng.hs.booking.item.validators; +import net.hostsharing.hsadminng.hs.booking.debitor.HsBookingDebitorEntity; import net.hostsharing.hsadminng.hs.booking.item.HsBookingItemEntity; +import net.hostsharing.hsadminng.hs.booking.project.HsBookingProjectEntity; import org.junit.jupiter.api.Test; import java.util.Map; import static java.util.Map.entry; import static net.hostsharing.hsadminng.hs.booking.item.HsBookingItemType.MANAGED_WEBSPACE; -import static net.hostsharing.hsadminng.hs.booking.item.validators.HsBookingItemEntityValidators.forType; import static org.assertj.core.api.Assertions.assertThat; class HsManagedWebspaceBookingItemValidatorUnitTest { + final HsBookingDebitorEntity debitor = HsBookingDebitorEntity.builder() + .debitorNumber(12345) + .build(); + final HsBookingProjectEntity project = HsBookingProjectEntity.builder() + .debitor(debitor) + .caption("Test-Project") + .build(); + @Test void validatesProperties() { // given final var mangedServerBookingItemEntity = HsBookingItemEntity.builder() .type(MANAGED_WEBSPACE) + .project(project) + .caption("Test Managed-Webspace") .resources(Map.ofEntries( entry("CPUs", 2), entry("RAM", 25), - entry("SSD", 25), entry("Traffic", 250), entry("SLA-EMail", true) )) .build(); - final var validator = forType(mangedServerBookingItemEntity.getType()); // when - final var result = validator.validate(mangedServerBookingItemEntity); + final var result = HsBookingItemEntityValidatorRegistry.doValidate(mangedServerBookingItemEntity); // then assertThat(result).containsExactlyInAnyOrder( - "'resources.CPUs' is not expected but is set to '2'", - "'resources.SLA-EMail' is not expected but is set to 'true'", - "'resources.RAM' is not expected but is set to '25'"); + "'D-12345:Test-Project:Test Managed-Webspace.resources.CPUs' is not expected but is set to '2'", + "'D-12345:Test-Project:Test Managed-Webspace.resources.RAM' is not expected but is set to '25'", + "'D-12345:Test-Project:Test Managed-Webspace.resources.SSD' is required but missing", + "'D-12345:Test-Project:Test Managed-Webspace.resources.SLA-EMail' is not expected but is set to 'true'" + ); } @Test void containsAllValidations() { // when - final var validator = forType(MANAGED_WEBSPACE); + final var validator = HsBookingItemEntityValidatorRegistry.forType(MANAGED_WEBSPACE); // then assertThat(validator.properties()).map(Map::toString).containsExactlyInAnyOrder( - "{type=integer, propertyName=SSD, required=true, unit=GB, min=1, max=100, step=1}", - "{type=integer, propertyName=HDD, required=false, unit=GB, min=0, max=250, step=10}", - "{type=integer, propertyName=Traffic, required=true, unit=GB, min=10, max=1000, step=10}", - "{type=enumeration, propertyName=SLA-Platform, required=false, values=[BASIC, EXT24H]}", - "{type=integer, propertyName=Daemons, required=false, unit=null, min=0, max=10, step=null}", - "{type=boolean, propertyName=Online Office Server, required=false, falseIf=null}"); + "{type=integer, propertyName=SSD, unit=GB, min=1, max=100, step=1, required=true, isTotalsValidator=false}", + "{type=integer, propertyName=HDD, unit=GB, min=0, max=250, step=10, required=false, isTotalsValidator=false}", + "{type=integer, propertyName=Traffic, unit=GB, min=10, max=1000, step=10, required=true, isTotalsValidator=false}", + "{type=integer, propertyName=Multi, min=1, max=100, step=1, required=false, defaultValue=1, isTotalsValidator=false}", + "{type=integer, propertyName=Daemons, min=0, max=10, required=false, defaultValue=0, isTotalsValidator=false}", + "{type=boolean, propertyName=Online Office Server, required=false, isTotalsValidator=false}", + "{type=enumeration, propertyName=SLA-Platform, values=[BASIC, EXT24H], required=false, defaultValue=BASIC, isTotalsValidator=false}"); } } diff --git a/src/test/java/net/hostsharing/hsadminng/hs/booking/item/validators/HsPrivateCloudBookingItemValidatorUnitTest.java b/src/test/java/net/hostsharing/hsadminng/hs/booking/item/validators/HsPrivateCloudBookingItemValidatorUnitTest.java new file mode 100644 index 00000000..5079f340 --- /dev/null +++ b/src/test/java/net/hostsharing/hsadminng/hs/booking/item/validators/HsPrivateCloudBookingItemValidatorUnitTest.java @@ -0,0 +1,112 @@ +package net.hostsharing.hsadminng.hs.booking.item.validators; + +import net.hostsharing.hsadminng.hs.booking.debitor.HsBookingDebitorEntity; +import net.hostsharing.hsadminng.hs.booking.item.HsBookingItemEntity; +import net.hostsharing.hsadminng.hs.booking.project.HsBookingProjectEntity; +import org.junit.jupiter.api.Test; + +import static java.util.List.of; +import static java.util.Map.entry; +import static java.util.Map.ofEntries; +import static net.hostsharing.hsadminng.hs.booking.item.HsBookingItemType.CLOUD_SERVER; +import static net.hostsharing.hsadminng.hs.booking.item.HsBookingItemType.MANAGED_SERVER; +import static net.hostsharing.hsadminng.hs.booking.item.HsBookingItemType.PRIVATE_CLOUD; +import static org.assertj.core.api.Assertions.assertThat; + +class HsPrivateCloudBookingItemValidatorUnitTest { + + final HsBookingDebitorEntity debitor = HsBookingDebitorEntity.builder() + .debitorNumber(12345) + .build(); + final HsBookingProjectEntity project = HsBookingProjectEntity.builder() + .debitor(debitor) + .caption("Test-Project") + .build(); + + @Test + void validatesPropertyTotals() { + // given + final var privateCloudBookingItemEntity = HsBookingItemEntity.builder() + .type(PRIVATE_CLOUD) + .resources(ofEntries( + entry("CPUs", 4), + entry("RAM", 20), + entry("SSD", 100), + entry("Traffic", 5000) + )) + .subBookingItems(of( + HsBookingItemEntity.builder() + .type(MANAGED_SERVER) + .resources(ofEntries( + entry("CPUs", 2), + entry("RAM", 10), + entry("SSD", 50), + entry("Traffic", 2500) + )) + .build(), + HsBookingItemEntity.builder() + .type(CLOUD_SERVER) + .resources(ofEntries( + entry("CPUs", 2), + entry("RAM", 10), + entry("SSD", 50), + entry("Traffic", 2500) + )) + .build() + )) + .build(); + + // when + final var result = HsBookingItemEntityValidatorRegistry.doValidate(privateCloudBookingItemEntity); + + // then + assertThat(result).isEmpty(); + } + + @Test + void validatesExceedingPropertyTotals() { + // given + final var privateCloudBookingItemEntity = HsBookingItemEntity.builder() + .project(project) + .type(PRIVATE_CLOUD) + .resources(ofEntries( + entry("CPUs", 4), + entry("RAM", 20), + entry("SSD", 100), + entry("Traffic", 5000) + )) + .subBookingItems(of( + HsBookingItemEntity.builder() + .type(MANAGED_SERVER) + .resources(ofEntries( + entry("CPUs", 3), + entry("RAM", 20), + entry("SSD", 100), + entry("Traffic", 3000) + )) + .build(), + HsBookingItemEntity.builder() + .type(CLOUD_SERVER) + .resources(ofEntries( + entry("CPUs", 2), + entry("RAM", 10), + entry("SSD", 50), + entry("Traffic", 2500) + )) + .build() + )) + .build(); + + // when + final var result = HsBookingItemEntityValidatorRegistry.doValidate(privateCloudBookingItemEntity); + + // then + assertThat(result).containsExactlyInAnyOrder( + "'D-12345:Test-Project:null.resources.CPUs' maximum total is 4, but actual total CPUs 5", + "'D-12345:Test-Project:null.resources.RAM' maximum total is 20 GB, but actual total RAM 30 GB", + "'D-12345:Test-Project:null.resources.SSD' maximum total is 100 GB, but actual total SSD 150 GB", + "'D-12345:Test-Project:null.resources.Traffic' maximum total is 5000 GB, but actual total Traffic 5500 GB" + ); + } + +} diff --git a/src/test/java/net/hostsharing/hsadminng/hs/booking/project/HsBookingProjectRepositoryIntegrationTest.java b/src/test/java/net/hostsharing/hsadminng/hs/booking/project/HsBookingProjectRepositoryIntegrationTest.java index 70676f84..e73bf942 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/booking/project/HsBookingProjectRepositoryIntegrationTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/booking/project/HsBookingProjectRepositoryIntegrationTest.java @@ -4,7 +4,7 @@ import net.hostsharing.hsadminng.context.Context; import net.hostsharing.hsadminng.hs.booking.debitor.HsBookingDebitorRepository; import net.hostsharing.hsadminng.rbac.rbacgrant.RawRbacGrantRepository; import net.hostsharing.hsadminng.rbac.rbacrole.RawRbacRoleRepository; -import net.hostsharing.hsadminng.rbac.test.Array; +import net.hostsharing.hsadminng.mapper.Array; import net.hostsharing.hsadminng.rbac.test.ContextBasedTestWithCleanup; import net.hostsharing.hsadminng.rbac.test.JpaAttempt; import org.junit.jupiter.api.Nested; @@ -23,7 +23,7 @@ import java.util.List; import static net.hostsharing.hsadminng.rbac.rbacgrant.RawRbacGrantEntity.distinctGrantDisplaysOf; import static net.hostsharing.hsadminng.rbac.rbacrole.RawRbacRoleEntity.distinctRoleNamesOf; -import static net.hostsharing.hsadminng.rbac.test.Array.fromFormatted; +import static net.hostsharing.hsadminng.mapper.Array.fromFormatted; import static net.hostsharing.hsadminng.rbac.test.JpaAttempt.attempt; import static org.assertj.core.api.Assertions.assertThat; diff --git a/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetControllerAcceptanceTest.java b/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetControllerAcceptanceTest.java index 5204a1ec..e9f8180d 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetControllerAcceptanceTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetControllerAcceptanceTest.java @@ -5,6 +5,7 @@ import io.restassured.http.ContentType; import net.hostsharing.hsadminng.HsadminNgApplication; import net.hostsharing.hsadminng.hs.booking.item.HsBookingItemEntity; import net.hostsharing.hsadminng.hs.booking.item.HsBookingItemRepository; +import net.hostsharing.hsadminng.hs.booking.item.HsBookingItemType; import net.hostsharing.hsadminng.hs.booking.project.HsBookingProjectRepository; import net.hostsharing.hsadminng.hs.office.debitor.HsOfficeDebitorRepository; import net.hostsharing.hsadminng.rbac.test.ContextBasedTestWithCleanup; @@ -20,8 +21,9 @@ import java.util.Map; import java.util.UUID; import static java.util.Map.entry; -import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.CLOUD_SERVER; import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.MANAGED_SERVER; +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.rbac.test.JsonMatcher.lenientlyEquals; import static org.assertj.core.api.Assertions.assertThat; import static org.hamcrest.Matchers.matchesRegex; @@ -77,25 +79,19 @@ class HsHostingAssetControllerAcceptanceTest extends ContextBasedTestWithCleanup "type": "MANAGED_WEBSPACE", "identifier": "sec01", "caption": "some Webspace", - "config": { - "extra": 42 - } + "config": {} }, { "type": "MANAGED_WEBSPACE", "identifier": "fir01", "caption": "some Webspace", - "config": { - "extra": 42 - } + "config": {} }, { "type": "MANAGED_WEBSPACE", "identifier": "thi01", "caption": "some Webspace", - "config": { - "extra": 42 - } + "config": {} } ] """)); @@ -110,41 +106,47 @@ class HsHostingAssetControllerAcceptanceTest extends ContextBasedTestWithCleanup RestAssured // @formatter:off .given() - .header("current-user", "superuser-alex@hostsharing.net") - .port(port) + .header("current-user", "superuser-alex@hostsharing.net") + .port(port) .when() - .get("http://localhost/api/hs/hosting/assets?type=" + MANAGED_SERVER) + . get("http://localhost/api/hs/hosting/assets?type=" + MANAGED_SERVER) .then().log().all().assertThat() - .statusCode(200) - .contentType("application/json") - .body("", lenientlyEquals(""" - [ - { - "type": "MANAGED_SERVER", - "identifier": "vm1011", - "caption": "some ManagedServer", - "config": { - "extra": 42 + .statusCode(200) + .contentType("application/json") + .body("", lenientlyEquals(""" + [ + { + "type": "MANAGED_SERVER", + "identifier": "vm1011", + "caption": "some ManagedServer", + "config": { + "monit_max_cpu_usage": 90, + "monit_max_ram_usage": 80, + "monit_max_ssd_usage": 70 + } + }, + { + "type": "MANAGED_SERVER", + "identifier": "vm1012", + "caption": "some ManagedServer", + "config": { + "monit_max_cpu_usage": 90, + "monit_max_ram_usage": 80, + "monit_max_ssd_usage": 70 + } + }, + { + "type": "MANAGED_SERVER", + "identifier": "vm1013", + "caption": "some ManagedServer", + "config": { + "monit_max_cpu_usage": 90, + "monit_max_ram_usage": 80, + "monit_max_ssd_usage": 70 + } } - }, - { - "type": "MANAGED_SERVER", - "identifier": "vm1012", - "caption": "some ManagedServer", - "config": { - "extra": 42 - } - }, - { - "type": "MANAGED_SERVER", - "identifier": "vm1013", - "caption": "some ManagedServer", - "config": { - "extra": 42 - } - } - ] - """)); + ] + """)); // @formatter:on } } @@ -156,7 +158,14 @@ class HsHostingAssetControllerAcceptanceTest extends ContextBasedTestWithCleanup void globalAdmin_canAddBookedAsset() { context.define("superuser-alex@hostsharing.net"); - final var givenBookingItem = givenBookingItem("D-1000111 default project", "some PrivateCloud"); + final var givenBookingItem = newBookingItem("D-1000111 default project", + HsBookingItemType.MANAGED_WEBSPACE, "separate ManagedWebspace BI", + Map.ofEntries( + entry("SSD", 50), + entry("Traffic", 50) + ) + ); + final var givenParentAsset = givenParentAsset(MANAGED_SERVER, "vm1011"); final var location = RestAssured // @formatter:off .given() @@ -165,12 +174,13 @@ class HsHostingAssetControllerAcceptanceTest extends ContextBasedTestWithCleanup .body(""" { "bookingItemUuid": "%s", - "type": "MANAGED_SERVER", - "identifier": "vm1400", - "caption": "some new ManagedServer", - "config": { "monit_max_ssd_usage": 80, "monit_max_cpu_usage": 90, "monit_max_ram_usage": 70 } + "type": "MANAGED_WEBSPACE", + "identifier": "fir10", + "parentAssetUuid": "%s", + "caption": "some separate ManagedWebspace HA", + "config": {} } - """.formatted(givenBookingItem.getUuid())) + """.formatted(givenBookingItem.getUuid(), givenParentAsset.getUuid())) .port(port) .when() .post("http://localhost/api/hs/hosting/assets") @@ -179,19 +189,20 @@ class HsHostingAssetControllerAcceptanceTest extends ContextBasedTestWithCleanup .contentType(ContentType.JSON) .body("", lenientlyEquals(""" { - "type": "MANAGED_SERVER", - "identifier": "vm1400", - "caption": "some new ManagedServer", - "config": { "monit_max_ssd_usage": 80, "monit_max_cpu_usage": 90, "monit_max_ram_usage": 70 } + "type": "MANAGED_WEBSPACE", + "identifier": "fir10", + "caption": "some separate ManagedWebspace HA", + "config": {} } """)) .header("Location", matchesRegex("http://localhost:[1-9][0-9]*/api/hs/hosting/assets/[^/]*")) .extract().header("Location"); // @formatter:on // finally, the new asset can be accessed under the generated UUID - final var newUserUuid = UUID.fromString( + final var newWebspace = UUID.fromString( location.substring(location.lastIndexOf('/') + 1)); - assertThat(newUserUuid).isNotNull(); + assertThat(newWebspace).isNotNull(); + toCleanup(HsHostingAssetEntity.class, newWebspace); } @Test @@ -240,7 +251,7 @@ class HsHostingAssetControllerAcceptanceTest extends ContextBasedTestWithCleanup } @Test - void additionalValidationsArePerformend_whenAddingAsset() { + void propertyValidationsArePerformend_whenAddingAsset() { context.define("superuser-alex@hostsharing.net"); final var givenBookingItem = givenBookingItem("D-1000111 default project", "some PrivateCloud"); @@ -267,9 +278,66 @@ class HsHostingAssetControllerAcceptanceTest extends ContextBasedTestWithCleanup .body("", lenientlyEquals(""" { "statusPhrase": "Bad Request", - "message": "['config.extra' is not expected but is set to '42', 'config.monit_max_ssd_usage' is expected to be >= 10 but is 0, 'config.monit_max_cpu_usage' is expected to be <= 100 but is 101, 'config.monit_max_ram_usage' is required but missing]" + "message": "[ + <<<'MANAGED_SERVER:vm1400.config.extra' is not expected but is set to '42', + <<<'MANAGED_SERVER:vm1400.config.monit_max_ssd_usage' is expected to be >= 10 but is 0, + <<<'MANAGED_SERVER:vm1400.config.monit_max_cpu_usage' is expected to be <= 100 but is 101, + <<<'MANAGED_SERVER:vm1400.config.monit_max_ram_usage' is required but missing + <<<]" } - """)); // @formatter:on + """.replaceAll(" +<<<", ""))); // @formatter:on + } + + + @Test + void totalsLimitValidationsArePerformend_whenAddingAsset() { + + context.define("superuser-alex@hostsharing.net"); + final var givenHostingAsset = givenHostingAsset(MANAGED_WEBSPACE, "fir01"); + assertThat(givenHostingAsset.getBookingItem().getResources().get("Multi")) + .as("precondition failed") + .isEqualTo(1); + + jpaAttempt.transacted(() -> { + context.define("superuser-alex@hostsharing.net"); + for (int n = 0; n < 25; ++n ) { + toCleanup(assetRepo.save( + HsHostingAssetEntity.builder() + .type(UNIX_USER) + .parentAsset(givenHostingAsset) + .identifier("fir01-%2d".formatted(n)) + .caption("Test UnixUser fir01-%2d".formatted(n)) + .build())); + } + }).assertSuccessful(); + + RestAssured // @formatter:off + .given() + .header("current-user", "superuser-alex@hostsharing.net") + .contentType(ContentType.JSON) + .body(""" + { + "parentAssetUuid": "%s", + "type": "UNIX_USER", + "identifier": "fir01-extra", + "caption": "some extra UnixUser", + "config": { } + } + """.formatted(givenHostingAsset.getUuid())) + .port(port) + .when() + .post("http://localhost/api/hs/hosting/assets") + .then().log().all().assertThat() + .statusCode(400) + .contentType(ContentType.JSON) + .body("", lenientlyEquals(""" + { + "statusPhrase": "Bad Request", + "message": "[ + <<<'D-1000111:D-1000111 default project:separate ManagedWebspace.resources.Multi=1 allows at maximum 25 unix users, but 26 found + <<<]" + } + """.replaceAll(" +<<<", ""))); // @formatter:on } } @@ -295,9 +363,7 @@ class HsHostingAssetControllerAcceptanceTest extends ContextBasedTestWithCleanup .body("", lenientlyEquals(""" { "caption": "some ManagedServer", - "config": { - "extra": 42 - } + "config": {} } """)); // @formatter:on } @@ -340,9 +406,7 @@ class HsHostingAssetControllerAcceptanceTest extends ContextBasedTestWithCleanup { "identifier": "vm1013", "caption": "some ManagedServer", - "config": { - "extra": 42 - } + "config": {} } """)); // @formatter:on } @@ -443,6 +507,29 @@ class HsHostingAssetControllerAcceptanceTest extends ContextBasedTestWithCleanup } } + HsHostingAssetEntity givenHostingAsset(final HsHostingAssetType type, final String identifier) { + return assetRepo.findByIdentifier(identifier).stream() + .filter(ha -> ha.getType()==type) + .findAny().orElseThrow(); + } + + HsBookingItemEntity newBookingItem( + final String projectCaption, + final HsBookingItemType type, final String bookingItemCaption, final Map resources) { + return jpaAttempt.transacted(() -> { + context.define("superuser-alex@hostsharing.net"); + final var project = projectRepo.findByCaption(projectCaption).stream() + .findAny().orElseThrow(); + final var bookingItem = HsBookingItemEntity.builder() + .project(project) + .type(type) + .caption(bookingItemCaption) + .resources(resources) + .build(); + return toCleanup(bookingItemRepo.save(bookingItem)); + }).assertSuccessful().returnedValue(); + } + HsBookingItemEntity givenBookingItem(final String projectCaption, final String bookingItemCaption) { return bookingItemRepo.findByCaption(bookingItemCaption).stream() .filter(bi -> bi.getRelatedProject().getCaption().contains(projectCaption)) diff --git a/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetEntityPatcherUnitTest.java b/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetEntityPatcherUnitTest.java index d726c9b4..2530f5fa 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetEntityPatcherUnitTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetEntityPatcherUnitTest.java @@ -40,11 +40,11 @@ class HsHostingAssetEntityPatcherUnitTest extends PatchUnitTestBase< private static final Map PATCH_CONFIG = patchMap( entry("CPU", 2), entry("HDD", null), - entry("SDD", 256) + entry("SSD", 256) ); private static final Map PATCHED_CONFIG = patchMap( entry("CPU", 2), - entry("SDD", 256), + entry("SSD", 256), entry("MEM", 64) ); diff --git a/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetPropsControllerAcceptanceTest.java b/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetPropsControllerAcceptanceTest.java index 55c2e29e..e8195eeb 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetPropsControllerAcceptanceTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetPropsControllerAcceptanceTest.java @@ -33,7 +33,8 @@ class HsHostingAssetPropsControllerAcceptanceTest { [ "MANAGED_SERVER", "MANAGED_WEBSPACE", - "CLOUD_SERVER" + "CLOUD_SERVER", + "UNIX_USER" ] """)); // @formatter:on @@ -55,56 +56,54 @@ class HsHostingAssetPropsControllerAcceptanceTest { { "type": "integer", "propertyName": "monit_min_free_ssd", - "required": false, - "unit": null, "min": 1, "max": 1000, - "step": null + "required": false, + "isTotalsValidator": false }, { "type": "integer", "propertyName": "monit_min_free_hdd", - "required": false, - "unit": null, "min": 1, "max": 4000, - "step": null + "required": false, + "isTotalsValidator": false }, { "type": "integer", "propertyName": "monit_max_ssd_usage", - "required": true, "unit": "%", "min": 10, "max": 100, - "step": null + "required": true, + "isTotalsValidator": false }, { "type": "integer", "propertyName": "monit_max_hdd_usage", - "required": false, "unit": "%", "min": 10, "max": 100, - "step": null + "required": false, + "isTotalsValidator": false }, { "type": "integer", "propertyName": "monit_max_cpu_usage", - "required": true, "unit": "%", "min": 10, "max": 100, - "step": null + "required": true, + "isTotalsValidator": false }, { "type": "integer", "propertyName": "monit_max_ram_usage", - "required": true, "unit": "%", "min": 10, "max": 100, - "step": null + "required": true, + "isTotalsValidator": false } ] """)); diff --git a/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetRepositoryIntegrationTest.java b/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetRepositoryIntegrationTest.java index f781046a..83560cc9 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetRepositoryIntegrationTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetRepositoryIntegrationTest.java @@ -3,10 +3,11 @@ package net.hostsharing.hsadminng.hs.hosting.asset; import net.hostsharing.hsadminng.context.Context; import net.hostsharing.hsadminng.hs.booking.item.HsBookingItemEntity; import net.hostsharing.hsadminng.hs.booking.item.HsBookingItemRepository; +import net.hostsharing.hsadminng.hs.booking.item.HsBookingItemType; import net.hostsharing.hsadminng.hs.booking.project.HsBookingProjectRepository; import net.hostsharing.hsadminng.rbac.rbacgrant.RawRbacGrantRepository; import net.hostsharing.hsadminng.rbac.rbacrole.RawRbacRoleRepository; -import net.hostsharing.hsadminng.rbac.test.Array; +import net.hostsharing.hsadminng.mapper.Array; import net.hostsharing.hsadminng.rbac.test.ContextBasedTestWithCleanup; import net.hostsharing.hsadminng.rbac.test.JpaAttempt; import org.junit.jupiter.api.Nested; @@ -30,7 +31,7 @@ import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.MANA import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.MANAGED_WEBSPACE; import static net.hostsharing.hsadminng.rbac.rbacgrant.RawRbacGrantEntity.distinctGrantDisplaysOf; import static net.hostsharing.hsadminng.rbac.rbacrole.RawRbacRoleEntity.distinctRoleNamesOf; -import static net.hostsharing.hsadminng.rbac.test.Array.fromFormatted; +import static net.hostsharing.hsadminng.mapper.Array.fromFormatted; import static net.hostsharing.hsadminng.rbac.test.JpaAttempt.attempt; import static org.assertj.core.api.Assertions.assertThat; @@ -70,12 +71,13 @@ class HsHostingAssetRepositoryIntegrationTest extends ContextBasedTestWithCleanu // given context("superuser-alex@hostsharing.net"); final var count = assetRepo.count(); - final var givenManagedServer = givenManagedServer("D-1000111 default project", MANAGED_SERVER); + final var givenManagedServer = givenHostingAsset("D-1000111 default project", MANAGED_SERVER); + final var newWebspaceBookingItem = newBookingItem(givenManagedServer.getBookingItem(), HsBookingItemType.MANAGED_WEBSPACE, "fir01"); // when final var result = attempt(em, () -> { final var newAsset = HsHostingAssetEntity.builder() - .bookingItem(givenManagedServer.getBookingItem()) + .bookingItem(newWebspaceBookingItem) .parentAsset(givenManagedServer) .caption("some new managed webspace") .type(MANAGED_WEBSPACE) @@ -95,18 +97,19 @@ class HsHostingAssetRepositoryIntegrationTest extends ContextBasedTestWithCleanu public void createsAndGrantsRoles() { // given context("superuser-alex@hostsharing.net"); + final var givenManagedServer = givenHostingAsset("D-1000111 default project", MANAGED_SERVER); + final var newWebspaceBookingItem = newBookingItem(givenManagedServer.getBookingItem(), HsBookingItemType.MANAGED_WEBSPACE, "fir01"); + em.flush(); final var initialRoleNames = distinctRoleNamesOf(rawRoleRepo.findAll()); - final var initialGrantNames = distinctGrantDisplaysOf(rawGrantRepo.findAll()).stream() - .map(s -> s.replace("hs_office_", "")) - .toList(); - final var givenBookingItem = givenBookingItem("D-1000111 default project", "some PrivateCloud"); + final var initialGrantNames = distinctGrantDisplaysOf(rawGrantRepo.findAll()); // when final var result = attempt(em, () -> { final var newAsset = HsHostingAssetEntity.builder() - .bookingItem(givenBookingItem) - .type(HsHostingAssetType.MANAGED_SERVER) - .identifier("vm9000") + .bookingItem(newWebspaceBookingItem) + .parentAsset(givenManagedServer) + .type(HsHostingAssetType.MANAGED_WEBSPACE) + .identifier("fir00") .caption("some new managed webspace") .build(); return toCleanup(assetRepo.save(newAsset)); @@ -117,29 +120,33 @@ class HsHostingAssetRepositoryIntegrationTest extends ContextBasedTestWithCleanu final var all = rawRoleRepo.findAll(); assertThat(distinctRoleNamesOf(all)).containsExactlyInAnyOrder(Array.from( initialRoleNames, - "hs_hosting_asset#vm9000:OWNER", - "hs_hosting_asset#vm9000:ADMIN", - "hs_hosting_asset#vm9000:AGENT", - "hs_hosting_asset#vm9000:TENANT")); + "hs_hosting_asset#fir00:ADMIN", + "hs_hosting_asset#fir00:AGENT", + "hs_hosting_asset#fir00:OWNER", + "hs_hosting_asset#fir00:TENANT")); assertThat(distinctGrantDisplaysOf(rawGrantRepo.findAll())) - .map(s -> s.replace("hs_office_", "")) .containsExactlyInAnyOrder(fromFormatted( initialGrantNames, // owner - "{ grant role:hs_hosting_asset#vm9000:OWNER to role:hs_booking_item#somePrivateCloud:ADMIN by system and assume }", - "{ grant perm:hs_hosting_asset#vm9000:DELETE to role:hs_hosting_asset#vm9000:OWNER by system and assume }", - "{ grant role:hs_hosting_asset#vm9000:ADMIN to role:hs_hosting_asset#vm9000:OWNER by system and assume }", + "{ grant role:hs_hosting_asset#fir00:OWNER to role:hs_booking_item#fir01:ADMIN by system and assume }", + "{ grant role:hs_hosting_asset#fir00:OWNER to role:hs_hosting_asset#vm1011:ADMIN by system and assume }", + "{ grant perm:hs_hosting_asset#fir00:DELETE to role:hs_hosting_asset#fir00:OWNER by system and assume }", // admin - "{ grant perm:hs_hosting_asset#vm9000:INSERT>hs_hosting_asset to role:hs_hosting_asset#vm9000:ADMIN by system and assume }", - "{ grant perm:hs_hosting_asset#vm9000:UPDATE to role:hs_hosting_asset#vm9000:ADMIN by system and assume }", - "{ grant role:hs_hosting_asset#vm9000:ADMIN to role:hs_booking_item#somePrivateCloud:AGENT by system and assume }", - "{ grant role:hs_hosting_asset#vm9000:TENANT to role:hs_hosting_asset#vm9000:AGENT by system and assume }", - "{ grant role:hs_hosting_asset#vm9000:AGENT to role:hs_hosting_asset#vm9000:ADMIN by system and assume }", + "{ grant role:hs_hosting_asset#fir00:ADMIN to role:hs_hosting_asset#fir00:OWNER by system and assume }", + "{ grant role:hs_hosting_asset#fir00:ADMIN to role:hs_booking_item#fir01:AGENT by system and assume }", + "{ grant perm:hs_hosting_asset#fir00:INSERT>hs_hosting_asset to role:hs_hosting_asset#fir00:ADMIN by system and assume }", + "{ grant perm:hs_hosting_asset#fir00:UPDATE to role:hs_hosting_asset#fir00:ADMIN by system and assume }", + + // agent + "{ grant role:hs_hosting_asset#fir00:ADMIN to role:hs_hosting_asset#vm1011:AGENT by system and assume }", + "{ grant role:hs_hosting_asset#fir00:AGENT to role:hs_hosting_asset#fir00:ADMIN by system and assume }", // tenant - "{ grant role:hs_booking_item#somePrivateCloud:TENANT to role:hs_hosting_asset#vm9000:TENANT by system and assume }", + "{ grant role:hs_booking_item#fir01:TENANT to role:hs_hosting_asset#fir00:TENANT by system and assume }", + "{ grant role:hs_hosting_asset#fir00:TENANT to role:hs_hosting_asset#fir00:AGENT by system and assume }", + "{ grant role:hs_hosting_asset#vm1011:TENANT to role:hs_hosting_asset#fir00:TENANT by system and assume }", null)); } @@ -164,9 +171,9 @@ class HsHostingAssetRepositoryIntegrationTest extends ContextBasedTestWithCleanu // then allTheseServersAreReturned( result, - "HsHostingAssetEntity(MANAGED_WEBSPACE, sec01, some Webspace, MANAGED_SERVER:vm1012, D-1000212:D-1000212 default project:separate ManagedServer, { extra: 42 })", - "HsHostingAssetEntity(MANAGED_WEBSPACE, thi01, some Webspace, MANAGED_SERVER:vm1013, D-1000313:D-1000313 default project:separate ManagedServer, { extra: 42 })", - "HsHostingAssetEntity(MANAGED_WEBSPACE, fir01, some Webspace, MANAGED_SERVER:vm1011, D-1000111:D-1000111 default project:separate ManagedServer, { extra: 42 })"); + "HsHostingAssetEntity(MANAGED_WEBSPACE, sec01, some Webspace, MANAGED_SERVER:vm1012, D-1000212:D-1000212 default project:separate ManagedWebspace)", + "HsHostingAssetEntity(MANAGED_WEBSPACE, fir01, some Webspace, MANAGED_SERVER:vm1011, D-1000111:D-1000111 default project:separate ManagedWebspace)", + "HsHostingAssetEntity(MANAGED_WEBSPACE, thi01, some Webspace, MANAGED_SERVER:vm1013, D-1000313:D-1000313 default project:separate ManagedWebspace)"); } @Test @@ -182,9 +189,8 @@ class HsHostingAssetRepositoryIntegrationTest extends ContextBasedTestWithCleanu // then: exactlyTheseAssetsAreReturned( result, - "HsHostingAssetEntity(MANAGED_WEBSPACE, fir01, some Webspace, MANAGED_SERVER:vm1011, D-1000111:D-1000111 default project:separate ManagedServer, { extra: 42 })", - "HsHostingAssetEntity(MANAGED_SERVER, vm1011, some ManagedServer, D-1000111:D-1000111 default project:some PrivateCloud, { extra: 42 })", - "HsHostingAssetEntity(CLOUD_SERVER, vm2011, another CloudServer, D-1000111:D-1000111 default project:some PrivateCloud, { extra: 42 })"); + "HsHostingAssetEntity(MANAGED_WEBSPACE, fir01, some Webspace, MANAGED_SERVER:vm1011, D-1000111:D-1000111 default project:separate ManagedWebspace)", + "HsHostingAssetEntity(MANAGED_SERVER, vm1011, some ManagedServer, D-1000111:D-1000111 default project:separate ManagedServer, { monit_max_cpu_usage: 90, monit_max_ram_usage: 80, monit_max_ssd_usage: 70 })"); } @Test @@ -200,7 +206,7 @@ class HsHostingAssetRepositoryIntegrationTest extends ContextBasedTestWithCleanu // then allTheseServersAreReturned( result, - "HsHostingAssetEntity(MANAGED_WEBSPACE, thi01, some Webspace, MANAGED_SERVER:vm1013, D-1000313:D-1000313 default project:separate ManagedServer, { extra: 42 })"); + "HsHostingAssetEntity(MANAGED_WEBSPACE, sec01, some Webspace, MANAGED_SERVER:vm1012, D-1000212:D-1000212 default project:separate ManagedWebspace)"); } } @@ -351,7 +357,7 @@ class HsHostingAssetRepositoryIntegrationTest extends ContextBasedTestWithCleanu private HsHostingAssetEntity givenSomeTemporaryAsset(final String projectCaption, final String identifier) { return jpaAttempt.transacted(() -> { context("superuser-alex@hostsharing.net"); - final var givenBookingItem = givenBookingItem("D-1000111 default project", "some PrivateCloud"); + final var givenBookingItem = givenBookingItem("D-1000111 default project", "test CloudServer"); final var newAsset = HsHostingAssetEntity.builder() .bookingItem(givenBookingItem) .type(CLOUD_SERVER) @@ -367,20 +373,30 @@ class HsHostingAssetRepositoryIntegrationTest extends ContextBasedTestWithCleanu } HsBookingItemEntity givenBookingItem(final String projectCaption, final String bookingItemCaption) { - final var givenProject = projectRepo.findByCaption(projectCaption).stream() - .findAny().orElseThrow(); - return bookingItemRepo.findAllByProjectUuid(givenProject.getUuid()).stream() - .filter(i -> i.getCaption().equals(bookingItemCaption)) + return bookingItemRepo.findByCaption(bookingItemCaption).stream() + .filter(i -> i.getRelatedProject().getCaption().equals(projectCaption)) .findAny().orElseThrow(); } - HsHostingAssetEntity givenManagedServer(final String projectCaption, final HsHostingAssetType type) { + HsHostingAssetEntity givenHostingAsset(final String projectCaption, final HsHostingAssetType type) { final var givenProject = projectRepo.findByCaption(projectCaption).stream() .findAny().orElseThrow(); return assetRepo.findAllByCriteria(givenProject.getUuid(), null, type).stream() .findAny().orElseThrow(); } + HsBookingItemEntity newBookingItem( + final HsBookingItemEntity parentBookingItem, + final HsBookingItemType type, + final String caption) { + final var newBookingItem = HsBookingItemEntity.builder() + .parentItem(parentBookingItem) + .type(type) + .caption(caption) + .build(); + return toCleanup(bookingItemRepo.save(newBookingItem)); + } + void exactlyTheseAssetsAreReturned( final List actualResult, final String... serverNames) { diff --git a/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsCloudServerHostingAssetValidatorUnitTest.java b/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsCloudServerHostingAssetValidatorUnitTest.java index de679c40..ee6644e0 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsCloudServerHostingAssetValidatorUnitTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsCloudServerHostingAssetValidatorUnitTest.java @@ -7,7 +7,6 @@ import java.util.Map; import static java.util.Map.entry; import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.CLOUD_SERVER; -import static net.hostsharing.hsadminng.hs.hosting.asset.validators.HsHostingAssetEntityValidators.forType; import static org.assertj.core.api.Assertions.assertThat; class HsCloudServerHostingAssetValidatorUnitTest { @@ -17,24 +16,25 @@ class HsCloudServerHostingAssetValidatorUnitTest { // given final var cloudServerHostingAssetEntity = HsHostingAssetEntity.builder() .type(CLOUD_SERVER) + .identifier("vm1234") .config(Map.ofEntries( entry("RAM", 2000) )) .build(); - final var validator = forType(cloudServerHostingAssetEntity.getType()); + final var validator = HsHostingAssetEntityValidatorRegistry.forType(cloudServerHostingAssetEntity.getType()); // when final var result = validator.validate(cloudServerHostingAssetEntity); // then - assertThat(result).containsExactly("'config.RAM' is not expected but is set to '2000'"); + assertThat(result).containsExactly("'CLOUD_SERVER:vm1234.config.RAM' is not expected but is set to '2000'"); } @Test void containsAllValidations() { // when - final var validator = forType(CLOUD_SERVER); + final var validator = HsHostingAssetEntityValidatorRegistry.forType(CLOUD_SERVER); // then assertThat(validator.properties()).map(Map::toString).isEmpty(); diff --git a/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsHostingAssetEntityValidatorsUnitTest.java b/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsHostingAssetEntityValidatorUnitTest.java similarity index 60% rename from src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsHostingAssetEntityValidatorsUnitTest.java rename to src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsHostingAssetEntityValidatorUnitTest.java index 0e07e30c..b92e5dc9 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsHostingAssetEntityValidatorsUnitTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsHostingAssetEntityValidatorUnitTest.java @@ -6,27 +6,27 @@ import org.junit.jupiter.api.Test; import jakarta.validation.ValidationException; import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.MANAGED_SERVER; -import static net.hostsharing.hsadminng.hs.hosting.asset.validators.HsHostingAssetEntityValidators.valid; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.catchThrowable; -class HsHostingAssetEntityValidatorsUnitTest { +class HsHostingAssetEntityValidatorUnitTest { @Test void validThrowsException() { // given final var managedServerHostingAssetEntity = HsHostingAssetEntity.builder() .type(MANAGED_SERVER) + .identifier("vm1234") .build(); // when - final var result = catchThrowable( ()-> valid(managedServerHostingAssetEntity) ); + final var result = catchThrowable( ()-> HsHostingAssetEntityValidatorRegistry.validated(managedServerHostingAssetEntity)); // then assertThat(result).isInstanceOf(ValidationException.class) .hasMessageContaining( - "'config.monit_max_ssd_usage' is required but missing", - "'config.monit_max_cpu_usage' is required but missing", - "'config.monit_max_ram_usage' is required but missing"); + "'MANAGED_SERVER:vm1234.config.monit_max_ssd_usage' is required but missing", + "'MANAGED_SERVER:vm1234.config.monit_max_cpu_usage' is required but missing", + "'MANAGED_SERVER:vm1234.config.monit_max_ram_usage' is required but missing"); } } diff --git a/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsManagedServerHostingAssetValidatorUnitTest.java b/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsManagedServerHostingAssetValidatorUnitTest.java index cb9e066b..b8e75436 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsManagedServerHostingAssetValidatorUnitTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsManagedServerHostingAssetValidatorUnitTest.java @@ -7,7 +7,6 @@ import java.util.Map; import static java.util.Map.entry; import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.MANAGED_SERVER; -import static net.hostsharing.hsadminng.hs.hosting.asset.validators.HsHostingAssetEntityValidators.forType; import static org.assertj.core.api.Assertions.assertThat; class HsManagedServerHostingAssetValidatorUnitTest { @@ -17,22 +16,23 @@ class HsManagedServerHostingAssetValidatorUnitTest { // given final var mangedWebspaceHostingAssetEntity = HsHostingAssetEntity.builder() .type(MANAGED_SERVER) + .identifier("vm1234") .config(Map.ofEntries( entry("monit_max_hdd_usage", "90"), entry("monit_max_cpu_usage", 2), entry("monit_max_ram_usage", 101) )) .build(); - final var validator = forType(mangedWebspaceHostingAssetEntity.getType()); + final var validator = HsHostingAssetEntityValidatorRegistry.forType(mangedWebspaceHostingAssetEntity.getType()); // when final var result = validator.validate(mangedWebspaceHostingAssetEntity); // then assertThat(result).containsExactlyInAnyOrder( - "'config.monit_max_ssd_usage' is required but missing", - "'config.monit_max_hdd_usage' is expected to be of type class java.lang.Integer, but is of type 'String'", - "'config.monit_max_cpu_usage' is expected to be >= 10 but is 2", - "'config.monit_max_ram_usage' is expected to be <= 100 but is 101"); + "'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_ssd_usage' is required but missing", + "'MANAGED_SERVER:vm1234.config.monit_max_hdd_usage' is expected to be of type class java.lang.Integer, but is of type 'String'"); } } diff --git a/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsManagedWebspaceHostingAssetValidatorUnitTest.java b/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsManagedWebspaceHostingAssetValidatorUnitTest.java index 83634501..d2e74894 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsManagedWebspaceHostingAssetValidatorUnitTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsManagedWebspaceHostingAssetValidatorUnitTest.java @@ -1,13 +1,14 @@ 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 java.util.Map.entry; -import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.MANAGED_SERVER; import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.MANAGED_WEBSPACE; import static org.assertj.core.api.Assertions.assertThat; import static net.hostsharing.hsadminng.hs.booking.project.TestHsBookingProject.TEST_PROJECT; @@ -16,21 +17,32 @@ class HsManagedWebspaceHostingAssetValidatorUnitTest { final HsBookingItemEntity managedServerBookingItem = HsBookingItemEntity.builder() .project(TEST_PROJECT) + .type(HsBookingItemType.MANAGED_SERVER) + .caption("Test Managed-Server") + .resources(Map.ofEntries( + entry("CPUs", 2), + entry("RAM", 25), + entry("SSD", 25), + entry("Traffic", 250), + entry("SLA-Platform", "EXT4H"), + entry("SLA-EMail", true) + )) .build(); final HsHostingAssetEntity mangedServerAssetEntity = HsHostingAssetEntity.builder() - .type(MANAGED_SERVER) + .type(HsHostingAssetType.MANAGED_SERVER) .bookingItem(managedServerBookingItem) + .identifier("vm1234") .config(Map.ofEntries( - entry("HDD", 0), - entry("SSD", 1), - entry("Traffic", 10) + entry("monit_max_ssd_usage", 70), + entry("monit_max_cpu_usage", 80), + entry("monit_max_ram_usage", 90) )) .build(); @Test void validatesIdentifier() { // given - final var validator = HsHostingAssetEntityValidators.forType(MANAGED_WEBSPACE); + final var validator = HsHostingAssetEntityValidatorRegistry.forType(MANAGED_WEBSPACE); final var mangedWebspaceHostingAssetEntity = HsHostingAssetEntity.builder() .type(MANAGED_WEBSPACE) .parentAsset(mangedServerAssetEntity) @@ -47,7 +59,7 @@ class HsManagedWebspaceHostingAssetValidatorUnitTest { @Test void validatesUnknownProperties() { // given - final var validator = HsHostingAssetEntityValidators.forType(MANAGED_WEBSPACE); + final var validator = HsHostingAssetEntityValidatorRegistry.forType(MANAGED_WEBSPACE); final var mangedWebspaceHostingAssetEntity = HsHostingAssetEntity.builder() .type(MANAGED_WEBSPACE) .parentAsset(mangedServerAssetEntity) @@ -61,13 +73,13 @@ class HsManagedWebspaceHostingAssetValidatorUnitTest { final var result = validator.validate(mangedWebspaceHostingAssetEntity); // then - assertThat(result).containsExactly("'config.unknown' is not expected but is set to 'some value'"); + assertThat(result).containsExactly("'MANAGED_WEBSPACE:abc00.config.unknown' is not expected but is set to 'some value'"); } @Test void validatesValidEntity() { // given - final var validator = HsHostingAssetEntityValidators.forType(MANAGED_WEBSPACE); + final var validator = HsHostingAssetEntityValidatorRegistry.forType(MANAGED_WEBSPACE); final var mangedWebspaceHostingAssetEntity = HsHostingAssetEntity.builder() .type(MANAGED_WEBSPACE) .parentAsset(mangedServerAssetEntity) diff --git a/src/test/java/net/hostsharing/hsadminng/hs/office/bankaccount/HsOfficeBankAccountRepositoryIntegrationTest.java b/src/test/java/net/hostsharing/hsadminng/hs/office/bankaccount/HsOfficeBankAccountRepositoryIntegrationTest.java index c46210c4..d7d07f69 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/office/bankaccount/HsOfficeBankAccountRepositoryIntegrationTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/office/bankaccount/HsOfficeBankAccountRepositoryIntegrationTest.java @@ -4,7 +4,7 @@ import net.hostsharing.hsadminng.context.Context; import net.hostsharing.hsadminng.rbac.test.ContextBasedTestWithCleanup; import net.hostsharing.hsadminng.rbac.rbacgrant.RawRbacGrantRepository; import net.hostsharing.hsadminng.rbac.rbacrole.RawRbacRoleRepository; -import net.hostsharing.hsadminng.rbac.test.Array; +import net.hostsharing.hsadminng.mapper.Array; import net.hostsharing.hsadminng.rbac.test.JpaAttempt; import org.apache.commons.lang3.RandomStringUtils; import org.junit.jupiter.api.Nested; diff --git a/src/test/java/net/hostsharing/hsadminng/hs/office/contact/HsOfficeContactRepositoryIntegrationTest.java b/src/test/java/net/hostsharing/hsadminng/hs/office/contact/HsOfficeContactRepositoryIntegrationTest.java index cca5c48c..4e591973 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/office/contact/HsOfficeContactRepositoryIntegrationTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/office/contact/HsOfficeContactRepositoryIntegrationTest.java @@ -4,7 +4,7 @@ import net.hostsharing.hsadminng.context.Context; import net.hostsharing.hsadminng.rbac.test.ContextBasedTestWithCleanup; import net.hostsharing.hsadminng.rbac.rbacgrant.RawRbacGrantRepository; import net.hostsharing.hsadminng.rbac.rbacrole.RawRbacRoleRepository; -import net.hostsharing.hsadminng.rbac.test.Array; +import net.hostsharing.hsadminng.mapper.Array; import net.hostsharing.hsadminng.rbac.test.JpaAttempt; import org.apache.commons.lang3.RandomStringUtils; import org.junit.jupiter.api.Nested; diff --git a/src/test/java/net/hostsharing/hsadminng/hs/office/coopassets/HsOfficeCoopAssetsTransactionRepositoryIntegrationTest.java b/src/test/java/net/hostsharing/hsadminng/hs/office/coopassets/HsOfficeCoopAssetsTransactionRepositoryIntegrationTest.java index ff6c9315..0c9215f9 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/office/coopassets/HsOfficeCoopAssetsTransactionRepositoryIntegrationTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/office/coopassets/HsOfficeCoopAssetsTransactionRepositoryIntegrationTest.java @@ -5,7 +5,7 @@ import net.hostsharing.hsadminng.hs.office.membership.HsOfficeMembershipReposito import net.hostsharing.hsadminng.rbac.test.ContextBasedTestWithCleanup; import net.hostsharing.hsadminng.rbac.rbacgrant.RawRbacGrantRepository; import net.hostsharing.hsadminng.rbac.rbacrole.RawRbacRoleRepository; -import net.hostsharing.hsadminng.rbac.test.Array; +import net.hostsharing.hsadminng.mapper.Array; import net.hostsharing.hsadminng.rbac.test.JpaAttempt; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; diff --git a/src/test/java/net/hostsharing/hsadminng/hs/office/coopshares/HsOfficeCoopSharesTransactionRepositoryIntegrationTest.java b/src/test/java/net/hostsharing/hsadminng/hs/office/coopshares/HsOfficeCoopSharesTransactionRepositoryIntegrationTest.java index 65f85b58..e6163cd4 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/office/coopshares/HsOfficeCoopSharesTransactionRepositoryIntegrationTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/office/coopshares/HsOfficeCoopSharesTransactionRepositoryIntegrationTest.java @@ -5,7 +5,7 @@ import net.hostsharing.hsadminng.hs.office.membership.HsOfficeMembershipReposito import net.hostsharing.hsadminng.rbac.test.ContextBasedTestWithCleanup; import net.hostsharing.hsadminng.rbac.rbacgrant.RawRbacGrantRepository; import net.hostsharing.hsadminng.rbac.rbacrole.RawRbacRoleRepository; -import net.hostsharing.hsadminng.rbac.test.Array; +import net.hostsharing.hsadminng.mapper.Array; import net.hostsharing.hsadminng.rbac.test.JpaAttempt; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; diff --git a/src/test/java/net/hostsharing/hsadminng/hs/office/debitor/HsOfficeDebitorRepositoryIntegrationTest.java b/src/test/java/net/hostsharing/hsadminng/hs/office/debitor/HsOfficeDebitorRepositoryIntegrationTest.java index b2e54d06..856356cf 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/office/debitor/HsOfficeDebitorRepositoryIntegrationTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/office/debitor/HsOfficeDebitorRepositoryIntegrationTest.java @@ -11,7 +11,7 @@ import net.hostsharing.hsadminng.rbac.test.ContextBasedTestWithCleanup; import net.hostsharing.hsadminng.rbac.rbacgrant.RawRbacGrantRepository; import net.hostsharing.hsadminng.rbac.rbacgrant.RbacGrantsDiagramService; import net.hostsharing.hsadminng.rbac.rbacrole.RawRbacRoleRepository; -import net.hostsharing.hsadminng.rbac.test.Array; +import net.hostsharing.hsadminng.mapper.Array; import net.hostsharing.hsadminng.rbac.test.JpaAttempt; import org.hibernate.Hibernate; import org.junit.jupiter.api.Disabled; diff --git a/src/test/java/net/hostsharing/hsadminng/hs/office/membership/HsOfficeMembershipRepositoryIntegrationTest.java b/src/test/java/net/hostsharing/hsadminng/hs/office/membership/HsOfficeMembershipRepositoryIntegrationTest.java index 1cba78da..701e6651 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/office/membership/HsOfficeMembershipRepositoryIntegrationTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/office/membership/HsOfficeMembershipRepositoryIntegrationTest.java @@ -7,7 +7,7 @@ import net.hostsharing.hsadminng.hs.office.partner.HsOfficePartnerRepository; import net.hostsharing.hsadminng.rbac.test.ContextBasedTestWithCleanup; import net.hostsharing.hsadminng.rbac.rbacgrant.RawRbacGrantRepository; import net.hostsharing.hsadminng.rbac.rbacrole.RawRbacRoleRepository; -import net.hostsharing.hsadminng.rbac.test.Array; +import net.hostsharing.hsadminng.mapper.Array; import net.hostsharing.hsadminng.rbac.test.JpaAttempt; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; diff --git a/src/test/java/net/hostsharing/hsadminng/hs/office/partner/HsOfficePartnerRepositoryIntegrationTest.java b/src/test/java/net/hostsharing/hsadminng/hs/office/partner/HsOfficePartnerRepositoryIntegrationTest.java index 39faf7eb..5daf0f8f 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/office/partner/HsOfficePartnerRepositoryIntegrationTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/office/partner/HsOfficePartnerRepositoryIntegrationTest.java @@ -30,7 +30,7 @@ import java.util.Objects; import static net.hostsharing.hsadminng.rbac.rbacgrant.RawRbacGrantEntity.distinctGrantDisplaysOf; import static net.hostsharing.hsadminng.rbac.rbacrole.RawRbacObjectEntity.objectDisplaysOf; import static net.hostsharing.hsadminng.rbac.rbacrole.RawRbacRoleEntity.distinctRoleNamesOf; -import static net.hostsharing.hsadminng.rbac.test.Array.from; +import static net.hostsharing.hsadminng.mapper.Array.from; import static net.hostsharing.hsadminng.rbac.test.JpaAttempt.attempt; import static org.assertj.core.api.Assertions.assertThat; diff --git a/src/test/java/net/hostsharing/hsadminng/hs/office/person/HsOfficePersonRepositoryIntegrationTest.java b/src/test/java/net/hostsharing/hsadminng/hs/office/person/HsOfficePersonRepositoryIntegrationTest.java index 7ce2fdf1..efd7064f 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/office/person/HsOfficePersonRepositoryIntegrationTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/office/person/HsOfficePersonRepositoryIntegrationTest.java @@ -4,7 +4,7 @@ import net.hostsharing.hsadminng.context.Context; import net.hostsharing.hsadminng.rbac.test.ContextBasedTestWithCleanup; import net.hostsharing.hsadminng.rbac.rbacgrant.RawRbacGrantRepository; import net.hostsharing.hsadminng.rbac.rbacrole.RawRbacRoleRepository; -import net.hostsharing.hsadminng.rbac.test.Array; +import net.hostsharing.hsadminng.mapper.Array; import net.hostsharing.hsadminng.rbac.test.JpaAttempt; import org.apache.commons.lang3.RandomStringUtils; import org.junit.jupiter.api.Nested; diff --git a/src/test/java/net/hostsharing/hsadminng/hs/office/relation/HsOfficeRelationRepositoryIntegrationTest.java b/src/test/java/net/hostsharing/hsadminng/hs/office/relation/HsOfficeRelationRepositoryIntegrationTest.java index fe9e2ef1..0792d656 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/office/relation/HsOfficeRelationRepositoryIntegrationTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/office/relation/HsOfficeRelationRepositoryIntegrationTest.java @@ -6,7 +6,7 @@ import net.hostsharing.hsadminng.hs.office.person.HsOfficePersonRepository; import net.hostsharing.hsadminng.rbac.test.ContextBasedTestWithCleanup; import net.hostsharing.hsadminng.rbac.rbacgrant.RawRbacGrantRepository; import net.hostsharing.hsadminng.rbac.rbacrole.RawRbacRoleRepository; -import net.hostsharing.hsadminng.rbac.test.Array; +import net.hostsharing.hsadminng.mapper.Array; import net.hostsharing.hsadminng.rbac.test.JpaAttempt; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; diff --git a/src/test/java/net/hostsharing/hsadminng/hs/office/sepamandate/HsOfficeSepaMandateRepositoryIntegrationTest.java b/src/test/java/net/hostsharing/hsadminng/hs/office/sepamandate/HsOfficeSepaMandateRepositoryIntegrationTest.java index 5544a3e3..ad7ee76e 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/office/sepamandate/HsOfficeSepaMandateRepositoryIntegrationTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/office/sepamandate/HsOfficeSepaMandateRepositoryIntegrationTest.java @@ -7,7 +7,7 @@ import net.hostsharing.hsadminng.hs.office.debitor.HsOfficeDebitorRepository; import net.hostsharing.hsadminng.rbac.test.ContextBasedTestWithCleanup; import net.hostsharing.hsadminng.rbac.rbacgrant.RawRbacGrantRepository; import net.hostsharing.hsadminng.rbac.rbacrole.RawRbacRoleRepository; -import net.hostsharing.hsadminng.rbac.test.Array; +import net.hostsharing.hsadminng.mapper.Array; import net.hostsharing.hsadminng.rbac.test.JpaAttempt; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; @@ -26,7 +26,7 @@ import java.util.List; import static net.hostsharing.hsadminng.rbac.rbacgrant.RawRbacGrantEntity.distinctGrantDisplaysOf; import static net.hostsharing.hsadminng.rbac.rbacrole.RawRbacRoleEntity.distinctRoleNamesOf; -import static net.hostsharing.hsadminng.rbac.test.Array.fromFormatted; +import static net.hostsharing.hsadminng.mapper.Array.fromFormatted; import static net.hostsharing.hsadminng.rbac.test.JpaAttempt.attempt; import static org.assertj.core.api.Assertions.assertThat; diff --git a/src/test/java/net/hostsharing/hsadminng/rbac/context/ContextIntegrationTests.java b/src/test/java/net/hostsharing/hsadminng/rbac/context/ContextIntegrationTests.java index 11cda37f..22e1df04 100644 --- a/src/test/java/net/hostsharing/hsadminng/rbac/context/ContextIntegrationTests.java +++ b/src/test/java/net/hostsharing/hsadminng/rbac/context/ContextIntegrationTests.java @@ -2,7 +2,7 @@ package net.hostsharing.hsadminng.rbac.context; import net.hostsharing.hsadminng.context.Context; import net.hostsharing.hsadminng.mapper.Mapper; -import net.hostsharing.hsadminng.rbac.test.Array; +import net.hostsharing.hsadminng.mapper.Array; import net.hostsharing.hsadminng.rbac.test.JpaAttempt; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; diff --git a/src/test/java/net/hostsharing/hsadminng/rbac/rbacrole/RbacRoleRepositoryIntegrationTest.java b/src/test/java/net/hostsharing/hsadminng/rbac/rbacrole/RbacRoleRepositoryIntegrationTest.java index 536d748c..e7a28261 100644 --- a/src/test/java/net/hostsharing/hsadminng/rbac/rbacrole/RbacRoleRepositoryIntegrationTest.java +++ b/src/test/java/net/hostsharing/hsadminng/rbac/rbacrole/RbacRoleRepositoryIntegrationTest.java @@ -1,7 +1,7 @@ package net.hostsharing.hsadminng.rbac.rbacrole; import net.hostsharing.hsadminng.context.Context; -import net.hostsharing.hsadminng.rbac.test.Array; +import net.hostsharing.hsadminng.mapper.Array; import net.hostsharing.hsadminng.rbac.test.JpaAttempt; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; diff --git a/src/test/java/net/hostsharing/hsadminng/rbac/rbacuser/RbacUserRepositoryIntegrationTest.java b/src/test/java/net/hostsharing/hsadminng/rbac/rbacuser/RbacUserRepositoryIntegrationTest.java index f1e6fef5..be6377a0 100644 --- a/src/test/java/net/hostsharing/hsadminng/rbac/rbacuser/RbacUserRepositoryIntegrationTest.java +++ b/src/test/java/net/hostsharing/hsadminng/rbac/rbacuser/RbacUserRepositoryIntegrationTest.java @@ -2,7 +2,7 @@ package net.hostsharing.hsadminng.rbac.rbacuser; import net.hostsharing.hsadminng.context.Context; import net.hostsharing.hsadminng.rbac.context.ContextBasedTest; -import net.hostsharing.hsadminng.rbac.test.Array; +import net.hostsharing.hsadminng.mapper.Array; import net.hostsharing.hsadminng.rbac.test.JpaAttempt; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test;