From d157730de7949e48a6d34cb4e3d2893de4738c28 Mon Sep 17 00:00:00 2001 From: Michael Hoennig Date: Thu, 20 Jun 2024 11:03:59 +0200 Subject: [PATCH] finalize PrivateCloud, Cloud- and ManagedServer and ManagedWebspace Billingtems and HostingAssets (#63) Co-authored-by: Michael Hoennig Reviewed-on: https://dev.hostsharing.net/hostsharing/hs.hsadmin.ng/pulls/63 Reviewed-by: Marc Sandlus --- .../errors/MultiValidationException.java | 6 +- .../HsBookingItemEntityValidator.java | 14 +- .../HsCloudServerBookingItemValidator.java | 22 ++- .../HsPrivateCloudBookingItemValidator.java | 36 +++- .../asset/HsHostingAssetController.java | 9 + .../hosting/asset/HsHostingAssetEntity.java | 4 +- .../HsHostingAssetEntityValidator.java | 6 +- .../HsManagedServerHostingAssetValidator.java | 44 ++++- .../hs/validation/EnumerationProperty.java | 22 ++- .../hs/validation/HsEntityValidator.java | 13 +- .../hs/validation/ValidatableProperty.java | 29 +++ ...oudServerBookingItemValidatorUnitTest.java | 11 +- ...gedServerBookingItemValidatorUnitTest.java | 8 +- ...vateCloudBookingItemValidatorUnitTest.java | 51 +++-- ...sHostingAssetControllerAcceptanceTest.java | 88 +++++---- ...ingAssetPropsControllerAcceptanceTest.java | 180 ++++++++++++++++-- ...HsHostingAssetEntityValidatorUnitTest.java | 7 +- ...edServerHostingAssetValidatorUnitTest.java | 1 - ...fficeDebitorRepositoryIntegrationTest.java | 2 +- .../test/ContextBasedTestWithCleanup.java | 35 +++- 20 files changed, 458 insertions(+), 130 deletions(-) diff --git a/src/main/java/net/hostsharing/hsadminng/errors/MultiValidationException.java b/src/main/java/net/hostsharing/hsadminng/errors/MultiValidationException.java index 9a6d459d..a6ba69e8 100644 --- a/src/main/java/net/hostsharing/hsadminng/errors/MultiValidationException.java +++ b/src/main/java/net/hostsharing/hsadminng/errors/MultiValidationException.java @@ -8,7 +8,11 @@ import static java.lang.String.join; public class MultiValidationException extends ValidationException { private MultiValidationException(final List violations) { - super("[\n" + join(",\n", violations) + "\n]"); + super( + violations.size() > 1 + ? "[\n" + join(",\n", violations) + "\n]" + : "[" + join(",\n", violations) + "]" + ); } public static void throwInvalid(final List violations) { 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 index 7d002bac..ee07e981 100644 --- 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 @@ -3,6 +3,7 @@ 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 org.apache.commons.lang3.BooleanUtils; import java.util.Collection; import java.util.List; @@ -59,19 +60,24 @@ public class HsBookingItemEntityValidator extends HsEntityValidator propDef.getValue(subItem.getResources())) - .map(HsBookingItemEntityValidator::toNonNullInteger) + .map(HsBookingItemEntityValidator::convertBooleanToInteger) + .map(HsBookingItemEntityValidator::toIntegerWithDefault0) .reduce(0, Integer::sum); - final var maxValue = getNonNullIntegerValue(propDef, bookingItem.getResources()); + final var maxValue = getIntegerValueWithDefault0(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%%" + ? "%s' maximum total is %d%s, but actual total %s is %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" + ? "%s' maximum total is %d%s, but actual total %s is %d%s" .formatted(propName, maxValue, propUnit, propName, totalValue, propUnit) : null; } } + + private static Object convertBooleanToInteger(final Object value) { + return value instanceof Boolean ? BooleanUtils.toInteger((Boolean)value) : value; + } } 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 07bb80da..d673f01a 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,7 +1,6 @@ package net.hostsharing.hsadminng.hs.booking.item.validators; - - +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; @@ -9,12 +8,21 @@ 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).withDefault(0), - integerProperty("Traffic").unit("GB").min(250).max(10000).step(250).required(), + // @formatter:off + booleanProperty("active") .withDefault(true), + + integerProperty("CPUs") .min( 1) .max( 32) .required(), + integerProperty("RAM").unit("GB") .min( 1) .max( 128) .required(), + integerProperty("SSD").unit("GB") .min( 0) .max( 1000) .step(25).required(), // (1) + 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() + // @formatter:on ); + + // (q) We do have pre-existing CloudServers without SSD, just HDD, thus SSD starts with min=0. + // TODO.impl: Validation that SSD+HDD is at minimum 25 GB is missing. + // e.g. validationGroup("SSD", "HDD").min(0); } } 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 index 317f2f0c..236a000a 100644 --- 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 @@ -1,18 +1,40 @@ 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") + // @formatter:off + integerProperty("CPUs") .min( 1).max( 128).required().asTotalLimit(), + integerProperty("RAM").unit("GB") .min( 1).max( 512).required().asTotalLimit(), + integerProperty("SSD").unit("GB") .min( 25).max( 4000).step(25).required().asTotalLimit(), + integerProperty("HDD").unit("GB") .min( 0).max(16000).step(250).withDefault(0).asTotalLimit(), + integerProperty("Traffic").unit("GB") .min(250).max(40000).step(250).required().asTotalLimit(), + +// Alternatively we could specify it similarly to "Multi" option but exclusively counting: +// integerProperty("Resource-Points") .min(4).max(100).required() +// .each("CPUs").countsAs(64) +// .each("RAM").countsAs(64) +// .each("SSD").countsAs(18) +// .each("HDD").countsAs(2) +// .each("Traffic").countsAs(1), + + integerProperty("SLA-Infrastructure EXT8H") .min( 0).max( 20).withDefault(0).asTotalLimitFor("SLA-Infrastructure", "EXT8H"), + integerProperty("SLA-Infrastructure EXT4H") .min( 0).max( 20).withDefault(0).asTotalLimitFor("SLA-Infrastructure", "EXT4H"), + integerProperty("SLA-Infrastructure EXT2H") .min( 0).max( 20).withDefault(0).asTotalLimitFor("SLA-Infrastructure", "EXT2H"), + + integerProperty("SLA-Platform EXT8H") .min( 0).max( 20).withDefault(0).asTotalLimitFor("SLA-Platform", "EXT8H"), + integerProperty("SLA-Platform EXT4H") .min( 0).max( 20).withDefault(0).asTotalLimitFor("SLA-Platform", "EXT4H"), + integerProperty("SLA-Platform EXT2H") .min( 0).max( 20).withDefault(0).asTotalLimitFor("SLA-Platform", "EXT2H"), + + integerProperty("SLA-EMail") .min( 0).max( 20).withDefault(0).asTotalLimit(), + integerProperty("SLA-Maria") .min( 0).max( 20).withDefault(0).asTotalLimit(), + integerProperty("SLA-PgSQL") .min( 0).max( 20).withDefault(0).asTotalLimit(), + integerProperty("SLA-Office") .min( 0).max( 20).withDefault(0).asTotalLimit(), + integerProperty("SLA-Web") .min( 0).max( 20).withDefault(0).asTotalLimit() + // @formatter:on ); } } 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 76003671..d3578833 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 @@ -1,5 +1,6 @@ package net.hostsharing.hsadminng.hs.hosting.asset; +import net.hostsharing.hsadminng.hs.booking.item.HsBookingItemRepository; import net.hostsharing.hsadminng.hs.hosting.generated.api.v1.api.HsHostingAssetsApi; import net.hostsharing.hsadminng.context.Context; @@ -34,6 +35,9 @@ public class HsHostingAssetController implements HsHostingAssetsApi { @Autowired private HsHostingAssetRepository assetRepo; + @Autowired + private HsBookingItemRepository bookingItemRepo; + @Override @Transactional(readOnly = true) public ResponseEntity> listAssets( @@ -124,6 +128,11 @@ public class HsHostingAssetController implements HsHostingAssetsApi { final BiConsumer RESOURCE_TO_ENTITY_POSTMAPPER = (resource, entity) -> { entity.putConfig(KeyValueMap.from(resource.getConfig())); + if (resource.getBookingItemUuid() != null) { + entity.setBookingItem(bookingItemRepo.findByUuid(resource.getBookingItemUuid()) + .orElseThrow(() -> new EntityNotFoundException("ERROR: [400] bookingItemUuid %s not found".formatted( + resource.getBookingItemUuid())))); + } if (resource.getParentAssetUuid() != null) { entity.setParentAsset(assetRepo.findByUuid(resource.getParentAssetUuid()) .orElseThrow(() -> new EntityNotFoundException("ERROR: [400] parentAssetUuid %s not found".formatted( 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 164e42d0..fa15537a 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 @@ -27,6 +27,7 @@ import jakarta.persistence.Id; import jakarta.persistence.JoinColumn; import jakarta.persistence.ManyToOne; import jakarta.persistence.OneToMany; +import jakarta.persistence.OneToOne; import jakarta.persistence.Table; import jakarta.persistence.Transient; import jakarta.persistence.Version; @@ -78,7 +79,7 @@ public class HsHostingAssetEntity implements Stringifyable, RbacObject { @Version private int version; - @ManyToOne(fetch = FetchType.LAZY) + @OneToOne(fetch = FetchType.LAZY) @JoinColumn(name = "bookingitemuuid") private HsBookingItemEntity bookingItem; @@ -142,7 +143,6 @@ public class HsHostingAssetEntity implements Stringifyable, RbacObject { dependsOnColumn("bookingItemUuid"), directlyFetchedByDependsOnColumn(), NULLABLE) - .toRole("bookingItem", AGENT).grantPermission(INSERT) .importEntityAlias("parentAsset", HsHostingAssetEntity.class, usingDefaultCase(), dependsOnColumn("parentAssetUuid"), 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 index 3a0438ee..c452d378 100644 --- 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 @@ -65,11 +65,11 @@ public class HsHostingAssetEntityValidator extends HsEntityValidator propDef.getValue(subItem.getConfig())) - .map(HsEntityValidator::toNonNullInteger) + .map(HsEntityValidator::toIntegerWithDefault0) .reduce(0, Integer::sum); - final var maxValue = getNonNullIntegerValue(propDef, hostingAsset.getConfig()); + final var maxValue = getIntegerValueWithDefault0(propDef, hostingAsset.getConfig()); return totalValue > maxValue - ? "%s' maximum total is %d%s, but actual total is %s %d%s".formatted( + ? "%s' maximum total is %d%s, but actual total %s is %d%s".formatted( propName, maxValue, propUnit, propName, totalValue, propUnit) : null; } 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 b2107866..00050010 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,18 +1,48 @@ package net.hostsharing.hsadminng.hs.hosting.asset.validators; +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 HsManagedServerHostingAssetValidator extends HsHostingAssetEntityValidator { public HsManagedServerHostingAssetValidator() { super( - integerProperty("monit_min_free_ssd").min(1).max(1000).optional(), - integerProperty("monit_min_free_hdd").min(1).max(4000).optional(), - integerProperty("monit_max_ssd_usage").unit("%").min(10).max(100).required(), - integerProperty("monit_max_hdd_usage").unit("%").min(10).max(100).optional(), - integerProperty("monit_max_cpu_usage").unit("%").min(10).max(100).required(), - integerProperty("monit_max_ram_usage").unit("%").min(10).max(100).required() - // TODO: stringProperty("monit_alarm_email").unit("GB").optional() + // monitoring + integerProperty("monit_max_cpu_usage").unit("%").min(10).max(100).withDefault(92), + integerProperty("monit_max_ram_usage").unit("%").min(10).max(100).withDefault(92), + integerProperty("monit_max_ssd_usage").unit("%").min(10).max(100).withDefault(98), + integerProperty("monit_min_free_ssd").min(1).max(1000).withDefault(5), + integerProperty("monit_max_hdd_usage").unit("%").min(10).max(100).withDefault(95), + integerProperty("monit_min_free_hdd").min(1).max(4000).withDefault(10), + // stringProperty("monit_alarm_email").unit("GB").optional() TODO.impl: via Contact? + + // other settings + // booleanProperty("fastcgi_small").withDefault(false), TODO.spec: clarify Salt-Grains + + // database software + booleanProperty("software-pgsql").withDefault(true), + booleanProperty("software-mariadb").withDefault(true), + + // PHP + enumerationProperty("php-default").valuesFromProperties("software-php-").withDefault("8.2"), + booleanProperty("software-php-5.6").withDefault(false), + booleanProperty("software-php-7.0").withDefault(false), + booleanProperty("software-php-7.1").withDefault(false), + booleanProperty("software-php-7.2").withDefault(false), + booleanProperty("software-php-7.3").withDefault(false), + booleanProperty("software-php-7.4").withDefault(true), + booleanProperty("software-php-8.0").withDefault(false), + booleanProperty("software-php-8.1").withDefault(false), + booleanProperty("software-php-8.2").withDefault(true), + + // other software + booleanProperty("software-postfix-tls-1.0").withDefault(false), + booleanProperty("software-dovecot-tls-1.0").withDefault(false), + booleanProperty("software-clamav").withDefault(true), + booleanProperty("software-collabora").withDefault(false), + booleanProperty("software-libreoffice").withDefault(false), + booleanProperty("software-imagemagick-ghostscript").withDefault(false) ); } } diff --git a/src/main/java/net/hostsharing/hsadminng/hs/validation/EnumerationProperty.java b/src/main/java/net/hostsharing/hsadminng/hs/validation/EnumerationProperty.java index 23e5ef61..923d7ae1 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/validation/EnumerationProperty.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/validation/EnumerationProperty.java @@ -7,6 +7,8 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Map; +import static java.util.Arrays.stream; + @Setter public class EnumerationProperty extends ValidatableProperty { @@ -30,9 +32,27 @@ public class EnumerationProperty extends ValidatableProperty { return this; } + public void deferredInit(final ValidatableProperty[] allProperties) { + if (deferredInit != null) { + if (this.values != null) { + throw new IllegalStateException("property " + toString() + " already has values"); + } + this.values = deferredInit.apply(allProperties); + } + } + + public ValidatableProperty valuesFromProperties(final String propertyNamePrefix) { + this.deferredInit = (ValidatableProperty[] allProperties) -> stream(allProperties) + .map(ValidatableProperty::propertyName) + .filter(name -> name.startsWith(propertyNamePrefix)) + .map(name -> name.substring(propertyNamePrefix.length())) + .toArray(String[]::new); + return this; + } + @Override protected void validate(final ArrayList result, final String propValue, final Map props) { - if (Arrays.stream(values).noneMatch(v -> v.equals(propValue))) { + if (stream(values).noneMatch(v -> v.equals(propValue))) { result.add(propertyName + "' is expected to be one of " + Arrays.toString(values) + " but is '" + propValue + "'"); } } 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 c06ed140..4c20f2a5 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/validation/HsEntityValidator.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/validation/HsEntityValidator.java @@ -16,6 +16,7 @@ public abstract class HsEntityValidator { public HsEntityValidator(final ValidatableProperty... validators) { propertyValidators = validators; + stream(propertyValidators).forEach(p -> p.deferredInit(propertyValidators)); } protected static List enrich(final String prefix, final List messages) { @@ -59,18 +60,24 @@ public abstract class HsEntityValidator { .orElse(emptyList())); } - protected static Integer getNonNullIntegerValue(final ValidatableProperty prop, final Map propValues) { + protected static Integer getIntegerValueWithDefault0(final ValidatableProperty prop, final Map propValues) { final var value = prop.getValue(propValues); if (value instanceof Integer) { return (Integer) value; } + if (value == null) { + return 0; + } throw new IllegalArgumentException(prop.propertyName + " Integer value expected, but got " + value); } - protected static Integer toNonNullInteger(final Object value) { + protected static Integer toIntegerWithDefault0(final Object value) { if (value instanceof Integer) { return (Integer) value; } - throw new IllegalArgumentException("Integer value expected, but got " + value); + if (value == null) { + return 0; + } + throw new IllegalArgumentException("Integer value (or null) expected, but got " + value); } } diff --git a/src/main/java/net/hostsharing/hsadminng/hs/validation/ValidatableProperty.java b/src/main/java/net/hostsharing/hsadminng/hs/validation/ValidatableProperty.java index 7795d47d..3b0bb099 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/validation/ValidatableProperty.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/validation/ValidatableProperty.java @@ -19,6 +19,7 @@ import java.util.function.Function; import static java.lang.Boolean.FALSE; import static java.lang.Boolean.TRUE; import static java.util.Collections.emptyList; +import static java.util.Optional.ofNullable; @RequiredArgsConstructor public abstract class ValidatableProperty { @@ -31,6 +32,7 @@ public abstract class ValidatableProperty { private final String[] keyOrder; private Boolean required; private T defaultValue; + protected Function[], T[]> deferredInit; private boolean isTotalsValidator = false; @JsonIgnore private List>> asTotalLimitValidators; // TODO.impl: move to BookingItemIntegerProperty @@ -57,11 +59,38 @@ public abstract class ValidatableProperty { return this; } + public void deferredInit(final ValidatableProperty[] allProperties) { + } + public ValidatableProperty asTotalLimit() { isTotalsValidator = true; return this; } + public ValidatableProperty asTotalLimitFor(final String propertyName, final String propertyValue) { + if (asTotalLimitValidators == null) { + asTotalLimitValidators = new ArrayList<>(); + } + final TriFunction> validator = + (final HsBookingItemEntity entity, final IntegerProperty prop, final Integer factor) -> { + + final var total = entity.getSubBookingItems().stream() + .map(server -> server.getResources().get(propertyName)) + .filter(propertyValue::equals) + .count(); + + final long limitingValue = ofNullable(prop.getValue(entity.getResources())).orElse(0); + if (total > factor*limitingValue) { + return List.of( + prop.propertyName() + " maximum total is " + (factor*limitingValue) + ", but actual total for " + propertyName + "=" + propertyValue + " is " + total + ); + } + return emptyList(); + }; + asTotalLimitValidators.add((final HsBookingItemEntity entity) -> validator.apply(entity, (IntegerProperty)this, 1)); + return this; + } + public String propertyName() { return propertyName; } 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 787b4c08..9258a4a1 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 @@ -55,9 +55,10 @@ class HsCloudServerBookingItemValidatorUnitTest { // then assertThat(validator.properties()).map(Map::toString).containsExactlyInAnyOrder( + "{type=boolean, propertyName=active, required=false, defaultValue=true, isTotalsValidator=false}", "{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=SSD, unit=GB, min=0, 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}"); @@ -109,10 +110,10 @@ class HsCloudServerBookingItemValidatorUnitTest { // 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" + "'D-12345:Test-Project:Test Cloud.resources.CPUs' maximum total is 4, but actual total CPUs is 5", + "'D-12345:Test-Project:Test Cloud.resources.RAM' maximum total is 20 GB, but actual total RAM is 30 GB", + "'D-12345:Test-Project:Test Cloud.resources.SSD' maximum total is 100 GB, but actual total SSD is 150 GB", + "'D-12345:Test-Project:Test Cloud.resources.Traffic' maximum total is 5000 GB, but actual total Traffic is 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 549b5700..1fe754ea 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 @@ -120,10 +120,10 @@ class HsManagedServerBookingItemValidatorUnitTest { // 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" + "'D-12345:Test-Project:null.resources.CPUs' maximum total is 4, but actual total CPUs is 5", + "'D-12345:Test-Project:null.resources.RAM' maximum total is 20 GB, but actual total RAM is 30 GB", + "'D-12345:Test-Project:null.resources.SSD' maximum total is 100 GB, but actual total SSD is 150 GB", + "'D-12345:Test-Project:null.resources.Traffic' maximum total is 5000 GB, but actual total Traffic is 5500 GB" ); } 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 index 5079f340..2a100d2c 100644 --- 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 @@ -28,29 +28,38 @@ class HsPrivateCloudBookingItemValidatorUnitTest { // given final var privateCloudBookingItemEntity = HsBookingItemEntity.builder() .type(PRIVATE_CLOUD) + .caption("myPC") .resources(ofEntries( entry("CPUs", 4), entry("RAM", 20), entry("SSD", 100), - entry("Traffic", 5000) + entry("Traffic", 5000), + entry("SLA-Platform EXT4H", 2), + entry("SLA-EMail", 2) )) .subBookingItems(of( HsBookingItemEntity.builder() .type(MANAGED_SERVER) + .caption("myMS-1") .resources(ofEntries( entry("CPUs", 2), entry("RAM", 10), entry("SSD", 50), - entry("Traffic", 2500) + entry("Traffic", 2500), + entry("SLA-Platform", "EXT4H"), + entry("SLA-EMail", true) )) .build(), HsBookingItemEntity.builder() .type(CLOUD_SERVER) + .caption("myMS-2") .resources(ofEntries( entry("CPUs", 2), entry("RAM", 10), entry("SSD", 50), - entry("Traffic", 2500) + entry("Traffic", 2500), + entry("SLA-Platform", "EXT4H"), + entry("SLA-EMail", true) )) .build() )) @@ -69,29 +78,42 @@ class HsPrivateCloudBookingItemValidatorUnitTest { final var privateCloudBookingItemEntity = HsBookingItemEntity.builder() .project(project) .type(PRIVATE_CLOUD) + .caption("myPC") .resources(ofEntries( entry("CPUs", 4), entry("RAM", 20), entry("SSD", 100), - entry("Traffic", 5000) + entry("Traffic", 5000), + entry("SLA-Platform EXT2H", 1), + entry("SLA-EMail", 1) )) .subBookingItems(of( HsBookingItemEntity.builder() .type(MANAGED_SERVER) + .caption("myMS-1") .resources(ofEntries( entry("CPUs", 3), entry("RAM", 20), entry("SSD", 100), - entry("Traffic", 3000) + entry("Traffic", 3000), + entry("SLA-Platform", "EXT2H"), + entry("SLA-EMail", true) )) .build(), HsBookingItemEntity.builder() .type(CLOUD_SERVER) + .caption("myMS-2") .resources(ofEntries( entry("CPUs", 2), entry("RAM", 10), entry("SSD", 50), - entry("Traffic", 2500) + entry("Traffic", 2500), + entry("SLA-Platform", "EXT2H"), + entry("SLA-EMail", true), + entry("SLA-Maria", true), + entry("SLA-PgSQL", true), + entry("SLA-Office", true), + entry("SLA-Web", true) )) .build() )) @@ -102,11 +124,16 @@ class HsPrivateCloudBookingItemValidatorUnitTest { // 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" - ); + "'D-12345:Test-Project:myPC.resources.CPUs' maximum total is 4, but actual total CPUs is 5", + "'D-12345:Test-Project:myPC.resources.RAM' maximum total is 20 GB, but actual total RAM is 30 GB", + "'D-12345:Test-Project:myPC.resources.SSD' maximum total is 100 GB, but actual total SSD is 150 GB", + "'D-12345:Test-Project:myPC.resources.Traffic' maximum total is 5000 GB, but actual total Traffic is 5500 GB", + "'D-12345:Test-Project:myPC.resources.SLA-Platform EXT2H maximum total is 1, but actual total for SLA-Platform=EXT2H is 2", + "'D-12345:Test-Project:myPC.resources.SLA-EMail' maximum total is 1, but actual total SLA-EMail is 2", + "'D-12345:Test-Project:myPC.resources.SLA-Maria' maximum total is 0, but actual total SLA-Maria is 1", + "'D-12345:Test-Project:myPC.resources.SLA-PgSQL' maximum total is 0, but actual total SLA-PgSQL is 1", + "'D-12345:Test-Project:myPC.resources.SLA-Office' maximum total is 0, but actual total SLA-Office is 1", + "'D-12345:Test-Project:myPC.resources.SLA-Web' maximum total is 0, but actual total SLA-Web is 1" + ); } - } 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 2ea554c6..84fe1627 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 @@ -10,8 +10,11 @@ import net.hostsharing.hsadminng.hs.booking.project.HsBookingProjectRepository; import net.hostsharing.hsadminng.hs.office.debitor.HsOfficeDebitorRepository; import net.hostsharing.hsadminng.rbac.test.ContextBasedTestWithCleanup; import net.hostsharing.hsadminng.rbac.test.JpaAttempt; +import org.junit.jupiter.api.ClassOrderer; import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Order; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestClassOrder; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.web.server.LocalServerPort; @@ -28,11 +31,12 @@ import static net.hostsharing.hsadminng.rbac.test.JsonMatcher.lenientlyEquals; import static org.assertj.core.api.Assertions.assertThat; import static org.hamcrest.Matchers.matchesRegex; +@Transactional @SpringBootTest( webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, classes = { HsadminNgApplication.class, JpaAttempt.class } ) -@Transactional +@TestClassOrder(ClassOrderer.OrderAnnotation.class) // fail early on fetching problems class HsHostingAssetControllerAcceptanceTest extends ContextBasedTestWithCleanup { @LocalServerPort @@ -54,6 +58,7 @@ class HsHostingAssetControllerAcceptanceTest extends ContextBasedTestWithCleanup JpaAttempt jpaAttempt; @Nested + @Order(2) class ListAssets { @Test @@ -152,6 +157,7 @@ class HsHostingAssetControllerAcceptanceTest extends ContextBasedTestWithCleanup } @Nested + @Order(3) class AddAsset { @Test @@ -231,17 +237,17 @@ class HsHostingAssetControllerAcceptanceTest extends ContextBasedTestWithCleanup .when() .post("http://localhost/api/hs/hosting/assets") .then().log().all().assertThat() - .statusCode(201) - .contentType(ContentType.JSON) - .body("", lenientlyEquals(""" - { - "type": "MANAGED_WEBSPACE", - "identifier": "fir90", - "caption": "some new ManagedWebspace in client's ManagedServer", - "config": {} - } - """)) - .header("Location", matchesRegex("http://localhost:[1-9][0-9]*/api/hs/hosting/assets/[^/]*")) + .statusCode(201) + .contentType(ContentType.JSON) + .body("", lenientlyEquals(""" + { + "type": "MANAGED_WEBSPACE", + "identifier": "fir90", + "caption": "some new ManagedWebspace in client's ManagedServer", + "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 @@ -258,34 +264,33 @@ class HsHostingAssetControllerAcceptanceTest extends ContextBasedTestWithCleanup RestAssured // @formatter:off .given() - .header("current-user", "superuser-alex@hostsharing.net") - .contentType(ContentType.JSON) - .body(""" - { - "bookingItemUuid": "%s", - "type": "MANAGED_SERVER", - "identifier": "vm1400", - "caption": "some new ManagedServer", - "config": { "monit_max_ssd_usage": 0, "monit_max_cpu_usage": 101, "extra": 42 } - } - """.formatted(givenBookingItem.getUuid())) - .port(port) + .header("current-user", "superuser-alex@hostsharing.net") + .contentType(ContentType.JSON) + .body(""" + { + "bookingItemUuid": "%s", + "type": "MANAGED_SERVER", + "identifier": "vm1400", + "caption": "some new ManagedServer", + "config": { "monit_max_ssd_usage": 0, "monit_max_cpu_usage": 101, "extra": 42 } + } + """.formatted(givenBookingItem.getUuid())) + .port(port) .when() - .post("http://localhost/api/hs/hosting/assets") + .post("http://localhost/api/hs/hosting/assets") .then().log().all().assertThat() - .statusCode(400) - .contentType(ContentType.JSON) - .body("", lenientlyEquals(""" - { - "statusPhrase": "Bad Request", - "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 - <<<]" - } - """.replaceAll(" +<<<", ""))); // @formatter:on + .statusCode(400) + .contentType(ContentType.JSON) + .body("", lenientlyEquals(""" + { + "statusPhrase": "Bad Request", + "message": "[ + <<<'MANAGED_SERVER:vm1400.config.extra' is not expected but is set to '42', + <<<'MANAGED_SERVER:vm1400.config.monit_max_cpu_usage' is expected to be <= 100 but is 101, + <<<'MANAGED_SERVER:vm1400.config.monit_max_ssd_usage' is expected to be >= 10 but is 0 + <<<]" + } + """.replaceAll(" +<<<", ""))); // @formatter:on } @@ -333,15 +338,14 @@ class HsHostingAssetControllerAcceptanceTest extends ContextBasedTestWithCleanup .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 - <<<]" + "message": "['D-1000111:D-1000111 default project:separate ManagedWebspace.resources.Multi=1 allows at maximum 25 unix users, but 26 found]" } """.replaceAll(" +<<<", ""))); // @formatter:on } } @Nested + @Order(1) class GetAsset { @Test @@ -413,6 +417,7 @@ class HsHostingAssetControllerAcceptanceTest extends ContextBasedTestWithCleanup } @Nested + @Order(4) class PatchAsset { @Test @@ -466,6 +471,7 @@ class HsHostingAssetControllerAcceptanceTest extends ContextBasedTestWithCleanup } @Nested + @Order(5) class DeleteAsset { @Test 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 e8195eeb..9a04c9b4 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 @@ -55,18 +55,22 @@ class HsHostingAssetPropsControllerAcceptanceTest { [ { "type": "integer", - "propertyName": "monit_min_free_ssd", - "min": 1, - "max": 1000, + "propertyName": "monit_max_cpu_usage", + "unit": "%", + "min": 10, + "max": 100, "required": false, + "defaultValue": 92, "isTotalsValidator": false }, { "type": "integer", - "propertyName": "monit_min_free_hdd", - "min": 1, - "max": 4000, + "propertyName": "monit_max_ram_usage", + "unit": "%", + "min": 10, + "max": 100, "required": false, + "defaultValue": 92, "isTotalsValidator": false }, { @@ -75,7 +79,17 @@ class HsHostingAssetPropsControllerAcceptanceTest { "unit": "%", "min": 10, "max": 100, - "required": true, + "required": false, + "defaultValue": 98, + "isTotalsValidator": false + }, + { + "type": "integer", + "propertyName": "monit_min_free_ssd", + "min": 1, + "max": 1000, + "required": false, + "defaultValue": 5, "isTotalsValidator": false }, { @@ -85,29 +99,157 @@ class HsHostingAssetPropsControllerAcceptanceTest { "min": 10, "max": 100, "required": false, + "defaultValue": 95, "isTotalsValidator": false }, { "type": "integer", - "propertyName": "monit_max_cpu_usage", - "unit": "%", - "min": 10, - "max": 100, - "required": true, + "propertyName": "monit_min_free_hdd", + "min": 1, + "max": 4000, + "required": false, + "defaultValue": 10, "isTotalsValidator": false }, { - "type": "integer", - "propertyName": "monit_max_ram_usage", - "unit": "%", - "min": 10, - "max": 100, - "required": true, + "type": "boolean", + "propertyName": "software-pgsql", + "required": false, + "defaultValue": true, + "isTotalsValidator": false + }, + { + "type": "boolean", + "propertyName": "software-mariadb", + "required": false, + "defaultValue": true, + "isTotalsValidator": false + }, + { + "type": "enumeration", + "propertyName": "php-default", + "values": [ + "5.6", + "7.0", + "7.1", + "7.2", + "7.3", + "7.4", + "8.0", + "8.1", + "8.2" + ], + "required": false, + "defaultValue": "8.2", + "isTotalsValidator": false + }, + { + "type": "boolean", + "propertyName": "software-php-5.6", + "required": false, + "defaultValue": false, + "isTotalsValidator": false + }, + { + "type": "boolean", + "propertyName": "software-php-7.0", + "required": false, + "defaultValue": false, + "isTotalsValidator": false + }, + { + "type": "boolean", + "propertyName": "software-php-7.1", + "required": false, + "defaultValue": false, + "isTotalsValidator": false + }, + { + "type": "boolean", + "propertyName": "software-php-7.2", + "required": false, + "defaultValue": false, + "isTotalsValidator": false + }, + { + "type": "boolean", + "propertyName": "software-php-7.3", + "required": false, + "defaultValue": false, + "isTotalsValidator": false + }, + { + "type": "boolean", + "propertyName": "software-php-7.4", + "required": false, + "defaultValue": true, + "isTotalsValidator": false + }, + { + "type": "boolean", + "propertyName": "software-php-8.0", + "required": false, + "defaultValue": false, + "isTotalsValidator": false + }, + { + "type": "boolean", + "propertyName": "software-php-8.1", + "required": false, + "defaultValue": false, + "isTotalsValidator": false + }, + { + "type": "boolean", + "propertyName": "software-php-8.2", + "required": false, + "defaultValue": true, + "isTotalsValidator": false + }, + { + "type": "boolean", + "propertyName": "software-postfix-tls-1.0", + "required": false, + "defaultValue": false, + "isTotalsValidator": false + }, + { + "type": "boolean", + "propertyName": "software-dovecot-tls-1.0", + "required": false, + "defaultValue": false, + "isTotalsValidator": false + }, + { + "type": "boolean", + "propertyName": "software-clamav", + "required": false, + "defaultValue": true, + "isTotalsValidator": false + }, + { + "type": "boolean", + "propertyName": "software-collabora", + "required": false, + "defaultValue": false, + "isTotalsValidator": false + }, + { + "type": "boolean", + "propertyName": "software-libreoffice", + "required": false, + "defaultValue": false, + "isTotalsValidator": false + }, + { + "type": "boolean", + "propertyName": "software-imagemagick-ghostscript", + "required": false, + "defaultValue": false, "isTotalsValidator": false } ] """)); // @formatter:on } - } diff --git a/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsHostingAssetEntityValidatorUnitTest.java b/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsHostingAssetEntityValidatorUnitTest.java index b92e5dc9..ddceba8e 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsHostingAssetEntityValidatorUnitTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsHostingAssetEntityValidatorUnitTest.java @@ -3,7 +3,6 @@ package net.hostsharing.hsadminng.hs.hosting.asset.validators; import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetEntity; import org.junit.jupiter.api.Test; -import jakarta.validation.ValidationException; import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.MANAGED_SERVER; import static org.assertj.core.api.Assertions.assertThat; @@ -23,10 +22,6 @@ class HsHostingAssetEntityValidatorUnitTest { final var result = catchThrowable( ()-> HsHostingAssetEntityValidatorRegistry.validated(managedServerHostingAssetEntity)); // then - assertThat(result).isInstanceOf(ValidationException.class) - .hasMessageContaining( - "'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"); + assertThat(result).isNull(); // all required properties have defaults } } 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 b8e75436..d22ef590 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 @@ -32,7 +32,6 @@ class HsManagedServerHostingAssetValidatorUnitTest { assertThat(result).containsExactlyInAnyOrder( "'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/office/debitor/HsOfficeDebitorRepositoryIntegrationTest.java b/src/test/java/net/hostsharing/hsadminng/hs/office/debitor/HsOfficeDebitorRepositoryIntegrationTest.java index 856356cf..dc1b3f61 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 @@ -109,9 +109,9 @@ class HsOfficeDebitorRepositoryIntegrationTest extends ContextBasedTestWithClean assertThat(debitorRepo.count()).isEqualTo(count + 1); } + @Transactional @ParameterizedTest @ValueSource(strings = {"", "a", "ab", "a12", "123", "12a"}) - @Transactional public void canNotCreateNewDebitorWithInvalidDefaultPrefix(final String givenPrefix) { // given context("superuser-alex@hostsharing.net"); diff --git a/src/test/java/net/hostsharing/hsadminng/rbac/test/ContextBasedTestWithCleanup.java b/src/test/java/net/hostsharing/hsadminng/rbac/test/ContextBasedTestWithCleanup.java index 6c9ac849..5e9d8347 100644 --- a/src/test/java/net/hostsharing/hsadminng/rbac/test/ContextBasedTestWithCleanup.java +++ b/src/test/java/net/hostsharing/hsadminng/rbac/test/ContextBasedTestWithCleanup.java @@ -14,9 +14,12 @@ import org.junit.jupiter.api.TestInfo; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.Repository; +import org.springframework.transaction.PlatformTransactionManager; import jakarta.persistence.*; import java.util.*; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.function.Supplier; import static java.lang.System.out; import static java.util.Comparator.comparing; @@ -28,9 +31,13 @@ import static org.assertj.core.api.Assertions.assertThat; public abstract class ContextBasedTestWithCleanup extends ContextBasedTest { private static final boolean DETAILED_BUT_SLOW_CHECK = true; + @PersistenceContext protected EntityManager em; + @Autowired + private PlatformTransactionManager tm; + @Autowired RbacGrantRepository rbacGrantRepo; @@ -166,12 +173,16 @@ public abstract class ContextBasedTestWithCleanup extends ContextBasedTest { @AfterEach void cleanupAndCheckCleanup(final TestInfo testInfo) { - out.println(ContextBasedTestWithCleanup.class.getSimpleName() + ".cleanupAndCheckCleanup"); - cleanupTemporaryTestData(); - deleteLeakedRbacObjects(); - long rbacObjectCount = assertNoNewRbacObjectsRolesAndGrantsLeaked(); + // If the whole test method has its own transaction, cleanup makes no sense. + // If that transaction even failed, cleaunup would cause an exception. + if (!tm.getTransaction(null).isRollbackOnly()) { + out.println(ContextBasedTestWithCleanup.class.getSimpleName() + ".cleanupAndCheckCleanup"); + cleanupTemporaryTestData(); + repeatUntilTrue(3, this::deleteLeakedRbacObjects); - out.println("TOTAL OBJECT COUNT (after): " + rbacObjectCount); + long rbacObjectCount = assertNoNewRbacObjectsRolesAndGrantsLeaked(); + out.println("TOTAL OBJECT COUNT (after): " + rbacObjectCount); + } } private void cleanupTemporaryTestData() { @@ -218,7 +229,8 @@ public abstract class ContextBasedTestWithCleanup extends ContextBasedTest { }).assertSuccessful().returnedValue(); } - private void deleteLeakedRbacObjects() { + private boolean deleteLeakedRbacObjects() { + final var deletionSuccessful = new AtomicBoolean(true); rbacObjectRepo.findAll().stream() .filter(o -> o.serialId > latestIntialTestDataSerialId) .sorted(comparing(o -> o.serialId)) @@ -235,8 +247,10 @@ public abstract class ContextBasedTestWithCleanup extends ContextBasedTest { if (exception != null) { out.println("DELETING leaked " + o.objectTable + "#" + o.uuid + " FAILED " + exception); + deletionSuccessful.set(false); } }); + return deletionSuccessful.get(); } private void assertEqual(final Set before, final Set after) { @@ -297,6 +311,15 @@ public abstract class ContextBasedTestWithCleanup extends ContextBasedTest { "doc/temp/" + name + ".md" ); } + + public static boolean repeatUntilTrue(int maxAttempts, Supplier method) { + for (int attempts = 0; attempts < maxAttempts; attempts++) { + if (method.get()) { + return true; + } + } + return false; + } } interface RbacObjectRepository extends Repository {