hosting-asset-validation-baseline (#56)

Co-authored-by: Michael Hoennig <michael@hoennig.de>
Reviewed-on: #56
Reviewed-by: Timotheus Pokorra <timotheus.pokorra@hostsharing.net>
This commit is contained in:
Michael Hoennig 2024-05-30 10:45:12 +02:00
parent 2e9e5d6ef0
commit 23a6f89943
41 changed files with 1211 additions and 571 deletions

View File

@ -17,6 +17,7 @@ import java.util.List;
import java.util.UUID; import java.util.UUID;
import java.util.function.BiConsumer; import java.util.function.BiConsumer;
import static net.hostsharing.hsadminng.hs.booking.item.validators.HsBookingItemEntityValidators.valid;
import static net.hostsharing.hsadminng.mapper.PostgresDateRange.toPostgresDateRange; import static net.hostsharing.hsadminng.mapper.PostgresDateRange.toPostgresDateRange;
@RestController @RestController
@ -56,7 +57,7 @@ public class HsBookingItemController implements HsBookingItemsApi {
final var entityToSave = mapper.map(body, HsBookingItemEntity.class, RESOURCE_TO_ENTITY_POSTMAPPER); final var entityToSave = mapper.map(body, HsBookingItemEntity.class, RESOURCE_TO_ENTITY_POSTMAPPER);
final var saved = bookingItemRepo.save(entityToSave); final var saved = bookingItemRepo.save(valid(entityToSave));
final var uri = final var uri =
MvcUriComponentsBuilder.fromController(getClass()) MvcUriComponentsBuilder.fromController(getClass())
@ -111,7 +112,7 @@ public class HsBookingItemController implements HsBookingItemsApi {
new HsBookingItemEntityPatcher(current).apply(body); new HsBookingItemEntityPatcher(current).apply(body);
final var saved = bookingItemRepo.save(current); final var saved = bookingItemRepo.save(valid(current));
final var mapped = mapper.map(saved, HsBookingItemResource.class, ENTITY_TO_RESOURCE_POSTMAPPER); final var mapped = mapper.map(saved, HsBookingItemResource.class, ENTITY_TO_RESOURCE_POSTMAPPER);
return ResponseEntity.ok(mapped); return ResponseEntity.ok(mapped);
} }

View File

@ -11,6 +11,7 @@ import lombok.NoArgsConstructor;
import lombok.Setter; import lombok.Setter;
import net.hostsharing.hsadminng.hs.office.debitor.HsOfficeDebitorEntity; import net.hostsharing.hsadminng.hs.office.debitor.HsOfficeDebitorEntity;
import net.hostsharing.hsadminng.hs.office.relation.HsOfficeRelationEntity; import net.hostsharing.hsadminng.hs.office.relation.HsOfficeRelationEntity;
import net.hostsharing.hsadminng.hs.validation.Validatable;
import net.hostsharing.hsadminng.mapper.PatchableMapWrapper; import net.hostsharing.hsadminng.mapper.PatchableMapWrapper;
import net.hostsharing.hsadminng.rbac.rbacdef.RbacView; import net.hostsharing.hsadminng.rbac.rbacdef.RbacView;
import net.hostsharing.hsadminng.rbac.rbacdef.RbacView.SQL; import net.hostsharing.hsadminng.rbac.rbacdef.RbacView.SQL;
@ -65,7 +66,7 @@ import static net.hostsharing.hsadminng.stringify.Stringify.stringify;
@Setter @Setter
@NoArgsConstructor @NoArgsConstructor
@AllArgsConstructor @AllArgsConstructor
public class HsBookingItemEntity implements Stringifyable, RbacObject { public class HsBookingItemEntity implements Stringifyable, RbacObject, Validatable<HsBookingItemEntity, HsBookingItemType> {
private static Stringify<HsBookingItemEntity> stringify = stringify(HsBookingItemEntity.class) private static Stringify<HsBookingItemEntity> stringify = stringify(HsBookingItemEntity.class)
.withProp(HsBookingItemEntity::getDebitor) .withProp(HsBookingItemEntity::getDebitor)
@ -142,6 +143,16 @@ public class HsBookingItemEntity implements Stringifyable, RbacObject {
":" + caption; ":" + caption;
} }
@Override
public String getPropertiesName() {
return "resources";
}
@Override
public Map<String, Object> getProperties() {
return resources;
}
public static RbacView rbac() { public static RbacView rbac() {
return rbacViewFor("bookingItem", HsBookingItemEntity.class) return rbacViewFor("bookingItem", HsBookingItemEntity.class)
.withIdentityView(SQL.query(""" .withIdentityView(SQL.query("""

View File

@ -0,0 +1,50 @@
package net.hostsharing.hsadminng.hs.booking.item.validators;
import lombok.experimental.UtilityClass;
import net.hostsharing.hsadminng.hs.booking.item.HsBookingItemEntity;
import net.hostsharing.hsadminng.hs.booking.item.HsBookingItemType;
import net.hostsharing.hsadminng.hs.validation.HsEntityValidator;
import jakarta.validation.ValidationException;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import static java.util.Arrays.stream;
import static net.hostsharing.hsadminng.hs.booking.item.HsBookingItemType.CLOUD_SERVER;
import static net.hostsharing.hsadminng.hs.booking.item.HsBookingItemType.MANAGED_SERVER;
import static net.hostsharing.hsadminng.hs.booking.item.HsBookingItemType.MANAGED_WEBSPACE;
@UtilityClass
public class HsBookingItemEntityValidators {
private static final Map<Enum<HsBookingItemType>, HsEntityValidator<HsBookingItemEntity, HsBookingItemType>> validators = new HashMap<>();
static {
register(CLOUD_SERVER, new HsCloudServerBookingItemValidator());
register(MANAGED_SERVER, new HsManagedServerBookingItemValidator());
register(MANAGED_WEBSPACE, new HsManagedWebspaceBookingItemValidator());
}
private static void register(final Enum<HsBookingItemType> type, final HsEntityValidator<HsBookingItemEntity, HsBookingItemType> validator) {
stream(validator.propertyValidators).forEach( entry -> {
entry.verifyConsistency(Map.entry(type, validator));
});
validators.put(type, validator);
}
public static HsEntityValidator<HsBookingItemEntity, HsBookingItemType> forType(final Enum<HsBookingItemType> type) {
return validators.get(type);
}
public static Set<Enum<HsBookingItemType>> types() {
return validators.keySet();
}
public static HsBookingItemEntity valid(final HsBookingItemEntity entityToSave) {
final var violations = HsBookingItemEntityValidators.forType(entityToSave.getType()).validate(entityToSave);
if (!violations.isEmpty()) {
throw new ValidationException(violations.toString());
}
return entityToSave;
}
}

View File

@ -0,0 +1,22 @@
package net.hostsharing.hsadminng.hs.booking.item.validators;
import net.hostsharing.hsadminng.hs.booking.item.HsBookingItemEntity;
import net.hostsharing.hsadminng.hs.booking.item.HsBookingItemType;
import net.hostsharing.hsadminng.hs.validation.HsEntityValidator;
import static net.hostsharing.hsadminng.hs.validation.EnumerationPropertyValidator.enumerationProperty;
import static net.hostsharing.hsadminng.hs.validation.IntegerPropertyValidator.integerProperty;
class HsCloudServerBookingItemValidator extends HsEntityValidator<HsBookingItemEntity, HsBookingItemType> {
HsCloudServerBookingItemValidator() {
super(
integerProperty("CPUs").min(1).max(32).required(),
integerProperty("RAM").unit("GB").min(1).max(128).required(),
integerProperty("SSD").unit("GB").min(25).max(1000).step(25).required(),
integerProperty("HDD").unit("GB").min(0).max(4000).step(250).optional(),
integerProperty("Traffic").unit("GB").min(250).max(10000).step(250).required(),
enumerationProperty("SLA-Infrastructure").values("BASIC", "EXT8H", "EXT4H", "EXT2H").optional()
);
}
}

View File

@ -0,0 +1,28 @@
package net.hostsharing.hsadminng.hs.booking.item.validators;
import net.hostsharing.hsadminng.hs.booking.item.HsBookingItemEntity;
import net.hostsharing.hsadminng.hs.booking.item.HsBookingItemType;
import net.hostsharing.hsadminng.hs.validation.HsEntityValidator;
import static net.hostsharing.hsadminng.hs.validation.BooleanPropertyValidator.booleanProperty;
import static net.hostsharing.hsadminng.hs.validation.EnumerationPropertyValidator.enumerationProperty;
import static net.hostsharing.hsadminng.hs.validation.IntegerPropertyValidator.integerProperty;
class HsManagedServerBookingItemValidator extends HsEntityValidator<HsBookingItemEntity, HsBookingItemType> {
HsManagedServerBookingItemValidator() {
super(
integerProperty("CPUs").min(1).max(32).required(),
integerProperty("RAM").unit("GB").min(1).max(128).required(),
integerProperty("SSD").unit("GB").min(25).max(1000).step(25).required(),
integerProperty("HDD").unit("GB").min(0).max(4000).step(250).optional(),
integerProperty("Traffic").unit("GB").min(250).max(10000).step(250).required(),
enumerationProperty("SLA-Platform").values("BASIC", "EXT8H", "EXT4H", "EXT2H").optional(),
booleanProperty("SLA-EMail").falseIf("SLA-Platform", "BASIC").optional(),
booleanProperty("SLA-Maria").falseIf("SLA-Platform", "BASIC").optional(),
booleanProperty("SLA-PgSQL").falseIf("SLA-Platform", "BASIC").optional(),
booleanProperty("SLA-Office").falseIf("SLA-Platform", "BASIC").optional(),
booleanProperty("SLA-Web").falseIf("SLA-Platform", "BASIC").optional()
);
}
}

View File

@ -0,0 +1,24 @@
package net.hostsharing.hsadminng.hs.booking.item.validators;
import net.hostsharing.hsadminng.hs.booking.item.HsBookingItemEntity;
import net.hostsharing.hsadminng.hs.booking.item.HsBookingItemType;
import net.hostsharing.hsadminng.hs.validation.HsEntityValidator;
import static net.hostsharing.hsadminng.hs.validation.BooleanPropertyValidator.booleanProperty;
import static net.hostsharing.hsadminng.hs.validation.EnumerationPropertyValidator.enumerationProperty;
import static net.hostsharing.hsadminng.hs.validation.IntegerPropertyValidator.integerProperty;
class HsManagedWebspaceBookingItemValidator extends HsEntityValidator<HsBookingItemEntity, HsBookingItemType> {
public HsManagedWebspaceBookingItemValidator() {
super(
integerProperty("SSD").unit("GB").min(1).max(100).step(1).required(),
integerProperty("HDD").unit("GB").min(0).max(250).step(10).optional(),
integerProperty("Traffic").unit("GB").min(10).max(1000).step(10).required(),
enumerationProperty("SLA-Platform").values("BASIC", "EXT24H").optional(),
integerProperty("Daemons").min(0).max(10).optional(),
booleanProperty("Online Office Server").optional()
);
}
}

View File

@ -1,6 +1,5 @@
package net.hostsharing.hsadminng.hs.hosting.asset; package net.hostsharing.hsadminng.hs.hosting.asset;
import net.hostsharing.hsadminng.hs.hosting.asset.validator.HsHostingAssetValidator;
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;
@ -16,11 +15,12 @@ import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.mvc.method.annotation.MvcUriComponentsBuilder; import org.springframework.web.servlet.mvc.method.annotation.MvcUriComponentsBuilder;
import jakarta.validation.ValidationException; import jakarta.persistence.EntityNotFoundException;
import java.util.List; import java.util.List;
import java.util.UUID; import java.util.UUID;
import java.util.function.BiConsumer; import java.util.function.BiConsumer;
import static net.hostsharing.hsadminng.hs.hosting.asset.validators.HsHostingAssetEntityValidators.valid;
@RestController @RestController
public class HsHostingAssetController implements HsHostingAssetsApi { public class HsHostingAssetController implements HsHostingAssetsApi {
@ -117,21 +117,17 @@ public class HsHostingAssetController implements HsHostingAssetsApi {
new HsHostingAssetEntityPatcher(current).apply(body); new HsHostingAssetEntityPatcher(current).apply(body);
final var saved = assetRepo.save(current); final var saved = assetRepo.save(valid(current));
final var mapped = mapper.map(saved, HsHostingAssetResource.class); final var mapped = mapper.map(saved, HsHostingAssetResource.class);
return ResponseEntity.ok(mapped); return ResponseEntity.ok(mapped);
} }
private HsHostingAssetEntity valid(final HsHostingAssetEntity entityToSave) {
final var violations = HsHostingAssetValidator.forType(entityToSave.getType()).validate(entityToSave);
if (!violations.isEmpty()) {
throw new ValidationException(violations.toString());
}
return entityToSave;
}
@SuppressWarnings("unchecked")
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.getParentAssetUuid() != null) {
entity.setParentAsset(assetRepo.findByUuid(resource.getParentAssetUuid())
.orElseThrow(() -> new EntityNotFoundException("ERROR: [400] parentAssetUuid %s not found".formatted(
resource.getParentAssetUuid()))));
}
}; };
} }

View File

@ -8,6 +8,7 @@ import lombok.Getter;
import lombok.NoArgsConstructor; import lombok.NoArgsConstructor;
import lombok.Setter; import lombok.Setter;
import net.hostsharing.hsadminng.hs.booking.item.HsBookingItemEntity; import net.hostsharing.hsadminng.hs.booking.item.HsBookingItemEntity;
import net.hostsharing.hsadminng.hs.validation.Validatable;
import net.hostsharing.hsadminng.mapper.PatchableMapWrapper; import net.hostsharing.hsadminng.mapper.PatchableMapWrapper;
import net.hostsharing.hsadminng.rbac.rbacdef.RbacView; import net.hostsharing.hsadminng.rbac.rbacdef.RbacView;
import net.hostsharing.hsadminng.rbac.rbacdef.RbacView.SQL; import net.hostsharing.hsadminng.rbac.rbacdef.RbacView.SQL;
@ -40,7 +41,6 @@ import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.CaseDef.inOtherCas
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Column.dependsOnColumn; import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Column.dependsOnColumn;
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.ColumnValue.usingCase; import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.ColumnValue.usingCase;
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.ColumnValue.usingDefaultCase; import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.ColumnValue.usingDefaultCase;
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Nullable.NOT_NULL;
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Nullable.NULLABLE; import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Nullable.NULLABLE;
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Permission.DELETE; import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Permission.DELETE;
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Permission.INSERT; import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Permission.INSERT;
@ -61,7 +61,7 @@ import static net.hostsharing.hsadminng.stringify.Stringify.stringify;
@Setter @Setter
@NoArgsConstructor @NoArgsConstructor
@AllArgsConstructor @AllArgsConstructor
public class HsHostingAssetEntity implements Stringifyable, RbacObject { public class HsHostingAssetEntity implements Stringifyable, RbacObject, Validatable<HsHostingAssetEntity, HsHostingAssetType> {
private static Stringify<HsHostingAssetEntity> stringify = stringify(HsHostingAssetEntity.class) private static Stringify<HsHostingAssetEntity> stringify = stringify(HsHostingAssetEntity.class)
.withProp(HsHostingAssetEntity::getType) .withProp(HsHostingAssetEntity::getType)
@ -114,6 +114,16 @@ public class HsHostingAssetEntity implements Stringifyable, RbacObject {
PatchableMapWrapper.of(configWrapper, (newWrapper) -> {configWrapper = newWrapper; }, config).assign(newConfg); PatchableMapWrapper.of(configWrapper, (newWrapper) -> {configWrapper = newWrapper; }, config).assign(newConfg);
} }
@Override
public String getPropertiesName() {
return "config";
}
@Override
public Map<String, Object> getProperties() {
return config;
}
@Override @Override
public String toString() { public String toString() {
return stringify.apply(this); return stringify.apply(this);
@ -137,7 +147,7 @@ public class HsHostingAssetEntity implements Stringifyable, RbacObject {
.importEntityAlias("bookingItem", HsBookingItemEntity.class, usingDefaultCase(), .importEntityAlias("bookingItem", HsBookingItemEntity.class, usingDefaultCase(),
dependsOnColumn("bookingItemUuid"), dependsOnColumn("bookingItemUuid"),
directlyFetchedByDependsOnColumn(), directlyFetchedByDependsOnColumn(),
NOT_NULL) NULLABLE)
.switchOnColumn("type", .switchOnColumn("type",
inCaseOf(CLOUD_SERVER.name(), inCaseOf(CLOUD_SERVER.name(),

View File

@ -1,6 +1,6 @@
package net.hostsharing.hsadminng.hs.hosting.asset; package net.hostsharing.hsadminng.hs.hosting.asset;
import net.hostsharing.hsadminng.hs.hosting.asset.validator.HsHostingAssetValidator; import net.hostsharing.hsadminng.hs.hosting.asset.validators.HsHostingAssetEntityValidators;
import net.hostsharing.hsadminng.hs.hosting.generated.api.v1.api.HsHostingAssetPropsApi; import net.hostsharing.hsadminng.hs.hosting.generated.api.v1.api.HsHostingAssetPropsApi;
import net.hostsharing.hsadminng.hs.hosting.generated.api.v1.model.HsHostingAssetTypeResource; import net.hostsharing.hsadminng.hs.hosting.generated.api.v1.model.HsHostingAssetTypeResource;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
@ -15,7 +15,7 @@ public class HsHostingAssetPropsController implements HsHostingAssetPropsApi {
@Override @Override
public ResponseEntity<List<String>> listAssetTypes() { public ResponseEntity<List<String>> listAssetTypes() {
final var resource = HsHostingAssetValidator.types().stream() final var resource = HsHostingAssetEntityValidators.types().stream()
.map(Enum::name) .map(Enum::name)
.toList(); .toList();
return ResponseEntity.ok(resource); return ResponseEntity.ok(resource);
@ -25,7 +25,7 @@ public class HsHostingAssetPropsController implements HsHostingAssetPropsApi {
public ResponseEntity<List<Object>> listAssetTypeProps( public ResponseEntity<List<Object>> listAssetTypeProps(
final HsHostingAssetTypeResource assetType) { final HsHostingAssetTypeResource assetType) {
final var propValidators = HsHostingAssetValidator.forType(HsHostingAssetType.of(assetType)); final var propValidators = HsHostingAssetEntityValidators.forType(HsHostingAssetType.of(assetType));
final List<Map<String, Object>> resource = propValidators.properties(); final List<Map<String, Object>> resource = propValidators.properties();
return ResponseEntity.ok(toListOfObjects(resource)); return ResponseEntity.ok(toListOfObjects(resource));
} }

View File

@ -1,172 +0,0 @@
package net.hostsharing.hsadminng.hs.hosting.asset.validator;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.RequiredArgsConstructor;
import lombok.Setter;
import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType;
import java.util.AbstractMap.SimpleImmutableEntry;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.Objects;
@RequiredArgsConstructor
public abstract class HsHostingAssetPropertyValidator<T> {
final Class<T> type;
final String propertyName;
private Boolean required;
public static <K, V> Map.Entry<K, V> defType(K k, V v) {
return new SimpleImmutableEntry<>(k, v);
}
public HsHostingAssetPropertyValidator<T> required() {
required = Boolean.TRUE;
return this;
}
public HsHostingAssetPropertyValidator<T> optional() {
required = Boolean.FALSE;
return this;
}
public final List<String> validate(final Map<String, Object> props) {
final var result = new ArrayList<String>();
final var propValue = props.get(propertyName);
if (propValue == null) {
if (required) {
result.add("'" + propertyName + "' is required but missing");
}
}
if (propValue != null){
if ( type.isInstance(propValue)) {
//noinspection unchecked
validate(result, (T) propValue, props);
} else {
result.add("'" + propertyName + "' is expected to be of type " + type + ", " +
"but is of type '" + propValue.getClass().getSimpleName() + "'");
}
}
return result;
}
protected abstract void validate(final ArrayList<String> result, final T propValue, final Map<String, Object> props);
public void verifyConsistency(final Map.Entry<HsHostingAssetType, HsHostingAssetValidator> typeDef) {
if (required == null ) {
throw new IllegalStateException(typeDef.getKey() + "[" + propertyName + "] not fully initialized, please call either .required() or .optional()" );
}
}
public Map<String, Object> toMap(final ObjectMapper mapper) {
final Map<String, Object> map = mapper.convertValue(this, Map.class);
map.put("type", simpleTypeName());
return map;
}
protected abstract String simpleTypeName();
}
@Setter
class IntegerPropertyValidator extends HsHostingAssetPropertyValidator<Integer>{
private String unit;
private Integer min;
private Integer max;
private Integer step;
public static IntegerPropertyValidator integerProperty(final String propertyName) {
return new IntegerPropertyValidator(propertyName);
}
private IntegerPropertyValidator(final String propertyName) {
super(Integer.class, propertyName);
}
@Override
protected void validate(final ArrayList<String> result, final Integer propValue, final Map<String, Object> props) {
if (min != null && propValue < min) {
result.add("'" + propertyName + "' is expected to be >= " + min + " but is " + propValue);
}
if (max != null && propValue > max) {
result.add("'" + propertyName + "' is expected to be <= " + max + " but is " + propValue);
}
if (step != null && propValue % step != 0) {
result.add("'" + propertyName + "' is expected to be multiple of " + step + " but is " + propValue);
}
}
@Override
protected String simpleTypeName() {
return "integer";
}
}
@Setter
class EnumPropertyValidator extends HsHostingAssetPropertyValidator<String> {
private String[] values;
private EnumPropertyValidator(final String propertyName) {
super(String.class, propertyName);
}
public static EnumPropertyValidator enumerationProperty(final String propertyName) {
return new EnumPropertyValidator(propertyName);
}
public HsHostingAssetPropertyValidator<String> values(final String... values) {
this.values = values;
return this;
}
@Override
protected void validate(final ArrayList<String> result, final String propValue, final Map<String, Object> props) {
if (Arrays.stream(values).noneMatch(v -> v.equals(propValue))) {
result.add("'" + propertyName + "' is expected to be one of " + Arrays.toString(values) + " but is '" + propValue + "'");
}
}
@Override
protected String simpleTypeName() {
return "enumeration";
}
}
@Setter
class BooleanPropertyValidator extends HsHostingAssetPropertyValidator<Boolean> {
private Map.Entry<String, String> falseIf;
private BooleanPropertyValidator(final String propertyName) {
super(Boolean.class, propertyName);
}
public static BooleanPropertyValidator booleanProperty(final String propertyName) {
return new BooleanPropertyValidator(propertyName);
}
HsHostingAssetPropertyValidator<Boolean> falseIf(final String refPropertyName, final String refPropertyValue) {
this.falseIf = new SimpleImmutableEntry<>(refPropertyName, refPropertyValue);
return this;
}
@Override
protected void validate(final ArrayList<String> result, final Boolean propValue, final Map<String, Object> props) {
if (falseIf != null && !Objects.equals(props.get(falseIf.getKey()), falseIf.getValue())) {
if (propValue) {
result.add("'" + propertyName + "' is expected to be false because " +
falseIf.getKey()+ "=" + falseIf.getValue() + " but is " + propValue);
}
}
}
@Override
protected String simpleTypeName() {
return "boolean";
}
}

View File

@ -1,99 +0,0 @@
package net.hostsharing.hsadminng.hs.hosting.asset.validator;
import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetEntity;
import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.Set;
import static java.util.Arrays.stream;
import static net.hostsharing.hsadminng.hs.hosting.asset.validator.EnumPropertyValidator.enumerationProperty;
import static net.hostsharing.hsadminng.hs.hosting.asset.validator.HsHostingAssetPropertyValidator.defType;
import static net.hostsharing.hsadminng.hs.hosting.asset.validator.BooleanPropertyValidator.booleanProperty;
import static net.hostsharing.hsadminng.hs.hosting.asset.validator.IntegerPropertyValidator.integerProperty;
public class HsHostingAssetValidator {
private static final Map<HsHostingAssetType, HsHostingAssetValidator> validators = Map.ofEntries(
defType(HsHostingAssetType.CLOUD_SERVER, new HsHostingAssetValidator(
integerProperty("CPUs").min(1).max(32).required(),
integerProperty("RAM").unit("GB").min(1).max(128).required(),
integerProperty("SSD").unit("GB").min(25).max(1000).step(25).required(),
integerProperty("HDD").unit("GB").min(0).max(4000).step(250).optional(),
integerProperty("Traffic").unit("GB").min(250).max(10000).step(250).required(),
enumerationProperty("SLA-Infrastructure").values("BASIC", "EXT8H", "EXT4H", "EXT2H").optional())),
defType(HsHostingAssetType.MANAGED_SERVER, new HsHostingAssetValidator(
integerProperty("CPUs").min(1).max(32).required(),
integerProperty("RAM").unit("GB").min(1).max(128).required(),
integerProperty("SSD").unit("GB").min(25).max(1000).step(25).required(),
integerProperty("HDD").unit("GB").min(0).max(4000).step(250).optional(),
integerProperty("Traffic").unit("GB").min(250).max(10000).step(250).required(),
enumerationProperty("SLA-Platform").values("BASIC", "EXT8H", "EXT4H", "EXT2H").optional(),
booleanProperty("SLA-EMail").falseIf("SLA-Platform", "BASIC").optional(),
booleanProperty("SLA-Maria").falseIf("SLA-Platform", "BASIC").optional(),
booleanProperty("SLA-PgSQL").falseIf("SLA-Platform", "BASIC").optional(),
booleanProperty("SLA-Office").falseIf("SLA-Platform", "BASIC").optional(),
booleanProperty("SLA-Web").falseIf("SLA-Platform", "BASIC").optional())),
defType(HsHostingAssetType.MANAGED_WEBSPACE, new HsHostingAssetValidator(
integerProperty("SSD").unit("GB").min(1).max(100).step(1).required(),
integerProperty("HDD").unit("GB").min(0).max(250).step(10).optional(),
integerProperty("Traffic").unit("GB").min(10).max(1000).step(10).required(),
enumerationProperty("SLA-Platform").values("BASIC", "EXT24H").optional(),
integerProperty("Daemons").min(0).max(10).optional(),
booleanProperty("Online Office Server").optional())
));
static {
validators.entrySet().forEach(typeDef -> {
stream(typeDef.getValue().propertyValidators).forEach( entry -> {
entry.verifyConsistency(typeDef);
});
});
}
private final HsHostingAssetPropertyValidator<?>[] propertyValidators;
public static HsHostingAssetValidator forType(final HsHostingAssetType type) {
return validators.get(type);
}
HsHostingAssetValidator(final HsHostingAssetPropertyValidator<?>... validators) {
propertyValidators = validators;
}
public static Set<HsHostingAssetType> types() {
return validators.keySet();
}
public List<String> validate(final HsHostingAssetEntity assetEntity) {
final var result = new ArrayList<String>();
assetEntity.getConfig().keySet().forEach( givenPropName -> {
if (stream(propertyValidators).map(pv -> pv.propertyName).noneMatch(propName -> propName.equals(givenPropName))) {
result.add("'" + givenPropName + "' is not expected but is '" +assetEntity.getConfig().get(givenPropName) + "'");
}
});
stream(propertyValidators).forEach(pv -> {
result.addAll(pv.validate(assetEntity.getConfig()));
});
return result;
}
public List<Map<String, Object>> properties() {
final var mapper = new ObjectMapper();
mapper.setVisibility(PropertyAccessor.FIELD, JsonAutoDetect.Visibility.ANY);
return Arrays.stream(propertyValidators)
.map(propertyValidator -> propertyValidator.toMap(mapper))
.map(HsHostingAssetValidator::asKeyValueMap)
.toList();
}
@SuppressWarnings({ "unchecked", "rawtypes" })
private static Map<String, Object> asKeyValueMap(final Map map) {
return (Map<String, Object>) map;
}
}

View File

@ -0,0 +1,20 @@
package net.hostsharing.hsadminng.hs.hosting.asset.validators;
import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetEntity;
import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType;
import net.hostsharing.hsadminng.hs.validation.HsEntityValidator;
import static net.hostsharing.hsadminng.hs.validation.IntegerPropertyValidator.integerProperty;
class HsCloudServerHostingAssetValidator extends HsEntityValidator<HsHostingAssetEntity, HsHostingAssetType> {
public HsCloudServerHostingAssetValidator() {
super(
integerProperty("CPUs").min(1).max(32).required(),
integerProperty("RAM").unit("GB").min(1).max(128).required(),
integerProperty("SSD").unit("GB").min(25).max(1000).step(25).required(),
integerProperty("HDD").unit("GB").min(0).max(4000).step(250).optional(),
integerProperty("Traffic").unit("GB").min(250).max(10000).step(250).required()
);
}
}

View File

@ -0,0 +1,51 @@
package net.hostsharing.hsadminng.hs.hosting.asset.validators;
import lombok.experimental.UtilityClass;
import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetEntity;
import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType;
import net.hostsharing.hsadminng.hs.validation.HsEntityValidator;
import jakarta.validation.ValidationException;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import static java.util.Arrays.stream;
import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.CLOUD_SERVER;
import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.MANAGED_SERVER;
import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.MANAGED_WEBSPACE;
@UtilityClass
public class HsHostingAssetEntityValidators {
private static final Map<Enum<HsHostingAssetType>, HsEntityValidator<HsHostingAssetEntity, HsHostingAssetType>> validators = new HashMap<>();
static {
register(CLOUD_SERVER, new HsCloudServerHostingAssetValidator());
register(MANAGED_SERVER, new HsManagedServerHostingAssetValidator());
register(MANAGED_WEBSPACE, new HsManagedWebspaceHostingAssetValidator());
}
private static void register(final Enum<HsHostingAssetType> type, final HsEntityValidator<HsHostingAssetEntity, HsHostingAssetType> validator) {
stream(validator.propertyValidators).forEach( entry -> {
entry.verifyConsistency(Map.entry(type, validator));
});
validators.put(type, validator);
}
public static HsEntityValidator<HsHostingAssetEntity, HsHostingAssetType> forType(final Enum<HsHostingAssetType> type) {
return validators.get(type);
}
public static Set<Enum<HsHostingAssetType>> types() {
return validators.keySet();
}
public static HsHostingAssetEntity valid(final HsHostingAssetEntity entityToSave) {
final var violations = HsHostingAssetEntityValidators.forType(entityToSave.getType()).validate(entityToSave);
if (!violations.isEmpty()) {
throw new ValidationException(violations.toString());
}
return entityToSave;
}
}

View File

@ -0,0 +1,20 @@
package net.hostsharing.hsadminng.hs.hosting.asset.validators;
import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetEntity;
import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType;
import net.hostsharing.hsadminng.hs.validation.HsEntityValidator;
import static net.hostsharing.hsadminng.hs.validation.IntegerPropertyValidator.integerProperty;
class HsManagedServerHostingAssetValidator extends HsEntityValidator<HsHostingAssetEntity, HsHostingAssetType> {
public HsManagedServerHostingAssetValidator() {
super(
integerProperty("CPUs").min(1).max(32).required(),
integerProperty("RAM").unit("GB").min(1).max(128).required(),
integerProperty("SSD").unit("GB").min(25).max(1000).step(25).required(),
integerProperty("HDD").unit("GB").min(0).max(4000).step(250).optional(),
integerProperty("Traffic").unit("GB").min(250).max(10000).step(250).required()
);
}
}

View File

@ -0,0 +1,34 @@
package net.hostsharing.hsadminng.hs.hosting.asset.validators;
import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetEntity;
import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType;
import net.hostsharing.hsadminng.hs.validation.HsEntityValidator;
import java.util.List;
import static net.hostsharing.hsadminng.hs.validation.IntegerPropertyValidator.integerProperty;
class HsManagedWebspaceHostingAssetValidator extends HsEntityValidator<HsHostingAssetEntity, HsHostingAssetType> {
public HsManagedWebspaceHostingAssetValidator() {
super(
integerProperty("SSD").unit("GB").min(1).max(100).step(1).required(),
integerProperty("HDD").unit("GB").min(0).max(250).step(10).optional(),
integerProperty("Traffic").unit("GB").min(10).max(1000).step(10).required()
);
}
@Override
public List<String> validate(final HsHostingAssetEntity assetEntity) {
final var result = super.validate(assetEntity);
validateIdentifierPattern(result, assetEntity);
return result;
}
private static void validateIdentifierPattern(final List<String> result, final HsHostingAssetEntity assetEntity) {
final var expectedIdentifierPattern = "^" + assetEntity.getParentAsset().getBookingItem().getDebitor().getDefaultPrefix() + "[0-9][0-9]$";
if ( !assetEntity.getIdentifier().matches(expectedIdentifierPattern)) {
result.add("'identifier' expected to match '"+expectedIdentifierPattern+"', but is '" + assetEntity.getIdentifier() + "'");
}
}
}

View File

@ -0,0 +1,42 @@
package net.hostsharing.hsadminng.hs.validation;
import lombok.Setter;
import java.util.AbstractMap;
import java.util.ArrayList;
import java.util.Map;
import java.util.Objects;
@Setter
public class BooleanPropertyValidator extends HsPropertyValidator<Boolean> {
private Map.Entry<String, String> falseIf;
private BooleanPropertyValidator(final String propertyName) {
super(Boolean.class, propertyName);
}
public static BooleanPropertyValidator booleanProperty(final String propertyName) {
return new BooleanPropertyValidator(propertyName);
}
public HsPropertyValidator<Boolean> falseIf(final String refPropertyName, final String refPropertyValue) {
this.falseIf = new AbstractMap.SimpleImmutableEntry<>(refPropertyName, refPropertyValue);
return this;
}
@Override
protected void validate(final ArrayList<String> result, final String propertiesName, final Boolean propValue, final Map<String, Object> props) {
if (falseIf != null && !Objects.equals(props.get(falseIf.getKey()), falseIf.getValue())) {
if (propValue) {
result.add("'"+propertiesName+"." + propertyName + "' is expected to be false because " +
propertiesName+"." + falseIf.getKey()+ "=" + falseIf.getValue() + " but is " + propValue);
}
}
}
@Override
protected String simpleTypeName() {
return "boolean";
}
}

View File

@ -0,0 +1,38 @@
package net.hostsharing.hsadminng.hs.validation;
import lombok.Setter;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Map;
@Setter
public class EnumerationPropertyValidator extends HsPropertyValidator<String> {
private String[] values;
private EnumerationPropertyValidator(final String propertyName) {
super(String.class, propertyName);
}
public static EnumerationPropertyValidator enumerationProperty(final String propertyName) {
return new EnumerationPropertyValidator(propertyName);
}
public HsPropertyValidator<String> values(final String... values) {
this.values = values;
return this;
}
@Override
protected void validate(final ArrayList<String> result, final String propertiesName, final String propValue, final Map<String, Object> props) {
if (Arrays.stream(values).noneMatch(v -> v.equals(propValue))) {
result.add("'"+propertiesName+"." + propertyName + "' is expected to be one of " + Arrays.toString(values) + " but is '" + propValue + "'");
}
}
@Override
protected String simpleTypeName() {
return "enumeration";
}
}

View File

@ -0,0 +1,49 @@
package net.hostsharing.hsadminng.hs.validation;
import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import static java.util.Arrays.stream;
public class HsEntityValidator<E extends Validatable<E, T>, T extends Enum<T>> {
public final HsPropertyValidator<?>[] propertyValidators;
public HsEntityValidator(final HsPropertyValidator<?>... validators) {
propertyValidators = validators;
}
public List<String> validate(final E assetEntity) {
final var result = new ArrayList<String>();
assetEntity.getProperties().keySet().forEach( givenPropName -> {
if (stream(propertyValidators).map(pv -> pv.propertyName).noneMatch(propName -> propName.equals(givenPropName))) {
result.add("'"+assetEntity.getPropertiesName()+"." + givenPropName + "' is not expected but is set to '" +assetEntity.getProperties().get(givenPropName) + "'");
}
});
stream(propertyValidators).forEach(pv -> {
result.addAll(pv.validate(assetEntity.getPropertiesName(), assetEntity.getProperties()));
});
return result;
}
public List<Map<String, Object>> properties() {
final var mapper = new ObjectMapper();
mapper.setVisibility(PropertyAccessor.FIELD, JsonAutoDetect.Visibility.ANY);
return Arrays.stream(propertyValidators)
.map(propertyValidator -> propertyValidator.toMap(mapper))
.map(HsEntityValidator::asKeyValueMap)
.toList();
}
@SuppressWarnings({ "unchecked", "rawtypes" })
private static Map<String, Object> asKeyValueMap(final Map map) {
return (Map<String, Object>) map;
}
}

View File

@ -0,0 +1,67 @@
package net.hostsharing.hsadminng.hs.validation;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.RequiredArgsConstructor;
import java.util.AbstractMap.SimpleImmutableEntry;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
@RequiredArgsConstructor
public abstract class HsPropertyValidator<T> {
final Class<T> type;
final String propertyName;
private Boolean required;
public static <K, V> Map.Entry<K, V> defType(K k, V v) {
return new SimpleImmutableEntry<>(k, v);
}
public HsPropertyValidator<T> required() {
required = Boolean.TRUE;
return this;
}
public HsPropertyValidator<T> optional() {
required = Boolean.FALSE;
return this;
}
public final List<String> validate(final String propertiesName, final Map<String, Object> props) {
final var result = new ArrayList<String>();
final var propValue = props.get(propertyName);
if (propValue == null) {
if (required) {
result.add("'"+propertiesName+"." + propertyName + "' is required but missing");
}
}
if (propValue != null){
if ( type.isInstance(propValue)) {
//noinspection unchecked
validate(result, propertiesName, (T) propValue, props);
} else {
result.add("'"+propertiesName+"." + propertyName + "' is expected to be of type " + type + ", " +
"but is of type '" + propValue.getClass().getSimpleName() + "'");
}
}
return result;
}
protected abstract void validate(final ArrayList<String> result, final String propertiesName, final T propValue, final Map<String, Object> props);
public void verifyConsistency(final Map.Entry<? extends Enum<?>, ?> typeDef) {
if (required == null ) {
throw new IllegalStateException(typeDef.getKey() + "[" + propertyName + "] not fully initialized, please call either .required() or .optional()" );
}
}
public Map<String, Object> toMap(final ObjectMapper mapper) {
final Map<String, Object> map = mapper.convertValue(this, Map.class);
map.put("type", simpleTypeName());
return map;
}
protected abstract String simpleTypeName();
}

View File

@ -0,0 +1,42 @@
package net.hostsharing.hsadminng.hs.validation;
import lombok.Setter;
import java.util.ArrayList;
import java.util.Map;
@Setter
public class IntegerPropertyValidator extends HsPropertyValidator<Integer> {
private String unit;
private Integer min;
private Integer max;
private Integer step;
public static IntegerPropertyValidator integerProperty(final String propertyName) {
return new IntegerPropertyValidator(propertyName);
}
private IntegerPropertyValidator(final String propertyName) {
super(Integer.class, propertyName);
}
@Override
protected void validate(final ArrayList<String> result, final String propertiesName, final Integer propValue, final Map<String, Object> props) {
if (min != null && propValue < min) {
result.add("'"+propertiesName+"." + propertyName + "' is expected to be >= " + min + " but is " + propValue);
}
if (max != null && propValue > max) {
result.add("'"+propertiesName+"." + propertyName + "' is expected to be <= " + max + " but is " + propValue);
}
if (step != null && propValue % step != 0) {
result.add("'"+propertiesName+"." + propertyName + "' is expected to be multiple of " + step + " but is " + propValue);
}
}
@Override
protected String simpleTypeName() {
return "integer";
}
}

View File

@ -0,0 +1,13 @@
package net.hostsharing.hsadminng.hs.validation;
import java.util.Map;
public interface Validatable<E, T extends Enum<T>> {
Enum<T> getType();
String getPropertiesName();
Map<String, Object> getProperties();
}

View File

@ -8,6 +8,7 @@ import jakarta.persistence.EntityManager;
import jakarta.persistence.PersistenceContext; import jakarta.persistence.PersistenceContext;
import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.NotNull;
import java.io.BufferedWriter; import java.io.BufferedWriter;
import java.io.File;
import java.io.FileWriter; import java.io.FileWriter;
import java.io.IOException; import java.io.IOException;
import java.util.*; import java.util.*;
@ -25,6 +26,7 @@ public class RbacGrantsDiagramService {
public static void writeToFile(final String title, final String graph, final String fileName) { public static void writeToFile(final String title, final String graph, final String fileName) {
new File("doc/temp").mkdirs();
try (BufferedWriter writer = new BufferedWriter(new FileWriter(fileName))) { try (BufferedWriter writer = new BufferedWriter(new FileWriter(fileName))) {
writer.write(""" writer.write("""
### all grants to %s ### all grants to %s
@ -192,8 +194,9 @@ public class RbacGrantsDiagramService {
return "[" + roleType + "\nref:" + uuid + "]"; return "[" + roleType + "\nref:" + uuid + "]";
} }
if (refType.equals("perm")) { if (refType.equals("perm")) {
final var roleType = idName.split(":")[1]; final var parts = idName.split(":");
return "{{" + roleType + "\nref:" + uuid + "}}"; final var permType = parts[2];
return "{{" + permType + "\nref:" + uuid + "}}";
} }
return ""; return "";
} }
@ -205,7 +208,7 @@ public class RbacGrantsDiagramService {
@NotNull @NotNull
private static String cleanId(final String idName) { private static String cleanId(final String idName) {
return idName.replaceAll("@.*", "") return idName.replaceAll("@.*", "")
.replace("[", "").replace("]", "").replace("(", "").replace(")", "").replace(",", ""); .replace("[", "").replace("]", "").replace("(", "").replace(")", "").replace(",", "").replace(">", ":");
} }

View File

@ -53,7 +53,11 @@ components:
bookingItemUuid: bookingItemUuid:
type: string type: string
format: uuid format: uuid
nullable: false nullable: true
parentAssetUuid:
type: string
format: uuid
nullable: true
type: type:
$ref: '#/components/schemas/HsHostingAssetType' $ref: '#/components/schemas/HsHostingAssetType'
identifier: identifier:
@ -72,7 +76,6 @@ components:
- type - type
- identifier - identifier
- caption - caption
- debitorUuid
- config - config
additionalProperties: false additionalProperties: false

View File

@ -32,9 +32,9 @@ begin
raise notice '- using debitor (%): %', relatedDebitor.uuid, relatedDebitor; raise notice '- using debitor (%): %', relatedDebitor.uuid, relatedDebitor;
insert insert
into hs_booking_item (uuid, debitoruuid, type, caption, validity, resources) into hs_booking_item (uuid, debitoruuid, type, caption, validity, resources)
values (uuid_generate_v4(), relatedDebitor.uuid, 'MANAGED_SERVER', 'some ManagedServer', daterange('20221001', null, '[]'), '{ "CPU": 2, "SDD": 512, "extra": 42 }'::jsonb), values (uuid_generate_v4(), relatedDebitor.uuid, 'MANAGED_SERVER', 'some ManagedServer', daterange('20221001', null, '[]'), '{ "CPUs": 2, "RAM": 8, "SDD": 512, "Traffic": 42 }'::jsonb),
(uuid_generate_v4(), relatedDebitor.uuid, 'CLOUD_SERVER', 'some CloudServer', daterange('20230115', '20240415', '[)'), '{ "CPU": 2, "HDD": 1024, "extra": 42 }'::jsonb), (uuid_generate_v4(), relatedDebitor.uuid, 'CLOUD_SERVER', 'some CloudServer', daterange('20230115', '20240415', '[)'), '{ "CPUs": 2, "RAM": 4, "HDD": 1024, "Traffic": 42 }'::jsonb),
(uuid_generate_v4(), relatedDebitor.uuid, 'PRIVATE_CLOUD', 'some PrivateCloud', daterange('20240401', null, '[]'), '{ "CPU": 10, "SDD": 10240, "HDD": 10240, "extra": 42 }'::jsonb); (uuid_generate_v4(), relatedDebitor.uuid, 'PRIVATE_CLOUD', 'some PrivateCloud', daterange('20240401', null, '[]'), '{ "CPUs": 10, "SDD": 10240, "HDD": 10240, "Traffic": 42 }'::jsonb);
end; $$; end; $$;
--// --//

View File

@ -24,12 +24,14 @@ create table if not exists hs_hosting_asset
( (
uuid uuid unique references RbacObject (uuid), uuid uuid unique references RbacObject (uuid),
version int not null default 0, version int not null default 0,
bookingItemUuid uuid not null references hs_booking_item(uuid), bookingItemUuid uuid null references hs_booking_item(uuid),
type HsHostingAssetType not null, type HsHostingAssetType not null,
parentAssetUuid uuid null references hs_hosting_asset(uuid), parentAssetUuid uuid null references hs_hosting_asset(uuid),
identifier varchar(80) not null, identifier varchar(80) not null,
caption varchar(80) not null, caption varchar(80) not null,
config jsonb not null config jsonb not null,
constraint chk_hs_hosting_asset_has_booking_item_or_parent_asset check (bookingItemUuid is not null or parentAssetUuid is not null)
); );
--// --//

View File

@ -39,8 +39,6 @@ begin
SELECT * FROM hs_hosting_asset WHERE uuid = NEW.parentAssetUuid INTO newParentServer; SELECT * FROM hs_hosting_asset WHERE uuid = NEW.parentAssetUuid INTO newParentServer;
SELECT * FROM hs_booking_item WHERE uuid = NEW.bookingItemUuid INTO newBookingItem; SELECT * FROM hs_booking_item WHERE uuid = NEW.bookingItemUuid INTO newBookingItem;
assert newBookingItem.uuid is not null, format('newBookingItem must not be null for NEW.bookingItemUuid = %s', NEW.bookingItemUuid);
perform createRoleWithGrants( perform createRoleWithGrants(
hsHostingAssetOWNER(NEW), hsHostingAssetOWNER(NEW),

View File

@ -21,6 +21,7 @@ import java.util.Map;
import java.util.UUID; import java.util.UUID;
import static java.util.Map.entry; import static java.util.Map.entry;
import static net.hostsharing.hsadminng.hs.booking.item.HsBookingItemType.MANAGED_WEBSPACE;
import static net.hostsharing.hsadminng.rbac.test.JsonMatcher.lenientlyEquals; 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;
@ -69,34 +70,39 @@ class HsBookingItemControllerAcceptanceTest extends ContextBasedTestWithCleanup
.body("", lenientlyEquals(""" .body("", lenientlyEquals("""
[ [
{ {
"type": "MANAGED_SERVER",
"caption": "some ManagedServer", "caption": "some ManagedServer",
"validFrom": "2022-10-01", "validFrom": "2022-10-01",
"validTo": null, "validTo": null,
"resources": { "resources": {
"CPU": 2, "RAM": 8,
"SDD": 512, "SDD": 512,
"extra": 42 "CPUs": 2,
"Traffic": 42
} }
}, },
{ {
"type": "CLOUD_SERVER",
"caption": "some CloudServer", "caption": "some CloudServer",
"validFrom": "2023-01-15", "validFrom": "2023-01-15",
"validTo": "2024-04-14", "validTo": "2024-04-14",
"resources": { "resources": {
"CPU": 2,
"HDD": 1024, "HDD": 1024,
"extra": 42 "RAM": 4,
"CPUs": 2,
"Traffic": 42
} }
}, },
{ {
"type": "PRIVATE_CLOUD",
"caption": "some PrivateCloud", "caption": "some PrivateCloud",
"validFrom": "2024-04-01", "validFrom": "2024-04-01",
"validTo": null, "validTo": null,
"resources": { "resources": {
"CPU": 10,
"HDD": 10240, "HDD": 10240,
"SDD": 10240, "SDD": 10240,
"extra": 42 "CPUs": 10,
"Traffic": 42
} }
} }
] ]
@ -123,7 +129,7 @@ class HsBookingItemControllerAcceptanceTest extends ContextBasedTestWithCleanup
"debitorUuid": "%s", "debitorUuid": "%s",
"type": "MANAGED_SERVER", "type": "MANAGED_SERVER",
"caption": "some new booking", "caption": "some new booking",
"resources": { "CPU": 12, "extra": 42 }, "resources": { "CPUs": 12, "RAM": 4, "SSD": 100, "Traffic": 250 },
"validFrom": "2022-10-13" "validFrom": "2022-10-13"
} }
""".formatted(givenDebitor.getUuid())) """.formatted(givenDebitor.getUuid()))
@ -139,7 +145,7 @@ class HsBookingItemControllerAcceptanceTest extends ContextBasedTestWithCleanup
"caption": "some new booking", "caption": "some new booking",
"validFrom": "2022-10-13", "validFrom": "2022-10-13",
"validTo": null, "validTo": null,
"resources": { "CPU": 12 } "resources": { "CPUs": 12, "SSD": 100, "Traffic": 250 }
} }
""")) """))
.header("Location", matchesRegex("http://localhost:[1-9][0-9]*/api/hs/booking/items/[^/]*")) .header("Location", matchesRegex("http://localhost:[1-9][0-9]*/api/hs/booking/items/[^/]*"))
@ -177,7 +183,12 @@ class HsBookingItemControllerAcceptanceTest extends ContextBasedTestWithCleanup
"caption": "some CloudServer", "caption": "some CloudServer",
"validFrom": "2023-01-15", "validFrom": "2023-01-15",
"validTo": "2024-04-14", "validTo": "2024-04-14",
"resources": { CPU: 2, HDD: 1024 } "resources": {
"HDD": 1024,
"RAM": 4,
"CPUs": 2,
"Traffic": 42
}
} }
""")); // @formatter:on """)); // @formatter:on
} }
@ -222,7 +233,12 @@ class HsBookingItemControllerAcceptanceTest extends ContextBasedTestWithCleanup
"caption": "some CloudServer", "caption": "some CloudServer",
"validFrom": "2023-01-15", "validFrom": "2023-01-15",
"validTo": "2024-04-14", "validTo": "2024-04-14",
"resources": { CPU: 2, HDD: 1024 } "resources": {
"HDD": 1024,
"RAM": 4,
"CPUs": 2,
"Traffic": 42
}
} }
""")); // @formatter:on """)); // @formatter:on
} }
@ -234,7 +250,8 @@ class HsBookingItemControllerAcceptanceTest extends ContextBasedTestWithCleanup
@Test @Test
void globalAdmin_canPatchAllUpdatablePropertiesOfBookingItem() { void globalAdmin_canPatchAllUpdatablePropertiesOfBookingItem() {
final var givenBookingItem = givenSomeTemporaryBookingItemForDebitorNumber(1000111, entry("something", 1)); final var givenBookingItem = givenSomeBookingItem(1000111, MANAGED_WEBSPACE,
resource("HDD", 100), resource("SSD", 50), resource("Traffic", 250));
RestAssured // @formatter:off RestAssured // @formatter:off
.given() .given()
@ -245,9 +262,9 @@ class HsBookingItemControllerAcceptanceTest extends ContextBasedTestWithCleanup
"validFrom": "2020-06-05", "validFrom": "2020-06-05",
"validTo": "2022-12-31", "validTo": "2022-12-31",
"resources": { "resources": {
"CPU": "4", "Traffic": 500,
"HDD": null, "HDD": null,
"SSD": "4096" "SSD": 100
} }
} }
""") """)
@ -263,9 +280,8 @@ class HsBookingItemControllerAcceptanceTest extends ContextBasedTestWithCleanup
"validFrom": "2022-11-01", "validFrom": "2022-11-01",
"validTo": "2022-12-31", "validTo": "2022-12-31",
"resources": { "resources": {
"CPU": "4", "Traffic": 500,
"SSD": "4096", "SSD": 100
"something": 1
} }
} }
""")); // @formatter:on """)); // @formatter:on
@ -288,7 +304,8 @@ class HsBookingItemControllerAcceptanceTest extends ContextBasedTestWithCleanup
@Test @Test
void globalAdmin_canDeleteArbitraryBookingItem() { void globalAdmin_canDeleteArbitraryBookingItem() {
context.define("superuser-alex@hostsharing.net"); context.define("superuser-alex@hostsharing.net");
final var givenBookingItem = givenSomeTemporaryBookingItemForDebitorNumber(1000111, entry("something", 1)); final var givenBookingItem = givenSomeBookingItem(1000111, MANAGED_WEBSPACE,
resource("HDD", 100), resource("SSD", 50), resource("Traffic", 250));
RestAssured // @formatter:off RestAssured // @formatter:off
.given() .given()
@ -306,7 +323,8 @@ class HsBookingItemControllerAcceptanceTest extends ContextBasedTestWithCleanup
@Test @Test
void normalUser_canNotDeleteUnrelatedBookingItem() { void normalUser_canNotDeleteUnrelatedBookingItem() {
context.define("superuser-alex@hostsharing.net"); context.define("superuser-alex@hostsharing.net");
final var givenBookingItem = givenSomeTemporaryBookingItemForDebitorNumber(1000111, entry("something", 1)); final var givenBookingItem = givenSomeBookingItem(1000111, MANAGED_WEBSPACE,
resource("HDD", 100), resource("SSD", 50), resource("Traffic", 250));
RestAssured // @formatter:off RestAssured // @formatter:off
.given() .given()
@ -322,15 +340,16 @@ class HsBookingItemControllerAcceptanceTest extends ContextBasedTestWithCleanup
} }
} }
private HsBookingItemEntity givenSomeTemporaryBookingItemForDebitorNumber(final int debitorNumber, @SafeVarargs
final Map.Entry<String, Integer> resources) { private HsBookingItemEntity givenSomeBookingItem(final int debitorNumber,
final HsBookingItemType hsBookingItemType, final Map.Entry<String, Object>... resources) {
return jpaAttempt.transacted(() -> { return jpaAttempt.transacted(() -> {
context.define("superuser-alex@hostsharing.net"); context.define("superuser-alex@hostsharing.net");
final var givenDebitor = debitorRepo.findDebitorByDebitorNumber(debitorNumber).get(0); final var givenDebitor = debitorRepo.findDebitorByDebitorNumber(debitorNumber).get(0);
final var newBookingItem = HsBookingItemEntity.builder() final var newBookingItem = HsBookingItemEntity.builder()
.uuid(UUID.randomUUID()) .uuid(UUID.randomUUID())
.debitor(givenDebitor) .debitor(givenDebitor)
.type(HsBookingItemType.MANAGED_WEBSPACE) .type(hsBookingItemType)
.caption("some test-booking") .caption("some test-booking")
.resources(Map.ofEntries(resources)) .resources(Map.ofEntries(resources))
.validity(Range.closedOpen( .validity(Range.closedOpen(
@ -340,4 +359,8 @@ class HsBookingItemControllerAcceptanceTest extends ContextBasedTestWithCleanup
return bookingItemRepo.save(newBookingItem); return bookingItemRepo.save(newBookingItem);
}).assertSuccessful().returnedValue(); }).assertSuccessful().returnedValue();
} }
private Map.Entry<String, Object> resource(final String key, final Object value) {
return entry(key, value);
}
} }

View File

@ -167,9 +167,9 @@ class HsBookingItemRepositoryIntegrationTest extends ContextBasedTestWithCleanup
// then // then
allTheseBookingItemsAreReturned( allTheseBookingItemsAreReturned(
result, result,
"HsBookingItemEntity(D-1000212, MANAGED_SERVER, [2022-10-01,), some ManagedServer, { CPU: 2, SDD: 512, extra: 42 })", "HsBookingItemEntity(D-1000212, MANAGED_SERVER, [2022-10-01,), some ManagedServer, { CPUs: 2, RAM: 8, SDD: 512, Traffic: 42 })",
"HsBookingItemEntity(D-1000212, CLOUD_SERVER, [2023-01-15,2024-04-15), some CloudServer, { CPU: 2, HDD: 1024, extra: 42 })", "HsBookingItemEntity(D-1000212, CLOUD_SERVER, [2023-01-15,2024-04-15), some CloudServer, { CPUs: 2, HDD: 1024, RAM: 4, Traffic: 42 })",
"HsBookingItemEntity(D-1000212, PRIVATE_CLOUD, [2024-04-01,), some PrivateCloud, { CPU: 10, HDD: 10240, SDD: 10240, extra: 42 })"); "HsBookingItemEntity(D-1000212, PRIVATE_CLOUD, [2024-04-01,), some PrivateCloud, { CPUs: 10, HDD: 10240, SDD: 10240, Traffic: 42 })");
} }
@Test @Test
@ -184,9 +184,9 @@ class HsBookingItemRepositoryIntegrationTest extends ContextBasedTestWithCleanup
// then: // then:
exactlyTheseBookingItemsAreReturned( exactlyTheseBookingItemsAreReturned(
result, result,
"HsBookingItemEntity(D-1000111, MANAGED_SERVER, [2022-10-01,), some ManagedServer, { CPU: 2, SDD: 512, extra: 42 })", "HsBookingItemEntity(D-1000111, MANAGED_SERVER, [2022-10-01,), some ManagedServer, { CPUs: 2, RAM: 8, SDD: 512, Traffic: 42 })",
"HsBookingItemEntity(D-1000111, CLOUD_SERVER, [2023-01-15,2024-04-15), some CloudServer, { CPU: 2, HDD: 1024, extra: 42 })", "HsBookingItemEntity(D-1000111, CLOUD_SERVER, [2023-01-15,2024-04-15), some CloudServer, { CPUs: 2, HDD: 1024, RAM: 4, Traffic: 42 })",
"HsBookingItemEntity(D-1000111, PRIVATE_CLOUD, [2024-04-01,), some PrivateCloud, { CPU: 10, HDD: 10240, SDD: 10240, extra: 42 })"); "HsBookingItemEntity(D-1000111, PRIVATE_CLOUD, [2024-04-01,), some PrivateCloud, { CPUs: 10, HDD: 10240, SDD: 10240, Traffic: 42 })");
} }
} }

View File

@ -0,0 +1,44 @@
package net.hostsharing.hsadminng.hs.booking.item.validators;
import net.hostsharing.hsadminng.hs.booking.item.HsBookingItemEntity;
import org.junit.jupiter.api.Test;
import jakarta.validation.ValidationException;
import static net.hostsharing.hsadminng.hs.booking.item.HsBookingItemType.MANAGED_SERVER;
import static net.hostsharing.hsadminng.hs.booking.item.HsBookingItemType.MANAGED_WEBSPACE;
import static net.hostsharing.hsadminng.hs.booking.item.validators.HsBookingItemEntityValidators.valid;
import static net.hostsharing.hsadminng.hs.booking.item.HsBookingItemType.CLOUD_SERVER;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.catchThrowable;
class HsBookingItemEntityValidatorsUnitTest {
@Test
void validThrowsException() {
// given
final var cloudServerBookingItemEntity = HsBookingItemEntity.builder()
.type(CLOUD_SERVER)
.build();
// when
final var result = catchThrowable( ()-> valid(cloudServerBookingItemEntity) );
// then
assertThat(result).isInstanceOf(ValidationException.class)
.hasMessageContaining(
"'resources.CPUs' is required but missing",
"'resources.RAM' is required but missing",
"'resources.SSD' is required but missing",
"'resources.Traffic' is required but missing");
}
@Test
void listsTypes() {
// when
final var result = HsBookingItemEntityValidators.types();
// then
assertThat(result).containsExactlyInAnyOrder(CLOUD_SERVER, MANAGED_SERVER, MANAGED_WEBSPACE);
}
}

View File

@ -0,0 +1,51 @@
package net.hostsharing.hsadminng.hs.booking.item.validators;
import net.hostsharing.hsadminng.hs.booking.item.HsBookingItemEntity;
import org.junit.jupiter.api.Test;
import java.util.Map;
import static java.util.Map.entry;
import static net.hostsharing.hsadminng.hs.booking.item.HsBookingItemType.CLOUD_SERVER;
import static net.hostsharing.hsadminng.hs.booking.item.validators.HsBookingItemEntityValidators.forType;
import static org.assertj.core.api.Assertions.assertThat;
class HsCloudServerBookingItemValidatorUnitTest {
@Test
void validatesProperties() {
// given
final var validator = HsBookingItemEntityValidators.forType(CLOUD_SERVER);
final var cloudServerBookingItemEntity = HsBookingItemEntity.builder()
.type(CLOUD_SERVER)
.resources(Map.ofEntries(
entry("CPUs", 2),
entry("RAM", 25),
entry("SSD", 25),
entry("Traffic", 250),
entry("SLA-EMail", true)
))
.build();
// when
final var result = validator.validate(cloudServerBookingItemEntity);
// then
assertThat(result).containsExactly("'resources.SLA-EMail' is not expected but is set to 'true'");
}
@Test
void containsAllValidations() {
// when
final var validator = forType(CLOUD_SERVER);
// then
assertThat(validator.properties()).map(Map::toString).containsExactlyInAnyOrder(
"{type=integer, propertyName=CPUs, required=true, unit=null, min=1, max=32, step=null}",
"{type=integer, propertyName=RAM, required=true, unit=GB, min=1, max=128, step=null}",
"{type=integer, propertyName=SSD, required=true, unit=GB, min=25, max=1000, step=25}",
"{type=integer, propertyName=HDD, required=false, unit=GB, min=0, max=4000, step=250}",
"{type=integer, propertyName=Traffic, required=true, unit=GB, min=250, max=10000, step=250}",
"{type=enumeration, propertyName=SLA-Infrastructure, required=false, values=[BASIC, EXT8H, EXT4H, EXT2H]}");
}
}

View File

@ -0,0 +1,56 @@
package net.hostsharing.hsadminng.hs.booking.item.validators;
import net.hostsharing.hsadminng.hs.booking.item.HsBookingItemEntity;
import org.junit.jupiter.api.Test;
import java.util.Map;
import static java.util.Map.entry;
import static net.hostsharing.hsadminng.hs.booking.item.HsBookingItemType.MANAGED_SERVER;
import static net.hostsharing.hsadminng.hs.booking.item.validators.HsBookingItemEntityValidators.forType;
import static org.assertj.core.api.Assertions.assertThat;
class HsManagedServerBookingItemValidatorUnitTest {
@Test
void validatesProperties() {
// given
final var validator = HsBookingItemEntityValidators.forType(MANAGED_SERVER);
final var mangedServerBookingItemEntity = HsBookingItemEntity.builder()
.type(MANAGED_SERVER)
.resources(Map.ofEntries(
entry("CPUs", 2),
entry("RAM", 25),
entry("SSD", 25),
entry("Traffic", 250),
entry("SLA-EMail", true)
))
.build();
// when
final var result = validator.validate(mangedServerBookingItemEntity);
// then
assertThat(result).containsExactly("'resources.SLA-EMail' is expected to be false because resources.SLA-Platform=BASIC but is true");
}
@Test
void containsAllValidations() {
// when
final var validator = forType(MANAGED_SERVER);
// then
assertThat(validator.properties()).map(Map::toString).containsExactlyInAnyOrder(
"{type=integer, propertyName=CPUs, required=true, unit=null, min=1, max=32, step=null}",
"{type=integer, propertyName=RAM, required=true, unit=GB, min=1, max=128, step=null}",
"{type=integer, propertyName=SSD, required=true, unit=GB, min=25, max=1000, step=25}",
"{type=integer, propertyName=HDD, required=false, unit=GB, min=0, max=4000, step=250}",
"{type=integer, propertyName=Traffic, required=true, unit=GB, min=250, max=10000, step=250}",
"{type=enumeration, propertyName=SLA-Platform, required=false, values=[BASIC, EXT8H, EXT4H, EXT2H]}",
"{type=boolean, propertyName=SLA-EMail, required=false, falseIf={SLA-Platform=BASIC}}",
"{type=boolean, propertyName=SLA-Maria, required=false, falseIf={SLA-Platform=BASIC}}",
"{type=boolean, propertyName=SLA-PgSQL, required=false, falseIf={SLA-Platform=BASIC}}",
"{type=boolean, propertyName=SLA-Office, required=false, falseIf={SLA-Platform=BASIC}}",
"{type=boolean, propertyName=SLA-Web, required=false, falseIf={SLA-Platform=BASIC}}");
}
}

View File

@ -0,0 +1,54 @@
package net.hostsharing.hsadminng.hs.booking.item.validators;
import net.hostsharing.hsadminng.hs.booking.item.HsBookingItemEntity;
import org.junit.jupiter.api.Test;
import java.util.Map;
import static java.util.Map.entry;
import static net.hostsharing.hsadminng.hs.booking.item.HsBookingItemType.MANAGED_WEBSPACE;
import static net.hostsharing.hsadminng.hs.booking.item.validators.HsBookingItemEntityValidators.forType;
import static org.assertj.core.api.Assertions.assertThat;
class HsManagedWebspaceBookingItemValidatorUnitTest {
@Test
void validatesProperties() {
// given
final var mangedServerBookingItemEntity = HsBookingItemEntity.builder()
.type(MANAGED_WEBSPACE)
.resources(Map.ofEntries(
entry("CPUs", 2),
entry("RAM", 25),
entry("SSD", 25),
entry("Traffic", 250),
entry("SLA-EMail", true)
))
.build();
final var validator = forType(mangedServerBookingItemEntity.getType());
// when
final var result = validator.validate(mangedServerBookingItemEntity);
// then
assertThat(result).containsExactlyInAnyOrder(
"'resources.CPUs' is not expected but is set to '2'",
"'resources.SLA-EMail' is not expected but is set to 'true'",
"'resources.RAM' is not expected but is set to '25'");
}
@Test
void containsAllValidations() {
// when
final var validator = forType(MANAGED_WEBSPACE);
// then
assertThat(validator.properties()).map(Map::toString).containsExactlyInAnyOrder(
"{type=integer, propertyName=SSD, required=true, unit=GB, min=1, max=100, step=1}",
"{type=integer, propertyName=HDD, required=false, unit=GB, min=0, max=250, step=10}",
"{type=integer, propertyName=Traffic, required=true, unit=GB, min=10, max=1000, step=10}",
"{type=enumeration, propertyName=SLA-Platform, required=false, values=[BASIC, EXT24H]}",
"{type=integer, propertyName=Daemons, required=false, unit=null, min=0, max=10, step=null}",
"{type=boolean, propertyName=Online Office Server, required=false, falseIf=null}");
}
}

View File

@ -19,6 +19,8 @@ import java.util.Map;
import java.util.UUID; import java.util.UUID;
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.MANAGED_SERVER;
import static net.hostsharing.hsadminng.rbac.test.JsonMatcher.lenientlyEquals; 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;
@ -113,7 +115,7 @@ class HsHostingAssetControllerAcceptanceTest extends ContextBasedTestWithCleanup
.header("current-user", "superuser-alex@hostsharing.net") .header("current-user", "superuser-alex@hostsharing.net")
.port(port) .port(port)
.when() .when()
.get("http://localhost/api/hs/hosting/assets?type=" + HsHostingAssetType.MANAGED_SERVER) .get("http://localhost/api/hs/hosting/assets?type=" + MANAGED_SERVER)
.then().log().all().assertThat() .then().log().all().assertThat()
.statusCode(200) .statusCode(200)
.contentType("application/json") .contentType("application/json")
@ -159,7 +161,7 @@ class HsHostingAssetControllerAcceptanceTest extends ContextBasedTestWithCleanup
class AddServer { class AddServer {
@Test @Test
void globalAdmin_canAddAsset() { void globalAdmin_canAddBookedAsset() {
context.define("superuser-alex@hostsharing.net"); context.define("superuser-alex@hostsharing.net");
final var givenBookingItem = givenBookingItem("First", "some PrivateCloud"); final var givenBookingItem = givenBookingItem("First", "some PrivateCloud");
@ -173,7 +175,7 @@ class HsHostingAssetControllerAcceptanceTest extends ContextBasedTestWithCleanup
"bookingItemUuid": "%s", "bookingItemUuid": "%s",
"type": "MANAGED_SERVER", "type": "MANAGED_SERVER",
"identifier": "vm1400", "identifier": "vm1400",
"caption": "some new CloudServer", "caption": "some new ManagedServer",
"config": { "CPUs": 2, "RAM": 100, "SSD": 300, "Traffic": 250 } "config": { "CPUs": 2, "RAM": 100, "SSD": 300, "Traffic": 250 }
} }
""".formatted(givenBookingItem.getUuid())) """.formatted(givenBookingItem.getUuid()))
@ -187,7 +189,7 @@ class HsHostingAssetControllerAcceptanceTest extends ContextBasedTestWithCleanup
{ {
"type": "MANAGED_SERVER", "type": "MANAGED_SERVER",
"identifier": "vm1400", "identifier": "vm1400",
"caption": "some new CloudServer", "caption": "some new ManagedServer",
"config": { "CPUs": 2, "RAM": 100, "SSD": 300, "Traffic": 250 } "config": { "CPUs": 2, "RAM": 100, "SSD": 300, "Traffic": 250 }
} }
""")) """))
@ -200,6 +202,48 @@ class HsHostingAssetControllerAcceptanceTest extends ContextBasedTestWithCleanup
assertThat(newUserUuid).isNotNull(); assertThat(newUserUuid).isNotNull();
} }
@Test
void parentAssetAgent_canAddSubAsset() {
context.define("superuser-alex@hostsharing.net");
final var givenParentAsset = givenParentAsset("First", MANAGED_SERVER);
final var location = RestAssured // @formatter:off
.given()
.header("current-user", "person-FirbySusan@example.com")
.contentType(ContentType.JSON)
.body("""
{
"parentAssetUuid": "%s",
"type": "MANAGED_WEBSPACE",
"identifier": "fir90",
"caption": "some new ManagedWebspace in client's ManagedServer",
"config": { "SSD": 100, "Traffic": 250 }
}
""".formatted(givenParentAsset.getUuid()))
.port(port)
.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": { "SSD": 100, "Traffic": 250 }
}
"""))
.header("Location", matchesRegex("http://localhost:[1-9][0-9]*/api/hs/hosting/assets/[^/]*"))
.extract().header("Location"); // @formatter:on
// finally, the new asset can be accessed under the generated UUID
final var newUserUuid = UUID.fromString(
location.substring(location.lastIndexOf('/') + 1));
assertThat(newUserUuid).isNotNull();
}
@Test @Test
void additionalValidationsArePerformend_whenAddingAsset() { void additionalValidationsArePerformend_whenAddingAsset() {
@ -215,7 +259,7 @@ class HsHostingAssetControllerAcceptanceTest extends ContextBasedTestWithCleanup
"bookingItemUuid": "%s", "bookingItemUuid": "%s",
"type": "MANAGED_SERVER", "type": "MANAGED_SERVER",
"identifier": "vm1400", "identifier": "vm1400",
"caption": "some new CloudServer", "caption": "some new ManagedServer",
"config": { "CPUs": 0, "extra": 42 } "config": { "CPUs": 0, "extra": 42 }
} }
""".formatted(givenBookingItem.getUuid())) """.formatted(givenBookingItem.getUuid()))
@ -228,14 +272,14 @@ class HsHostingAssetControllerAcceptanceTest extends ContextBasedTestWithCleanup
.body("", lenientlyEquals(""" .body("", lenientlyEquals("""
{ {
"statusPhrase": "Bad Request", "statusPhrase": "Bad Request",
"message": "['extra' is not expected but is '42', 'CPUs' is expected to be >= 1 but is 0, 'RAM' is required but missing, 'SSD' is required but missing, 'Traffic' is required but missing]" "message": "['config.extra' is not expected but is set to '42', 'config.CPUs' is expected to be >= 1 but is 0, 'config.RAM' is required but missing, 'config.SSD' is required but missing, 'config.Traffic' is required but missing]"
} }
""")); // @formatter:on """)); // @formatter:on
} }
} }
@Nested @Nested
class GetASset { class GetAsset {
@Test @Test
void globalAdmin_canGetArbitraryAsset() { void globalAdmin_canGetArbitraryAsset() {
@ -321,7 +365,8 @@ class HsHostingAssetControllerAcceptanceTest extends ContextBasedTestWithCleanup
@Test @Test
void globalAdmin_canPatchAllUpdatablePropertiesOfAsset() { void globalAdmin_canPatchAllUpdatablePropertiesOfAsset() {
final var givenAsset = givenSomeTemporaryAssetForDebitorNumber("2001", entry("something", 1)); final var givenAsset = givenSomeTemporaryHostingAsset("2001", CLOUD_SERVER,
config("CPUs", 4), config("RAM", 100), config("HDD", 100), config("Traffic", 2000));
RestAssured // @formatter:off RestAssured // @formatter:off
.given() .given()
@ -330,9 +375,9 @@ class HsHostingAssetControllerAcceptanceTest extends ContextBasedTestWithCleanup
.body(""" .body("""
{ {
"config": { "config": {
"CPU": "4", "CPUs": 2,
"HDD": null, "HDD": null,
"SSD": "4096" "SSD": 250
} }
} }
""") """)
@ -348,9 +393,9 @@ class HsHostingAssetControllerAcceptanceTest extends ContextBasedTestWithCleanup
"identifier": "vm2001", "identifier": "vm2001",
"caption": "some test-asset", "caption": "some test-asset",
"config": { "config": {
"CPU": "4", "CPUs": 2,
"SSD": "4096", "RAM": 100,
"something": 1 "SSD": 250
} }
} }
""")); // @formatter:on """)); // @formatter:on
@ -359,7 +404,7 @@ class HsHostingAssetControllerAcceptanceTest extends ContextBasedTestWithCleanup
context.define("superuser-alex@hostsharing.net"); context.define("superuser-alex@hostsharing.net");
assertThat(assetRepo.findByUuid(givenAsset.getUuid())).isPresent().get() assertThat(assetRepo.findByUuid(givenAsset.getUuid())).isPresent().get()
.matches(asset -> { .matches(asset -> {
assertThat(asset.toString()).isEqualTo("HsHostingAssetEntity(CLOUD_SERVER, vm2001, some test-asset, D-1000111:some CloudServer, { CPU: 4, SSD: 4096, something: 1 })"); assertThat(asset.toString()).isEqualTo("HsHostingAssetEntity(CLOUD_SERVER, vm2001, some test-asset, D-1000111:some CloudServer, { CPUs: 2, RAM: 100, SSD: 250, Traffic: 2000 })");
return true; return true;
}); });
} }
@ -371,7 +416,8 @@ class HsHostingAssetControllerAcceptanceTest extends ContextBasedTestWithCleanup
@Test @Test
void globalAdmin_canDeleteArbitraryAsset() { void globalAdmin_canDeleteArbitraryAsset() {
context.define("superuser-alex@hostsharing.net"); context.define("superuser-alex@hostsharing.net");
final var givenAsset = givenSomeTemporaryAssetForDebitorNumber("2002", entry("something", 1)); final var givenAsset = givenSomeTemporaryHostingAsset("2002", CLOUD_SERVER,
config("CPUs", 4), config("RAM", 100), config("HDD", 100), config("Traffic", 2000));
RestAssured // @formatter:off RestAssured // @formatter:off
.given() .given()
@ -389,7 +435,8 @@ class HsHostingAssetControllerAcceptanceTest extends ContextBasedTestWithCleanup
@Test @Test
void normalUser_canNotDeleteUnrelatedAsset() { void normalUser_canNotDeleteUnrelatedAsset() {
context.define("superuser-alex@hostsharing.net"); context.define("superuser-alex@hostsharing.net");
final var givenAsset = givenSomeTemporaryAssetForDebitorNumber("2003", entry("something", 1)); final var givenAsset = givenSomeTemporaryHostingAsset("2003", CLOUD_SERVER,
config("CPUs", 4), config("RAM", 100), config("HDD", 100), config("Traffic", 2000));
RestAssured // @formatter:off RestAssured // @formatter:off
.given() .given()
@ -412,14 +459,22 @@ class HsHostingAssetControllerAcceptanceTest extends ContextBasedTestWithCleanup
.findAny().orElseThrow(); .findAny().orElseThrow();
} }
private HsHostingAssetEntity givenSomeTemporaryAssetForDebitorNumber(final String identifierSuffix, HsHostingAssetEntity givenParentAsset(final String debitorName, final HsHostingAssetType assetType) {
final Map.Entry<String, Integer> resources) { final var givenDebitor = debitorRepo.findDebitorByOptionalNameLike(debitorName).stream().findAny().orElseThrow();
final var givenAsset = assetRepo.findAllByCriteria(givenDebitor.getUuid(), null, assetType).stream().findAny().orElseThrow();
return givenAsset;
}
@SafeVarargs
private HsHostingAssetEntity givenSomeTemporaryHostingAsset(final String identifierSuffix,
final HsHostingAssetType hostingAssetType,
final Map.Entry<String, Object>... resources) {
return jpaAttempt.transacted(() -> { return jpaAttempt.transacted(() -> {
context.define("superuser-alex@hostsharing.net"); context.define("superuser-alex@hostsharing.net");
final var newAsset = HsHostingAssetEntity.builder() final var newAsset = HsHostingAssetEntity.builder()
.uuid(UUID.randomUUID()) .uuid(UUID.randomUUID())
.bookingItem(givenBookingItem("First", "some CloudServer")) .bookingItem(givenBookingItem("First", "some CloudServer"))
.type(HsHostingAssetType.CLOUD_SERVER) .type(hostingAssetType)
.identifier("vm" + identifierSuffix) .identifier("vm" + identifierSuffix)
.caption("some test-asset") .caption("some test-asset")
.config(Map.ofEntries(resources)) .config(Map.ofEntries(resources))
@ -428,4 +483,8 @@ class HsHostingAssetControllerAcceptanceTest extends ContextBasedTestWithCleanup
return assetRepo.save(newAsset); return assetRepo.save(newAsset);
}).assertSuccessful().returnedValue(); }).assertSuccessful().returnedValue();
} }
private Map.Entry<String, Object> config(final String key, final Object value) {
return entry(key, value);
}
} }

View File

@ -40,7 +40,7 @@ class HsHostingAssetPropsControllerAcceptanceTest {
} }
@Test @Test
void globalAdmin_canListPropertiesOfGivenAssetType() { void anyone_canListPropertiesOfGivenAssetType() {
RestAssured // @formatter:off RestAssured // @formatter:off
.given() .given()
@ -96,57 +96,6 @@ class HsHostingAssetPropsControllerAcceptanceTest {
"min": 250, "min": 250,
"max": 10000, "max": 10000,
"step": 250 "step": 250
},
{
"type": "enumeration",
"propertyName": "SLA-Platform",
"required": false,
"values": [
"BASIC",
"EXT8H",
"EXT4H",
"EXT2H"
]
},
{
"type": "boolean",
"propertyName": "SLA-EMail",
"required": false,
"falseIf": {
"SLA-Platform": "BASIC"
}
},
{
"type": "boolean",
"propertyName": "SLA-Maria",
"required": false,
"falseIf": {
"SLA-Platform": "BASIC"
}
},
{
"type": "boolean",
"propertyName": "SLA-PgSQL",
"required": false,
"falseIf": {
"SLA-Platform": "BASIC"
}
},
{
"type": "boolean",
"propertyName": "SLA-Office",
"required": false,
"falseIf": {
"SLA-Platform": "BASIC"
}
},
{
"type": "boolean",
"propertyName": "SLA-Web",
"required": false,
"falseIf": {
"SLA-Platform": "BASIC"
}
} }
] ]
""")); """));

View File

@ -1,97 +0,0 @@
package net.hostsharing.hsadminng.hs.hosting.asset;
import net.hostsharing.hsadminng.hs.hosting.asset.validator.HsHostingAssetValidator;
import org.junit.jupiter.api.Test;
import java.util.Map;
import static java.util.Collections.emptyMap;
import static java.util.Map.entry;
import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.MANAGED_SERVER;
import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.MANAGED_WEBSPACE;
import static org.assertj.core.api.Assertions.assertThat;
class HsHostingAssetValidatorUnitTest {
@Test
void validatesMissingProperties() {
// given
final var validator = HsHostingAssetValidator.forType(MANAGED_WEBSPACE);
final var mangedWebspaceHostingAssetEntity = HsHostingAssetEntity.builder()
.type(MANAGED_WEBSPACE)
.config(emptyMap())
.build();
// when
final var result = validator.validate(mangedWebspaceHostingAssetEntity);
// then
assertThat(result).containsExactlyInAnyOrder(
"'SSD' is required but missing",
"'Traffic' is required but missing"
);
}
@Test
void validatesUnknownProperties() {
// given
final var validator = HsHostingAssetValidator.forType(MANAGED_WEBSPACE);
final var mangedWebspaceHostingAssetEntity = HsHostingAssetEntity.builder()
.type(MANAGED_WEBSPACE)
.config(Map.ofEntries(
entry("HDD", 0),
entry("SSD", 1),
entry("Traffic", 10),
entry("unknown", "some value")
))
.build();
// when
final var result = validator.validate(mangedWebspaceHostingAssetEntity);
// then
assertThat(result).containsExactly("'unknown' is not expected but is 'some value'");
}
@Test
void validatesDependentProperties() {
// given
final var validator = HsHostingAssetValidator.forType(MANAGED_SERVER);
final var mangedWebspaceHostingAssetEntity = HsHostingAssetEntity.builder()
.type(MANAGED_SERVER)
.config(Map.ofEntries(
entry("CPUs", 2),
entry("RAM", 25),
entry("SSD", 25),
entry("Traffic", 250),
entry("SLA-EMail", true)
))
.build();
// when
final var result = validator.validate(mangedWebspaceHostingAssetEntity);
// then
assertThat(result).containsExactly("'SLA-EMail' is expected to be false because SLA-Platform=BASIC but is true");
}
@Test
void validatesValidProperties() {
// given
final var validator = HsHostingAssetValidator.forType(MANAGED_WEBSPACE);
final var mangedWebspaceHostingAssetEntity = HsHostingAssetEntity.builder()
.type(MANAGED_WEBSPACE)
.config(Map.ofEntries(
entry("HDD", 200),
entry("SSD", 25),
entry("Traffic", 250)
))
.build();
// when
final var result = validator.validate(mangedWebspaceHostingAssetEntity);
// then
assertThat(result).isEmpty();
}
}

View File

@ -0,0 +1,55 @@
package net.hostsharing.hsadminng.hs.hosting.asset.validators;
import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetEntity;
import org.junit.jupiter.api.Test;
import java.util.Map;
import static java.util.Map.entry;
import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.CLOUD_SERVER;
import static net.hostsharing.hsadminng.hs.hosting.asset.validators.HsHostingAssetEntityValidators.forType;
import static org.assertj.core.api.Assertions.assertThat;
class HsCloudServerHostingAssetValidatorUnitTest {
@Test
void validatesProperties() {
// given
final var cloudServerHostingAssetEntity = HsHostingAssetEntity.builder()
.type(CLOUD_SERVER)
.config(Map.ofEntries(
entry("RAM", 2000),
entry("SSD", 256),
entry("Traffic", "250"),
entry("SLA-Platform", "xxx")
))
.build();
final var validator = forType(cloudServerHostingAssetEntity.getType());
// when
final var result = validator.validate(cloudServerHostingAssetEntity);
// then
assertThat(result).containsExactlyInAnyOrder(
"'config.SLA-Platform' is not expected but is set to 'xxx'",
"'config.CPUs' is required but missing",
"'config.RAM' is expected to be <= 128 but is 2000",
"'config.SSD' is expected to be multiple of 25 but is 256",
"'config.Traffic' is expected to be of type class java.lang.Integer, but is of type 'String'");
}
@Test
void containsAllValidations() {
// when
final var validator = forType(CLOUD_SERVER);
// then
assertThat(validator.properties()).map(Map::toString).containsExactlyInAnyOrder(
"{type=integer, propertyName=CPUs, required=true, unit=null, min=1, max=32, step=null}",
"{type=integer, propertyName=RAM, required=true, unit=GB, min=1, max=128, step=null}",
"{type=integer, propertyName=SSD, required=true, unit=GB, min=25, max=1000, step=25}",
"{type=integer, propertyName=HDD, required=false, unit=GB, min=0, max=4000, step=250}",
"{type=integer, propertyName=Traffic, required=true, unit=GB, min=250, max=10000, step=250}");
}
}

View File

@ -0,0 +1,33 @@
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.CLOUD_SERVER;
import static net.hostsharing.hsadminng.hs.hosting.asset.validators.HsHostingAssetEntityValidators.valid;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.catchThrowable;
class HsHostingAssetEntityValidatorsUnitTest {
@Test
void validThrowsException() {
// given
final var cloudServerHostingAssetEntity = HsHostingAssetEntity.builder()
.type(CLOUD_SERVER)
.build();
// when
final var result = catchThrowable( ()-> valid(cloudServerHostingAssetEntity) );
// then
assertThat(result).isInstanceOf(ValidationException.class)
.hasMessageContaining(
"'config.CPUs' is required but missing",
"'config.RAM' is required but missing",
"'config.SSD' is required but missing",
"'config.Traffic' is required but missing");
}
}

View File

@ -0,0 +1,40 @@
package net.hostsharing.hsadminng.hs.hosting.asset.validators;
import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetEntity;
import org.junit.jupiter.api.Test;
import java.util.Map;
import static java.util.Map.entry;
import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.MANAGED_SERVER;
import static net.hostsharing.hsadminng.hs.hosting.asset.validators.HsHostingAssetEntityValidators.forType;
import static org.assertj.core.api.Assertions.assertThat;
class HsManagedServerHostingAssetValidatorUnitTest {
@Test
void validatesProperties() {
// given
final var mangedWebspaceHostingAssetEntity = HsHostingAssetEntity.builder()
.type(MANAGED_SERVER)
.config(Map.ofEntries(
entry("RAM", 2000),
entry("SSD", 256),
entry("Traffic", "250"),
entry("SLA-Platform", "xxx")
))
.build();
final var validator = forType(mangedWebspaceHostingAssetEntity.getType());
// when
final var result = validator.validate(mangedWebspaceHostingAssetEntity);
// then
assertThat(result).containsExactlyInAnyOrder(
"'config.SLA-Platform' is not expected but is set to 'xxx'",
"'config.CPUs' is required but missing",
"'config.RAM' is expected to be <= 128 but is 2000",
"'config.SSD' is expected to be multiple of 25 but is 256",
"'config.Traffic' is expected to be of type class java.lang.Integer, but is of type 'String'");
}
}

View File

@ -0,0 +1,120 @@
package net.hostsharing.hsadminng.hs.hosting.asset.validators;
import net.hostsharing.hsadminng.hs.booking.item.HsBookingItemEntity;
import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetEntity;
import net.hostsharing.hsadminng.hs.office.debitor.HsOfficeDebitorEntity;
import org.junit.jupiter.api.Test;
import java.util.Map;
import static java.util.Collections.emptyMap;
import static java.util.Map.entry;
import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.MANAGED_SERVER;
import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.MANAGED_WEBSPACE;
import static org.assertj.core.api.Assertions.assertThat;
class HsManagedWebspaceHostingAssetValidatorUnitTest {
final HsBookingItemEntity managedServerBookingItem = HsBookingItemEntity.builder()
.debitor(HsOfficeDebitorEntity.builder().defaultPrefix("abc").build()
)
.build();
final HsHostingAssetEntity mangedServerAssetEntity = HsHostingAssetEntity.builder()
.type(MANAGED_SERVER)
.bookingItem(managedServerBookingItem)
.config(Map.ofEntries(
entry("HDD", 0),
entry("SSD", 1),
entry("Traffic", 10)
))
.build();
@Test
void validatesIdentifier() {
// given
final var validator = HsHostingAssetEntityValidators.forType(MANAGED_WEBSPACE);
final var mangedWebspaceHostingAssetEntity = HsHostingAssetEntity.builder()
.type(MANAGED_WEBSPACE)
.parentAsset(mangedServerAssetEntity)
.identifier("xyz00")
.config(Map.ofEntries(
entry("HDD", 0),
entry("SSD", 1),
entry("Traffic", 10)
))
.build();
// when
final var result = validator.validate(mangedWebspaceHostingAssetEntity);
// then
assertThat(result).containsExactly("'identifier' expected to match '^abc[0-9][0-9]$', but is 'xyz00'");
}
@Test
void validatesMissingProperties() {
// given
final var validator = HsHostingAssetEntityValidators.forType(MANAGED_WEBSPACE);
final var mangedWebspaceHostingAssetEntity = HsHostingAssetEntity.builder()
.type(MANAGED_WEBSPACE)
.parentAsset(mangedServerAssetEntity)
.identifier("abc00")
.config(emptyMap())
.build();
// when
final var result = validator.validate(mangedWebspaceHostingAssetEntity);
// then
assertThat(result).containsExactlyInAnyOrder(
"'config.SSD' is required but missing",
"'config.Traffic' is required but missing"
);
}
@Test
void validatesUnknownProperties() {
// given
final var validator = HsHostingAssetEntityValidators.forType(MANAGED_WEBSPACE);
final var mangedWebspaceHostingAssetEntity = HsHostingAssetEntity.builder()
.type(MANAGED_WEBSPACE)
.parentAsset(mangedServerAssetEntity)
.identifier("abc00")
.config(Map.ofEntries(
entry("HDD", 0),
entry("SSD", 1),
entry("Traffic", 10),
entry("unknown", "some value")
))
.build();
// when
final var result = validator.validate(mangedWebspaceHostingAssetEntity);
// then
assertThat(result).containsExactly("'config.unknown' is not expected but is set to 'some value'");
}
@Test
void validatesValidProperties() {
// given
final var validator = HsHostingAssetEntityValidators.forType(MANAGED_WEBSPACE);
final var mangedWebspaceHostingAssetEntity = HsHostingAssetEntity.builder()
.type(MANAGED_WEBSPACE)
.parentAsset(mangedServerAssetEntity)
.identifier("abc00")
.config(Map.ofEntries(
entry("HDD", 200),
entry("SSD", 25),
entry("Traffic", 250)
))
.build();
// when
final var result = validator.validate(mangedWebspaceHostingAssetEntity);
// then
assertThat(result).isEmpty();
}
}