Cloud, Server and Webspace BillingItems and HostingAssets

This commit is contained in:
Michael Hoennig 2024-06-17 06:26:30 +02:00
parent 62867a4cac
commit 0d0c7d74fb
22 changed files with 453 additions and 131 deletions

View File

@ -8,7 +8,11 @@ import static java.lang.String.join;
public class MultiValidationException extends ValidationException { public class MultiValidationException extends ValidationException {
private MultiValidationException(final List<String> violations) { private MultiValidationException(final List<String> 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<String> violations) { public static void throwInvalid(final List<String> violations) {

View File

@ -17,8 +17,6 @@ import net.hostsharing.hsadminng.rbac.rbacdef.RbacView.SQL;
import net.hostsharing.hsadminng.rbac.rbacobject.RbacObject; import net.hostsharing.hsadminng.rbac.rbacobject.RbacObject;
import net.hostsharing.hsadminng.stringify.Stringify; import net.hostsharing.hsadminng.stringify.Stringify;
import net.hostsharing.hsadminng.stringify.Stringifyable; import net.hostsharing.hsadminng.stringify.Stringifyable;
import org.hibernate.annotations.NotFound;
import org.hibernate.annotations.NotFoundAction;
import org.hibernate.annotations.Type; import org.hibernate.annotations.Type;
import jakarta.persistence.CascadeType; import jakarta.persistence.CascadeType;

View File

@ -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.booking.item.HsBookingItemEntity;
import net.hostsharing.hsadminng.hs.validation.HsEntityValidator; import net.hostsharing.hsadminng.hs.validation.HsEntityValidator;
import net.hostsharing.hsadminng.hs.validation.ValidatableProperty; import net.hostsharing.hsadminng.hs.validation.ValidatableProperty;
import org.apache.commons.lang3.BooleanUtils;
import java.util.Collection; import java.util.Collection;
import java.util.List; import java.util.List;
@ -59,19 +60,24 @@ public class HsBookingItemEntityValidator extends HsEntityValidator<HsBookingIte
final var totalValue = ofNullable(bookingItem.getSubBookingItems()).orElse(emptyList()) final var totalValue = ofNullable(bookingItem.getSubBookingItems()).orElse(emptyList())
.stream() .stream()
.map(subItem -> propDef.getValue(subItem.getResources())) .map(subItem -> propDef.getValue(subItem.getResources()))
.map(HsBookingItemEntityValidator::toNonNullInteger) .map(HsBookingItemEntityValidator::convertBooleanToInteger)
.map(HsBookingItemEntityValidator::toIntegerWithDefault0)
.reduce(0, Integer::sum); .reduce(0, Integer::sum);
final var maxValue = getNonNullIntegerValue(propDef, bookingItem.getResources()); final var maxValue = getIntegerValueWithDefault0(propDef, bookingItem.getResources());
if (propDef.thresholdPercentage() != null ) { if (propDef.thresholdPercentage() != null ) {
return totalValue > (maxValue * propDef.thresholdPercentage() / 100) 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()) .formatted(propName, maxValue, propUnit, propName, totalValue, propUnit, propDef.thresholdPercentage())
: null; : null;
} else { } else {
return totalValue > maxValue 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) .formatted(propName, maxValue, propUnit, propName, totalValue, propUnit)
: null; : null;
} }
} }
private static Object convertBooleanToInteger(final Object value) {
return value instanceof Boolean ? BooleanUtils.toInteger((Boolean)value) : value;
}
} }

View File

@ -2,6 +2,7 @@ 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.EnumerationProperty.enumerationProperty;
import static net.hostsharing.hsadminng.hs.validation.IntegerProperty.integerProperty; import static net.hostsharing.hsadminng.hs.validation.IntegerProperty.integerProperty;
@ -9,12 +10,17 @@ class HsCloudServerBookingItemValidator extends HsBookingItemEntityValidator {
HsCloudServerBookingItemValidator() { HsCloudServerBookingItemValidator() {
super( super(
integerProperty("CPUs").min(1).max(32).required(), // @formatter:off
integerProperty("RAM").unit("GB").min(1).max(128).required(), booleanProperty("active") .withDefault(true),
integerProperty("SSD").unit("GB").min(25).max(1000).step(25).required(),
integerProperty("HDD").unit("GB").min(0).max(4000).step(250).withDefault(0), integerProperty("CPUs") .min( 1) .max( 32) .required(),
integerProperty("Traffic").unit("GB").min(250).max(10000).step(250).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(),
enumerationProperty("SLA-Infrastructure").values("BASIC", "EXT8H", "EXT4H", "EXT2H").optional() enumerationProperty("SLA-Infrastructure").values("BASIC", "EXT8H", "EXT4H", "EXT2H").optional()
// @formatter:on
); );
} }
} }

View File

@ -1,18 +1,40 @@
package net.hostsharing.hsadminng.hs.booking.item.validators; 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; import static net.hostsharing.hsadminng.hs.validation.IntegerProperty.integerProperty;
class HsPrivateCloudBookingItemValidator extends HsBookingItemEntityValidator { class HsPrivateCloudBookingItemValidator extends HsBookingItemEntityValidator {
HsPrivateCloudBookingItemValidator() { HsPrivateCloudBookingItemValidator() {
super( super(
integerProperty("CPUs").min(4).max(128).required().asTotalLimit(), // @formatter:off
integerProperty("RAM").unit("GB").min(4).max(512).required().asTotalLimit(), integerProperty("CPUs") .min( 1).max( 128).required().asTotalLimit(),
integerProperty("SSD").unit("GB").min(100).max(4000).step(25).required().asTotalLimit(), integerProperty("RAM").unit("GB") .min( 1).max( 512).required().asTotalLimit(),
integerProperty("HDD").unit("GB").min(0).max(16000).step(25).withDefault(0).asTotalLimit(), integerProperty("SSD").unit("GB") .min( 25).max( 4000).step(25).required().asTotalLimit(),
integerProperty("Traffic").unit("GB").min(1000).max(40000).step(250).required().asTotalLimit(), integerProperty("HDD").unit("GB") .min( 0).max(16000).step(250).withDefault(0).asTotalLimit(),
enumerationProperty("SLA-Infrastructure").values("BASIC", "EXT8H", "EXT4H", "EXT2H").withDefault("BASIC") 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
); );
} }
} }

View File

@ -1,5 +1,6 @@
package net.hostsharing.hsadminng.hs.hosting.asset; 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.hs.hosting.generated.api.v1.api.HsHostingAssetsApi;
import net.hostsharing.hsadminng.context.Context; import net.hostsharing.hsadminng.context.Context;
@ -34,6 +35,9 @@ public class HsHostingAssetController implements HsHostingAssetsApi {
@Autowired @Autowired
private HsHostingAssetRepository assetRepo; private HsHostingAssetRepository assetRepo;
@Autowired
private HsBookingItemRepository bookingItemRepo;
@Override @Override
@Transactional(readOnly = true) @Transactional(readOnly = true)
public ResponseEntity<List<HsHostingAssetResource>> listAssets( public ResponseEntity<List<HsHostingAssetResource>> listAssets(
@ -124,6 +128,11 @@ public class HsHostingAssetController implements HsHostingAssetsApi {
final BiConsumer<HsHostingAssetInsertResource, HsHostingAssetEntity> RESOURCE_TO_ENTITY_POSTMAPPER = (resource, entity) -> { final BiConsumer<HsHostingAssetInsertResource, HsHostingAssetEntity> RESOURCE_TO_ENTITY_POSTMAPPER = (resource, entity) -> {
entity.putConfig(KeyValueMap.from(resource.getConfig())); 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) { if (resource.getParentAssetUuid() != null) {
entity.setParentAsset(assetRepo.findByUuid(resource.getParentAssetUuid()) entity.setParentAsset(assetRepo.findByUuid(resource.getParentAssetUuid())
.orElseThrow(() -> new EntityNotFoundException("ERROR: [400] parentAssetUuid %s not found".formatted( .orElseThrow(() -> new EntityNotFoundException("ERROR: [400] parentAssetUuid %s not found".formatted(

View File

@ -27,6 +27,7 @@ import jakarta.persistence.Id;
import jakarta.persistence.JoinColumn; import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne; import jakarta.persistence.ManyToOne;
import jakarta.persistence.OneToMany; import jakarta.persistence.OneToMany;
import jakarta.persistence.OneToOne;
import jakarta.persistence.Table; import jakarta.persistence.Table;
import jakarta.persistence.Transient; import jakarta.persistence.Transient;
import jakarta.persistence.Version; import jakarta.persistence.Version;
@ -78,7 +79,7 @@ public class HsHostingAssetEntity implements Stringifyable, RbacObject {
@Version @Version
private int version; private int version;
@ManyToOne(fetch = FetchType.LAZY) @OneToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "bookingitemuuid") @JoinColumn(name = "bookingitemuuid")
private HsBookingItemEntity bookingItem; private HsBookingItemEntity bookingItem;
@ -142,7 +143,6 @@ public class HsHostingAssetEntity implements Stringifyable, RbacObject {
dependsOnColumn("bookingItemUuid"), dependsOnColumn("bookingItemUuid"),
directlyFetchedByDependsOnColumn(), directlyFetchedByDependsOnColumn(),
NULLABLE) NULLABLE)
.toRole("bookingItem", AGENT).grantPermission(INSERT)
.importEntityAlias("parentAsset", HsHostingAssetEntity.class, usingDefaultCase(), .importEntityAlias("parentAsset", HsHostingAssetEntity.class, usingDefaultCase(),
dependsOnColumn("parentAssetUuid"), dependsOnColumn("parentAssetUuid"),

View File

@ -65,11 +65,11 @@ public class HsHostingAssetEntityValidator extends HsEntityValidator<HsHostingAs
final var totalValue = ofNullable(hostingAsset.getSubHostingAssets()).orElse(emptyList()) final var totalValue = ofNullable(hostingAsset.getSubHostingAssets()).orElse(emptyList())
.stream() .stream()
.map(subItem -> propDef.getValue(subItem.getConfig())) .map(subItem -> propDef.getValue(subItem.getConfig()))
.map(HsEntityValidator::toNonNullInteger) .map(HsEntityValidator::toIntegerWithDefault0)
.reduce(0, Integer::sum); .reduce(0, Integer::sum);
final var maxValue = getNonNullIntegerValue(propDef, hostingAsset.getConfig()); final var maxValue = getIntegerValueWithDefault0(propDef, hostingAsset.getConfig());
return totalValue > maxValue 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) propName, maxValue, propUnit, propName, totalValue, propUnit)
: null; : null;
} }

View File

@ -1,18 +1,48 @@
package net.hostsharing.hsadminng.hs.hosting.asset.validators; 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; import static net.hostsharing.hsadminng.hs.validation.IntegerProperty.integerProperty;
class HsManagedServerHostingAssetValidator extends HsHostingAssetEntityValidator { class HsManagedServerHostingAssetValidator extends HsHostingAssetEntityValidator {
public HsManagedServerHostingAssetValidator() { public HsManagedServerHostingAssetValidator() {
super( super(
integerProperty("monit_min_free_ssd").min(1).max(1000).optional(), // monitoring
integerProperty("monit_min_free_hdd").min(1).max(4000).optional(), integerProperty("monit_max_cpu_usage").unit("%").min(10).max(100).withDefault(92),
integerProperty("monit_max_ssd_usage").unit("%").min(10).max(100).required(), integerProperty("monit_max_ram_usage").unit("%").min(10).max(100).withDefault(92),
integerProperty("monit_max_hdd_usage").unit("%").min(10).max(100).optional(), integerProperty("monit_max_ssd_usage").unit("%").min(10).max(100).withDefault(98),
integerProperty("monit_max_cpu_usage").unit("%").min(10).max(100).required(), integerProperty("monit_min_free_ssd").min(1).max(1000).withDefault(5),
integerProperty("monit_max_ram_usage").unit("%").min(10).max(100).required() integerProperty("monit_max_hdd_usage").unit("%").min(10).max(100).withDefault(95),
// TODO: stringProperty("monit_alarm_email").unit("GB").optional() 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)
); );
} }
} }

View File

@ -7,6 +7,8 @@ import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.Map; import java.util.Map;
import static java.util.Arrays.stream;
@Setter @Setter
public class EnumerationProperty extends ValidatableProperty<String> { public class EnumerationProperty extends ValidatableProperty<String> {
@ -30,9 +32,27 @@ public class EnumerationProperty extends ValidatableProperty<String> {
return this; return this;
} }
public void deferredInit(final ValidatableProperty<?>[] allProperties) {
if (deferredInit != null) {
if (this.values != null) {
throw new IllegalStateException("property " + toString() + " already values");
}
this.values = deferredInit.apply(allProperties);
}
}
public ValidatableProperty<String> 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 @Override
protected void validate(final ArrayList<String> result, final String propValue, final Map<String, Object> props) { protected void validate(final ArrayList<String> result, final String propValue, final Map<String, Object> 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 + "'"); result.add(propertyName + "' is expected to be one of " + Arrays.toString(values) + " but is '" + propValue + "'");
} }
} }

View File

@ -16,6 +16,7 @@ public abstract class HsEntityValidator<E> {
public HsEntityValidator(final ValidatableProperty<?>... validators) { public HsEntityValidator(final ValidatableProperty<?>... validators) {
propertyValidators = validators; propertyValidators = validators;
stream(propertyValidators).forEach(p -> p.deferredInit(propertyValidators));
} }
protected static List<String> enrich(final String prefix, final List<String> messages) { protected static List<String> enrich(final String prefix, final List<String> messages) {
@ -59,18 +60,24 @@ public abstract class HsEntityValidator<E> {
.orElse(emptyList())); .orElse(emptyList()));
} }
protected static Integer getNonNullIntegerValue(final ValidatableProperty<?> prop, final Map<String, Object> propValues) { protected static Integer getIntegerValueWithDefault0(final ValidatableProperty<?> prop, final Map<String, Object> propValues) {
final var value = prop.getValue(propValues); final var value = prop.getValue(propValues);
if (value instanceof Integer) { if (value instanceof Integer) {
return (Integer) value; return (Integer) value;
} }
if (value == null) {
return 0;
}
throw new IllegalArgumentException(prop.propertyName + " Integer value expected, but got " + value); 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) { if (value instanceof Integer) {
return (Integer) value; 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);
} }
} }

View File

@ -19,6 +19,7 @@ import java.util.function.Function;
import static java.lang.Boolean.FALSE; import static java.lang.Boolean.FALSE;
import static java.lang.Boolean.TRUE; import static java.lang.Boolean.TRUE;
import static java.util.Collections.emptyList; import static java.util.Collections.emptyList;
import static java.util.Optional.ofNullable;
@RequiredArgsConstructor @RequiredArgsConstructor
public abstract class ValidatableProperty<T> { public abstract class ValidatableProperty<T> {
@ -31,6 +32,7 @@ public abstract class ValidatableProperty<T> {
private final String[] keyOrder; private final String[] keyOrder;
private Boolean required; private Boolean required;
private T defaultValue; private T defaultValue;
protected Function<ValidatableProperty<?>[], T[]> deferredInit;
private boolean isTotalsValidator = false; private boolean isTotalsValidator = false;
@JsonIgnore @JsonIgnore
private List<Function<HsBookingItemEntity, List<String>>> asTotalLimitValidators; // TODO.impl: move to BookingItemIntegerProperty private List<Function<HsBookingItemEntity, List<String>>> asTotalLimitValidators; // TODO.impl: move to BookingItemIntegerProperty
@ -57,11 +59,38 @@ public abstract class ValidatableProperty<T> {
return this; return this;
} }
public void deferredInit(final ValidatableProperty<?>[] allProperties) {
}
public ValidatableProperty<T> asTotalLimit() { public ValidatableProperty<T> asTotalLimit() {
isTotalsValidator = true; isTotalsValidator = true;
return this; return this;
} }
public ValidatableProperty<T> asTotalLimitFor(final String propertyName, final String propertyValue) {
if (asTotalLimitValidators == null) {
asTotalLimitValidators = new ArrayList<>();
}
final TriFunction<HsBookingItemEntity, IntegerProperty, Integer, List<String>> 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() { public String propertyName() {
return propertyName; return propertyName;
} }

View File

@ -55,6 +55,7 @@ class HsCloudServerBookingItemValidatorUnitTest {
// then // then
assertThat(validator.properties()).map(Map::toString).containsExactlyInAnyOrder( 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=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=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=25, max=1000, step=25, required=true, isTotalsValidator=false}",
@ -109,10 +110,10 @@ class HsCloudServerBookingItemValidatorUnitTest {
// then // then
assertThat(result).containsExactlyInAnyOrder( 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.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 30 GB", "'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 150 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 5500 GB" "'D-12345:Test-Project:Test Cloud.resources.Traffic' maximum total is 5000 GB, but actual total Traffic is 5500 GB"
); );
} }
} }

View File

@ -120,10 +120,10 @@ class HsManagedServerBookingItemValidatorUnitTest {
// then // then
assertThat(result).containsExactlyInAnyOrder( 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.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 30 GB", "'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 150 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 5500 GB" "'D-12345:Test-Project:null.resources.Traffic' maximum total is 5000 GB, but actual total Traffic is 5500 GB"
); );
} }

View File

@ -28,29 +28,38 @@ class HsPrivateCloudBookingItemValidatorUnitTest {
// given // given
final var privateCloudBookingItemEntity = HsBookingItemEntity.builder() final var privateCloudBookingItemEntity = HsBookingItemEntity.builder()
.type(PRIVATE_CLOUD) .type(PRIVATE_CLOUD)
.caption("myPC")
.resources(ofEntries( .resources(ofEntries(
entry("CPUs", 4), entry("CPUs", 4),
entry("RAM", 20), entry("RAM", 20),
entry("SSD", 100), entry("SSD", 100),
entry("Traffic", 5000) entry("Traffic", 5000),
entry("SLA-Platform EXT4H", 2),
entry("SLA-EMail", 2)
)) ))
.subBookingItems(of( .subBookingItems(of(
HsBookingItemEntity.builder() HsBookingItemEntity.builder()
.type(MANAGED_SERVER) .type(MANAGED_SERVER)
.caption("myMS-1")
.resources(ofEntries( .resources(ofEntries(
entry("CPUs", 2), entry("CPUs", 2),
entry("RAM", 10), entry("RAM", 10),
entry("SSD", 50), entry("SSD", 50),
entry("Traffic", 2500) entry("Traffic", 2500),
entry("SLA-Platform", "EXT4H"),
entry("SLA-EMail", true)
)) ))
.build(), .build(),
HsBookingItemEntity.builder() HsBookingItemEntity.builder()
.type(CLOUD_SERVER) .type(CLOUD_SERVER)
.caption("myMS-2")
.resources(ofEntries( .resources(ofEntries(
entry("CPUs", 2), entry("CPUs", 2),
entry("RAM", 10), entry("RAM", 10),
entry("SSD", 50), entry("SSD", 50),
entry("Traffic", 2500) entry("Traffic", 2500),
entry("SLA-Platform", "EXT4H"),
entry("SLA-EMail", true)
)) ))
.build() .build()
)) ))
@ -69,29 +78,42 @@ class HsPrivateCloudBookingItemValidatorUnitTest {
final var privateCloudBookingItemEntity = HsBookingItemEntity.builder() final var privateCloudBookingItemEntity = HsBookingItemEntity.builder()
.project(project) .project(project)
.type(PRIVATE_CLOUD) .type(PRIVATE_CLOUD)
.caption("myPC")
.resources(ofEntries( .resources(ofEntries(
entry("CPUs", 4), entry("CPUs", 4),
entry("RAM", 20), entry("RAM", 20),
entry("SSD", 100), entry("SSD", 100),
entry("Traffic", 5000) entry("Traffic", 5000),
entry("SLA-Platform EXT2H", 1),
entry("SLA-EMail", 1)
)) ))
.subBookingItems(of( .subBookingItems(of(
HsBookingItemEntity.builder() HsBookingItemEntity.builder()
.type(MANAGED_SERVER) .type(MANAGED_SERVER)
.caption("myMS-1")
.resources(ofEntries( .resources(ofEntries(
entry("CPUs", 3), entry("CPUs", 3),
entry("RAM", 20), entry("RAM", 20),
entry("SSD", 100), entry("SSD", 100),
entry("Traffic", 3000) entry("Traffic", 3000),
entry("SLA-Platform", "EXT2H"),
entry("SLA-EMail", true)
)) ))
.build(), .build(),
HsBookingItemEntity.builder() HsBookingItemEntity.builder()
.type(CLOUD_SERVER) .type(CLOUD_SERVER)
.caption("myMS-2")
.resources(ofEntries( .resources(ofEntries(
entry("CPUs", 2), entry("CPUs", 2),
entry("RAM", 10), entry("RAM", 10),
entry("SSD", 50), 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() .build()
)) ))
@ -102,11 +124,16 @@ class HsPrivateCloudBookingItemValidatorUnitTest {
// then // then
assertThat(result).containsExactlyInAnyOrder( assertThat(result).containsExactlyInAnyOrder(
"'D-12345:Test-Project:null.resources.CPUs' maximum total is 4, but actual total CPUs 5", "'D-12345:Test-Project:myPC.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 30 GB", "'D-12345:Test-Project:myPC.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 150 GB", "'D-12345:Test-Project:myPC.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 5500 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"
);
} }
} }

View File

@ -10,8 +10,11 @@ import net.hostsharing.hsadminng.hs.booking.project.HsBookingProjectRepository;
import net.hostsharing.hsadminng.hs.office.debitor.HsOfficeDebitorRepository; import net.hostsharing.hsadminng.hs.office.debitor.HsOfficeDebitorRepository;
import net.hostsharing.hsadminng.rbac.test.ContextBasedTestWithCleanup; import net.hostsharing.hsadminng.rbac.test.ContextBasedTestWithCleanup;
import net.hostsharing.hsadminng.rbac.test.JpaAttempt; import net.hostsharing.hsadminng.rbac.test.JpaAttempt;
import org.junit.jupiter.api.ClassOrderer;
import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Order;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestClassOrder;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.web.server.LocalServerPort; 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.assertj.core.api.Assertions.assertThat;
import static org.hamcrest.Matchers.matchesRegex; import static org.hamcrest.Matchers.matchesRegex;
@Transactional
@SpringBootTest( @SpringBootTest(
webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT,
classes = { HsadminNgApplication.class, JpaAttempt.class } classes = { HsadminNgApplication.class, JpaAttempt.class }
) )
@Transactional @TestClassOrder(ClassOrderer.OrderAnnotation.class) // fail early on fetching problems
class HsHostingAssetControllerAcceptanceTest extends ContextBasedTestWithCleanup { class HsHostingAssetControllerAcceptanceTest extends ContextBasedTestWithCleanup {
@LocalServerPort @LocalServerPort
@ -54,6 +58,7 @@ class HsHostingAssetControllerAcceptanceTest extends ContextBasedTestWithCleanup
JpaAttempt jpaAttempt; JpaAttempt jpaAttempt;
@Nested @Nested
@Order(2)
class ListAssets { class ListAssets {
@Test @Test
@ -152,6 +157,7 @@ class HsHostingAssetControllerAcceptanceTest extends ContextBasedTestWithCleanup
} }
@Nested @Nested
@Order(3)
class AddAsset { class AddAsset {
@Test @Test
@ -231,17 +237,17 @@ class HsHostingAssetControllerAcceptanceTest extends ContextBasedTestWithCleanup
.when() .when()
.post("http://localhost/api/hs/hosting/assets") .post("http://localhost/api/hs/hosting/assets")
.then().log().all().assertThat() .then().log().all().assertThat()
.statusCode(201) .statusCode(201)
.contentType(ContentType.JSON) .contentType(ContentType.JSON)
.body("", lenientlyEquals(""" .body("", lenientlyEquals("""
{ {
"type": "MANAGED_WEBSPACE", "type": "MANAGED_WEBSPACE",
"identifier": "fir90", "identifier": "fir90",
"caption": "some new ManagedWebspace in client's ManagedServer", "caption": "some new ManagedWebspace in client's ManagedServer",
"config": {} "config": {}
} }
""")) """))
.header("Location", matchesRegex("http://localhost:[1-9][0-9]*/api/hs/hosting/assets/[^/]*")) .header("Location", matchesRegex("http://localhost:[1-9][0-9]*/api/hs/hosting/assets/[^/]*"))
.extract().header("Location"); // @formatter:on .extract().header("Location"); // @formatter:on
// finally, the new asset can be accessed under the generated UUID // finally, the new asset can be accessed under the generated UUID
@ -258,34 +264,33 @@ class HsHostingAssetControllerAcceptanceTest extends ContextBasedTestWithCleanup
RestAssured // @formatter:off RestAssured // @formatter:off
.given() .given()
.header("current-user", "superuser-alex@hostsharing.net") .header("current-user", "superuser-alex@hostsharing.net")
.contentType(ContentType.JSON) .contentType(ContentType.JSON)
.body(""" .body("""
{ {
"bookingItemUuid": "%s", "bookingItemUuid": "%s",
"type": "MANAGED_SERVER", "type": "MANAGED_SERVER",
"identifier": "vm1400", "identifier": "vm1400",
"caption": "some new ManagedServer", "caption": "some new ManagedServer",
"config": { "monit_max_ssd_usage": 0, "monit_max_cpu_usage": 101, "extra": 42 } "config": { "monit_max_ssd_usage": 0, "monit_max_cpu_usage": 101, "extra": 42 }
} }
""".formatted(givenBookingItem.getUuid())) """.formatted(givenBookingItem.getUuid()))
.port(port) .port(port)
.when() .when()
.post("http://localhost/api/hs/hosting/assets") .post("http://localhost/api/hs/hosting/assets")
.then().log().all().assertThat() .then().log().all().assertThat()
.statusCode(400) .statusCode(400)
.contentType(ContentType.JSON) .contentType(ContentType.JSON)
.body("", lenientlyEquals(""" .body("", lenientlyEquals("""
{ {
"statusPhrase": "Bad Request", "statusPhrase": "Bad Request",
"message": "[ "message": "[
<<<'MANAGED_SERVER:vm1400.config.extra' is not expected but is set to '42', <<<'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_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
<<<'MANAGED_SERVER:vm1400.config.monit_max_ram_usage' is required but missing <<<]"
<<<]" }
} """.replaceAll(" +<<<", ""))); // @formatter:on
""".replaceAll(" +<<<", ""))); // @formatter:on
} }
@ -333,15 +338,14 @@ class HsHostingAssetControllerAcceptanceTest extends ContextBasedTestWithCleanup
.body("", lenientlyEquals(""" .body("", lenientlyEquals("""
{ {
"statusPhrase": "Bad Request", "statusPhrase": "Bad Request",
"message": "[ "message": "['D-1000111:D-1000111 default project:separate ManagedWebspace.resources.Multi=1 allows at maximum 25 unix users, but 26 found]"
<<<'D-1000111:D-1000111 default project:separate ManagedWebspace.resources.Multi=1 allows at maximum 25 unix users, but 26 found
<<<]"
} }
""".replaceAll(" +<<<", ""))); // @formatter:on """.replaceAll(" +<<<", ""))); // @formatter:on
} }
} }
@Nested @Nested
@Order(1)
class GetAsset { class GetAsset {
@Test @Test
@ -413,6 +417,7 @@ class HsHostingAssetControllerAcceptanceTest extends ContextBasedTestWithCleanup
} }
@Nested @Nested
@Order(4)
class PatchAsset { class PatchAsset {
@Test @Test
@ -466,6 +471,7 @@ class HsHostingAssetControllerAcceptanceTest extends ContextBasedTestWithCleanup
} }
@Nested @Nested
@Order(5)
class DeleteAsset { class DeleteAsset {
@Test @Test

View File

@ -55,18 +55,22 @@ class HsHostingAssetPropsControllerAcceptanceTest {
[ [
{ {
"type": "integer", "type": "integer",
"propertyName": "monit_min_free_ssd", "propertyName": "monit_max_cpu_usage",
"min": 1, "unit": "%",
"max": 1000, "min": 10,
"max": 100,
"required": false, "required": false,
"defaultValue": 92,
"isTotalsValidator": false "isTotalsValidator": false
}, },
{ {
"type": "integer", "type": "integer",
"propertyName": "monit_min_free_hdd", "propertyName": "monit_max_ram_usage",
"min": 1, "unit": "%",
"max": 4000, "min": 10,
"max": 100,
"required": false, "required": false,
"defaultValue": 92,
"isTotalsValidator": false "isTotalsValidator": false
}, },
{ {
@ -75,7 +79,17 @@ class HsHostingAssetPropsControllerAcceptanceTest {
"unit": "%", "unit": "%",
"min": 10, "min": 10,
"max": 100, "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 "isTotalsValidator": false
}, },
{ {
@ -85,29 +99,157 @@ class HsHostingAssetPropsControllerAcceptanceTest {
"min": 10, "min": 10,
"max": 100, "max": 100,
"required": false, "required": false,
"defaultValue": 95,
"isTotalsValidator": false "isTotalsValidator": false
}, },
{ {
"type": "integer", "type": "integer",
"propertyName": "monit_max_cpu_usage", "propertyName": "monit_min_free_hdd",
"unit": "%", "min": 1,
"min": 10, "max": 4000,
"max": 100, "required": false,
"required": true, "defaultValue": 10,
"isTotalsValidator": false "isTotalsValidator": false
}, },
{ {
"type": "integer", "type": "boolean",
"propertyName": "monit_max_ram_usage", "propertyName": "software-pgsql",
"unit": "%", "required": false,
"min": 10, "defaultValue": true,
"max": 100, "isTotalsValidator": false
"required": true, },
{
"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 "isTotalsValidator": false
} }
] ]
""")); """));
// @formatter:on // @formatter:on
} }
} }

View File

@ -24,8 +24,6 @@ import jakarta.servlet.http.HttpServletRequest;
import java.util.Arrays; import java.util.Arrays;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.concurrent.TimeUnit;
import java.util.function.Supplier;
import static java.util.Map.entry; 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.CLOUD_SERVER;

View File

@ -3,7 +3,6 @@ package net.hostsharing.hsadminng.hs.hosting.asset.validators;
import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetEntity; import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetEntity;
import org.junit.jupiter.api.Test; 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.HsHostingAssetType.MANAGED_SERVER;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
@ -23,10 +22,6 @@ class HsHostingAssetEntityValidatorUnitTest {
final var result = catchThrowable( ()-> HsHostingAssetEntityValidatorRegistry.validated(managedServerHostingAssetEntity)); final var result = catchThrowable( ()-> HsHostingAssetEntityValidatorRegistry.validated(managedServerHostingAssetEntity));
// then // then
assertThat(result).isInstanceOf(ValidationException.class) assertThat(result).isNull(); // all required properties have defaults
.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");
} }
} }

View File

@ -32,7 +32,6 @@ class HsManagedServerHostingAssetValidatorUnitTest {
assertThat(result).containsExactlyInAnyOrder( 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_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_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'"); "'MANAGED_SERVER:vm1234.config.monit_max_hdd_usage' is expected to be of type class java.lang.Integer, but is of type 'String'");
} }
} }

View File

@ -109,9 +109,9 @@ class HsOfficeDebitorRepositoryIntegrationTest extends ContextBasedTestWithClean
assertThat(debitorRepo.count()).isEqualTo(count + 1); assertThat(debitorRepo.count()).isEqualTo(count + 1);
} }
@Transactional
@ParameterizedTest @ParameterizedTest
@ValueSource(strings = {"", "a", "ab", "a12", "123", "12a"}) @ValueSource(strings = {"", "a", "ab", "a12", "123", "12a"})
@Transactional
public void canNotCreateNewDebitorWithInvalidDefaultPrefix(final String givenPrefix) { public void canNotCreateNewDebitorWithInvalidDefaultPrefix(final String givenPrefix) {
// given // given
context("superuser-alex@hostsharing.net"); context("superuser-alex@hostsharing.net");

View File

@ -14,9 +14,12 @@ import org.junit.jupiter.api.TestInfo;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.jpa.repository.Query; import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.Repository; import org.springframework.data.repository.Repository;
import org.springframework.transaction.PlatformTransactionManager;
import jakarta.persistence.*; import jakarta.persistence.*;
import java.util.*; import java.util.*;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.Supplier;
import static java.lang.System.out; import static java.lang.System.out;
import static java.util.Comparator.comparing; import static java.util.Comparator.comparing;
@ -28,9 +31,13 @@ import static org.assertj.core.api.Assertions.assertThat;
public abstract class ContextBasedTestWithCleanup extends ContextBasedTest { public abstract class ContextBasedTestWithCleanup extends ContextBasedTest {
private static final boolean DETAILED_BUT_SLOW_CHECK = true; private static final boolean DETAILED_BUT_SLOW_CHECK = true;
@PersistenceContext @PersistenceContext
protected EntityManager em; protected EntityManager em;
@Autowired
private PlatformTransactionManager tm;
@Autowired @Autowired
RbacGrantRepository rbacGrantRepo; RbacGrantRepository rbacGrantRepo;
@ -166,12 +173,16 @@ public abstract class ContextBasedTestWithCleanup extends ContextBasedTest {
@AfterEach @AfterEach
void cleanupAndCheckCleanup(final TestInfo testInfo) { void cleanupAndCheckCleanup(final TestInfo testInfo) {
out.println(ContextBasedTestWithCleanup.class.getSimpleName() + ".cleanupAndCheckCleanup"); // If the whole test method has its own transaction, cleanup makes no sense.
cleanupTemporaryTestData(); // If that transaction even failed, cleaunup would cause an exception.
deleteLeakedRbacObjects(); if (!tm.getTransaction(null).isRollbackOnly()) {
long rbacObjectCount = assertNoNewRbacObjectsRolesAndGrantsLeaked(); 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() { private void cleanupTemporaryTestData() {
@ -218,7 +229,8 @@ public abstract class ContextBasedTestWithCleanup extends ContextBasedTest {
}).assertSuccessful().returnedValue(); }).assertSuccessful().returnedValue();
} }
private void deleteLeakedRbacObjects() { private boolean deleteLeakedRbacObjects() {
final var deletionSuccessful = new AtomicBoolean(true);
rbacObjectRepo.findAll().stream() rbacObjectRepo.findAll().stream()
.filter(o -> o.serialId > latestIntialTestDataSerialId) .filter(o -> o.serialId > latestIntialTestDataSerialId)
.sorted(comparing(o -> o.serialId)) .sorted(comparing(o -> o.serialId))
@ -235,8 +247,10 @@ public abstract class ContextBasedTestWithCleanup extends ContextBasedTest {
if (exception != null) { if (exception != null) {
out.println("DELETING leaked " + o.objectTable + "#" + o.uuid + " FAILED " + exception); out.println("DELETING leaked " + o.objectTable + "#" + o.uuid + " FAILED " + exception);
deletionSuccessful.set(false);
} }
}); });
return deletionSuccessful.get();
} }
private void assertEqual(final Set<String> before, final Set<String> after) { private void assertEqual(final Set<String> before, final Set<String> after) {
@ -297,6 +311,15 @@ public abstract class ContextBasedTestWithCleanup extends ContextBasedTest {
"doc/temp/" + name + ".md" "doc/temp/" + name + ".md"
); );
} }
public static boolean repeatUntilTrue(int maxAttempts, Supplier<Boolean> method) {
for (int attempts = 0; attempts < maxAttempts; attempts++) {
if (method.get()) {
return true;
}
}
return false;
}
} }
interface RbacObjectRepository extends Repository<RbacObjectEntity, UUID> { interface RbacObjectRepository extends Repository<RbacObjectEntity, UUID> {