From 2e9e5d6ef03fbe3ad38934393db096a5f0bf2416 Mon Sep 17 00:00:00 2001 From: Michael Hoennig Date: Mon, 6 May 2024 10:50:59 +0200 Subject: [PATCH] hosting-asset-validation-for-cloud-server-to-webspace (#54) Co-authored-by: Michael Hoennig Reviewed-on: https://dev.hostsharing.net/hostsharing/hs.hsadmin.ng/pulls/54 Reviewed-by: Marc Sandlus --- .../asset/HsHostingAssetController.java | 12 +- .../asset/HsHostingAssetPropsController.java | 39 ++++ .../HsHostingAssetPropertyValidator.java | 172 ++++++++++++++++++ .../validator/HsHostingAssetValidator.java | 99 ++++++++++ .../hs/hosting/asset/validator/lombok.config | 3 + .../hs-hosting/hs-hosting-asset-schemas.yaml | 93 ++++++++-- .../hs-hosting-asset-types-props.yaml | 26 +++ .../hs-hosting/hs-hosting-asset-types.yaml | 19 ++ .../hs-hosting/hs-hosting-assets.yaml | 4 +- .../api-definition/hs-hosting/hs-hosting.yaml | 10 +- .../hsadminng/arch/ArchitectureTest.java | 1 + ...sHostingAssetControllerAcceptanceTest.java | 37 +++- ...ingAssetPropsControllerAcceptanceTest.java | 156 ++++++++++++++++ .../HsHostingAssetValidatorUnitTest.java | 97 ++++++++++ 14 files changed, 750 insertions(+), 18 deletions(-) create mode 100644 src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetPropsController.java create mode 100644 src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validator/HsHostingAssetPropertyValidator.java create mode 100644 src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validator/HsHostingAssetValidator.java create mode 100644 src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validator/lombok.config create mode 100644 src/main/resources/api-definition/hs-hosting/hs-hosting-asset-types-props.yaml create mode 100644 src/main/resources/api-definition/hs-hosting/hs-hosting-asset-types.yaml create mode 100644 src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetPropsControllerAcceptanceTest.java create mode 100644 src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetValidatorUnitTest.java diff --git a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetController.java b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetController.java index 62a62b34..384fc2e3 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetController.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetController.java @@ -1,5 +1,6 @@ package net.hostsharing.hsadminng.hs.hosting.asset; +import net.hostsharing.hsadminng.hs.hosting.asset.validator.HsHostingAssetValidator; import net.hostsharing.hsadminng.hs.hosting.generated.api.v1.api.HsHostingAssetsApi; import net.hostsharing.hsadminng.context.Context; @@ -15,6 +16,7 @@ import org.springframework.transaction.annotation.Transactional; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.servlet.mvc.method.annotation.MvcUriComponentsBuilder; +import jakarta.validation.ValidationException; import java.util.List; import java.util.UUID; import java.util.function.BiConsumer; @@ -60,7 +62,7 @@ public class HsHostingAssetController implements HsHostingAssetsApi { final var entityToSave = mapper.map(body, HsHostingAssetEntity.class, RESOURCE_TO_ENTITY_POSTMAPPER); - final var saved = assetRepo.save(entityToSave); + final var saved = assetRepo.save(valid(entityToSave)); final var uri = MvcUriComponentsBuilder.fromController(getClass()) @@ -120,6 +122,14 @@ public class HsHostingAssetController implements HsHostingAssetsApi { 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 RESOURCE_TO_ENTITY_POSTMAPPER = (resource, entity) -> { entity.putConfig(KeyValueMap.from(resource.getConfig())); diff --git a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetPropsController.java b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetPropsController.java new file mode 100644 index 00000000..8a3f1523 --- /dev/null +++ b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetPropsController.java @@ -0,0 +1,39 @@ +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.HsHostingAssetPropsApi; +import net.hostsharing.hsadminng.hs.hosting.generated.api.v1.model.HsHostingAssetTypeResource; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.RestController; + +import java.util.List; +import java.util.Map; + + +@RestController +public class HsHostingAssetPropsController implements HsHostingAssetPropsApi { + + @Override + public ResponseEntity> listAssetTypes() { + final var resource = HsHostingAssetValidator.types().stream() + .map(Enum::name) + .toList(); + return ResponseEntity.ok(resource); + } + + @Override + public ResponseEntity> listAssetTypeProps( + final HsHostingAssetTypeResource assetType) { + + final var propValidators = HsHostingAssetValidator.forType(HsHostingAssetType.of(assetType)); + final List> resource = propValidators.properties(); + return ResponseEntity.ok(toListOfObjects(resource)); + } + + private List toListOfObjects(final List> resource) { + // OpenApi ony generates List not List> for the Java interface. + // But Spring properly converts the List of Maps, thus we can simply cast the type: + //noinspection rawtypes,unchecked + return (List) resource; + } +} diff --git a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validator/HsHostingAssetPropertyValidator.java b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validator/HsHostingAssetPropertyValidator.java new file mode 100644 index 00000000..7e61845f --- /dev/null +++ b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validator/HsHostingAssetPropertyValidator.java @@ -0,0 +1,172 @@ +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 { + + final Class type; + final String propertyName; + private Boolean required; + + public static Map.Entry defType(K k, V v) { + return new SimpleImmutableEntry<>(k, v); + } + + public HsHostingAssetPropertyValidator required() { + required = Boolean.TRUE; + return this; + } + + public HsHostingAssetPropertyValidator optional() { + required = Boolean.FALSE; + return this; + } + + public final List validate(final Map props) { + final var result = new ArrayList(); + final var propValue = props.get(propertyName); + if (propValue == null) { + if (required) { + result.add("'" + propertyName + "' is required but missing"); + } + } + if (propValue != null){ + if ( type.isInstance(propValue)) { + //noinspection unchecked + validate(result, (T) propValue, props); + } else { + result.add("'" + propertyName + "' is expected to be of type " + type + ", " + + "but is of type '" + propValue.getClass().getSimpleName() + "'"); + } + } + return result; + } + + protected abstract void validate(final ArrayList result, final T propValue, final Map props); + + public void verifyConsistency(final Map.Entry typeDef) { + if (required == null ) { + throw new IllegalStateException(typeDef.getKey() + "[" + propertyName + "] not fully initialized, please call either .required() or .optional()" ); + } + } + + public Map toMap(final ObjectMapper mapper) { + final Map map = mapper.convertValue(this, Map.class); + map.put("type", simpleTypeName()); + return map; + } + + protected abstract String simpleTypeName(); +} + +@Setter +class IntegerPropertyValidator extends HsHostingAssetPropertyValidator{ + + private String unit; + private Integer min; + private Integer max; + private Integer step; + + public static IntegerPropertyValidator integerProperty(final String propertyName) { + return new IntegerPropertyValidator(propertyName); + } + + private IntegerPropertyValidator(final String propertyName) { + super(Integer.class, propertyName); + } + + + @Override + protected void validate(final ArrayList result, final Integer propValue, final Map props) { + if (min != null && propValue < min) { + result.add("'" + propertyName + "' is expected to be >= " + min + " but is " + propValue); + } + if (max != null && propValue > max) { + result.add("'" + propertyName + "' is expected to be <= " + max + " but is " + propValue); + } + if (step != null && propValue % step != 0) { + result.add("'" + propertyName + "' is expected to be multiple of " + step + " but is " + propValue); + } + } + + @Override + protected String simpleTypeName() { + return "integer"; + } +} + +@Setter +class EnumPropertyValidator extends HsHostingAssetPropertyValidator { + + 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 values(final String... values) { + this.values = values; + return this; + } + + @Override + protected void validate(final ArrayList result, final String propValue, final Map props) { + if (Arrays.stream(values).noneMatch(v -> v.equals(propValue))) { + result.add("'" + propertyName + "' is expected to be one of " + Arrays.toString(values) + " but is '" + propValue + "'"); + } + } + + @Override + protected String simpleTypeName() { + return "enumeration"; + } +} + +@Setter +class BooleanPropertyValidator extends HsHostingAssetPropertyValidator { + + private Map.Entry falseIf; + + private BooleanPropertyValidator(final String propertyName) { + super(Boolean.class, propertyName); + } + + public static BooleanPropertyValidator booleanProperty(final String propertyName) { + return new BooleanPropertyValidator(propertyName); + } + + HsHostingAssetPropertyValidator falseIf(final String refPropertyName, final String refPropertyValue) { + this.falseIf = new SimpleImmutableEntry<>(refPropertyName, refPropertyValue); + return this; + } + + @Override + protected void validate(final ArrayList result, final Boolean propValue, final Map 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"; + } +} diff --git a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validator/HsHostingAssetValidator.java b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validator/HsHostingAssetValidator.java new file mode 100644 index 00000000..1389de21 --- /dev/null +++ b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validator/HsHostingAssetValidator.java @@ -0,0 +1,99 @@ +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 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 types() { + return validators.keySet(); + } + + public List validate(final HsHostingAssetEntity assetEntity) { + final var result = new ArrayList(); + 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> 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 asKeyValueMap(final Map map) { + return (Map) map; + } + +} diff --git a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validator/lombok.config b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validator/lombok.config new file mode 100644 index 00000000..18183936 --- /dev/null +++ b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validator/lombok.config @@ -0,0 +1,3 @@ +lombok.addLombokGeneratedAnnotation = true +lombok.accessors.chain = true +lombok.accessors.fluent = true diff --git a/src/main/resources/api-definition/hs-hosting/hs-hosting-asset-schemas.yaml b/src/main/resources/api-definition/hs-hosting/hs-hosting-asset-schemas.yaml index f3ecb6a3..59696a23 100644 --- a/src/main/resources/api-definition/hs-hosting/hs-hosting-asset-schemas.yaml +++ b/src/main/resources/api-definition/hs-hosting/hs-hosting-asset-schemas.yaml @@ -80,18 +80,85 @@ components: # forces generating a java.lang.Object containing a Map, instead of class AssetConfiguration anyOf: - type: object - properties: - CPU: - type: integer - minimum: 1 - maximum: 16 - SSD: - type: integer - minimum: 16 - maximum: 4096 - HDD: - type: integer - minimum: 16 - maximum: 4096 + # single source of supported properties just via /api/hs/hosting/asset-types/{assetType} + # TODO.impl: later, we could generate the config types and their properties from the validation config additionalProperties: true + PropertyDescriptor: + type: object + properties: + "type": + type: string + enum: + - integer + - boolean + - enumeration + "propertyName": + type: string + pattern: "^[ a-zA-Z0-9_-]$" + "required": + type: boolean + required: + - type + - propertyName + - required + + IntegerPropertyDescriptor: + allOf: + - $ref: '#/components/schemas/PropertyDescriptor' + - type: object + properties: + "type": + type: string + enum: + - integer + "unit": + type: string + "min": + type: integer + minimum: 0 + "max": + type: integer + minimum: 0 + "step": + type: integer + minimum: 1 + required: + - "type" + - "propertyName" + - "required" + + BooleanPropertyDescriptor: + allOf: + - $ref: '#/components/schemas/PropertyDescriptor' + - type: object + properties: + "type": + type: string + enum: + - boolean + "falseIf": + type: object + anyOf: + - type: object + additionalProperties: true + + EnumerationPropertyDescriptor: + allOf: + - $ref: '#/components/schemas/PropertyDescriptor' + - type: object + properties: + "type": + type: string + enum: + - enumeration + "values": + type: array + items: + type: string + + HsHostingAssetProps: + anyOf: + - $ref: '#/components/schemas/IntegerPropertyDescriptor' + - $ref: '#/components/schemas/BooleanPropertyDescriptor' + - $ref: '#/components/schemas/EnumerationPropertyDescriptor' diff --git a/src/main/resources/api-definition/hs-hosting/hs-hosting-asset-types-props.yaml b/src/main/resources/api-definition/hs-hosting/hs-hosting-asset-types-props.yaml new file mode 100644 index 00000000..c7723c22 --- /dev/null +++ b/src/main/resources/api-definition/hs-hosting/hs-hosting-asset-types-props.yaml @@ -0,0 +1,26 @@ +get: + summary: Returns a list of available asset properties for the given type. + description: Returns the list of available properties and their validations for a given asset type. + tags: + - hs-hosting-asset-props + operationId: listAssetTypeProps + parameters: + - name: assetType + in: path + required: true + schema: + $ref: 'hs-hosting-asset-schemas.yaml#/components/schemas/HsHostingAssetType' + description: The asset type whose properties are to be returned. + responses: + "200": + description: OK + content: + 'application/json': + schema: + type: array + items: + $ref: 'hs-hosting-asset-schemas.yaml#/components/schemas/HsHostingAssetProps' + "401": + $ref: 'error-responses.yaml#/components/responses/Unauthorized' + "403": + $ref: 'error-responses.yaml#/components/responses/Forbidden' diff --git a/src/main/resources/api-definition/hs-hosting/hs-hosting-asset-types.yaml b/src/main/resources/api-definition/hs-hosting/hs-hosting-asset-types.yaml new file mode 100644 index 00000000..f1ab17e0 --- /dev/null +++ b/src/main/resources/api-definition/hs-hosting/hs-hosting-asset-types.yaml @@ -0,0 +1,19 @@ +get: + summary: Returns a list of available asset types. + description: Returns the list of asset types to enable an adaptive UI. + tags: + - hs-hosting-asset-props + operationId: listAssetTypes + responses: + "200": + description: OK + content: + 'application/json': + schema: + type: array + items: + type: string + "401": + $ref: 'error-responses.yaml#/components/responses/Unauthorized' + "403": + $ref: 'error-responses.yaml#/components/responses/Forbidden' diff --git a/src/main/resources/api-definition/hs-hosting/hs-hosting-assets.yaml b/src/main/resources/api-definition/hs-hosting/hs-hosting-assets.yaml index 8b81ecc7..a08a36a1 100644 --- a/src/main/resources/api-definition/hs-hosting/hs-hosting-assets.yaml +++ b/src/main/resources/api-definition/hs-hosting/hs-hosting-assets.yaml @@ -13,18 +13,20 @@ get: schema: type: string format: uuid + description: The UUID of the debitor, whose hosting assets are to be listed. - name: parentAssetUuid in: query required: false schema: type: string format: uuid + description: The UUID of the parentAsset, whose hosting assets are to be listed. - name: type in: query required: false schema: $ref: 'hs-hosting-asset-schemas.yaml#/components/schemas/HsHostingAssetType' - description: The UUID of the debitor, whose hosting assets are to be listed. + description: The type of hosting assets to be listed. responses: "200": description: OK diff --git a/src/main/resources/api-definition/hs-hosting/hs-hosting.yaml b/src/main/resources/api-definition/hs-hosting/hs-hosting.yaml index 4f8f29d5..b0df69dc 100644 --- a/src/main/resources/api-definition/hs-hosting/hs-hosting.yaml +++ b/src/main/resources/api-definition/hs-hosting/hs-hosting.yaml @@ -8,10 +8,18 @@ servers: paths: - # Items + # Assets /api/hs/hosting/assets: $ref: "hs-hosting-assets.yaml" /api/hs/hosting/assets/{assetUuid}: $ref: "hs-hosting-assets-with-uuid.yaml" + + # Asset-Types + + /api/hs/hosting/asset-types: + $ref: "hs-hosting-asset-types.yaml" + + /api/hs/hosting/asset-types/{assetType}: + $ref: "hs-hosting-asset-types-props.yaml" diff --git a/src/test/java/net/hostsharing/hsadminng/arch/ArchitectureTest.java b/src/test/java/net/hostsharing/hsadminng/arch/ArchitectureTest.java index 15f9c152..0cb1a086 100644 --- a/src/test/java/net/hostsharing/hsadminng/arch/ArchitectureTest.java +++ b/src/test/java/net/hostsharing/hsadminng/arch/ArchitectureTest.java @@ -52,6 +52,7 @@ public class ArchitectureTest { "..hs.office.sepamandate", "..hs.booking.item", "..hs.hosting.asset", + "..hs.hosting.asset.validator", "..errors", "..mapper", "..ping", diff --git a/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetControllerAcceptanceTest.java b/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetControllerAcceptanceTest.java index 26d1b763..0cde4075 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetControllerAcceptanceTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetControllerAcceptanceTest.java @@ -174,7 +174,7 @@ class HsHostingAssetControllerAcceptanceTest extends ContextBasedTestWithCleanup "type": "MANAGED_SERVER", "identifier": "vm1400", "caption": "some new CloudServer", - "config": { "CPU": 3, "extra": 42 } + "config": { "CPUs": 2, "RAM": 100, "SSD": 300, "Traffic": 250 } } """.formatted(givenBookingItem.getUuid())) .port(port) @@ -188,7 +188,7 @@ class HsHostingAssetControllerAcceptanceTest extends ContextBasedTestWithCleanup "type": "MANAGED_SERVER", "identifier": "vm1400", "caption": "some new CloudServer", - "config": { "CPU": 3, "extra": 42 } + "config": { "CPUs": 2, "RAM": 100, "SSD": 300, "Traffic": 250 } } """)) .header("Location", matchesRegex("http://localhost:[1-9][0-9]*/api/hs/hosting/assets/[^/]*")) @@ -199,6 +199,39 @@ class HsHostingAssetControllerAcceptanceTest extends ContextBasedTestWithCleanup location.substring(location.lastIndexOf('/') + 1)); assertThat(newUserUuid).isNotNull(); } + + @Test + void additionalValidationsArePerformend_whenAddingAsset() { + + context.define("superuser-alex@hostsharing.net"); + final var givenBookingItem = givenBookingItem("First", "some PrivateCloud"); + + final var location = RestAssured // @formatter:off + .given() + .header("current-user", "superuser-alex@hostsharing.net") + .contentType(ContentType.JSON) + .body(""" + { + "bookingItemUuid": "%s", + "type": "MANAGED_SERVER", + "identifier": "vm1400", + "caption": "some new CloudServer", + "config": { "CPUs": 0, "extra": 42 } + } + """.formatted(givenBookingItem.getUuid())) + .port(port) + .when() + .post("http://localhost/api/hs/hosting/assets") + .then().log().all().assertThat() + .statusCode(400) + .contentType(ContentType.JSON) + .body("", lenientlyEquals(""" + { + "statusPhrase": "Bad Request", + "message": "['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]" + } + """)); // @formatter:on + } } @Nested diff --git a/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetPropsControllerAcceptanceTest.java b/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetPropsControllerAcceptanceTest.java new file mode 100644 index 00000000..58c7bf91 --- /dev/null +++ b/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetPropsControllerAcceptanceTest.java @@ -0,0 +1,156 @@ +package net.hostsharing.hsadminng.hs.hosting.asset; + +import io.restassured.RestAssured; +import net.hostsharing.hsadminng.HsadminNgApplication; +import net.hostsharing.hsadminng.rbac.test.JpaAttempt; +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.web.server.LocalServerPort; + +import static net.hostsharing.hsadminng.rbac.test.JsonMatcher.lenientlyEquals; + +@SpringBootTest( + webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, + classes = { HsadminNgApplication.class, JpaAttempt.class } +) +class HsHostingAssetPropsControllerAcceptanceTest { + + @LocalServerPort + private Integer port; + + @Test + void anyone_canListAvailableAssetTypes() { + + RestAssured // @formatter:off + .given() + .port(port) + .when() + .get("http://localhost/api/hs/hosting/asset-types") + .then().log().all().assertThat() + .statusCode(200) + .contentType("application/json") + .body("", lenientlyEquals(""" + [ + "MANAGED_SERVER", + "MANAGED_WEBSPACE", + "CLOUD_SERVER" + ] + """)); + // @formatter:on + } + + @Test + void globalAdmin_canListPropertiesOfGivenAssetType() { + + RestAssured // @formatter:off + .given() + .port(port) + .when() + .get("http://localhost/api/hs/hosting/asset-types/" + HsHostingAssetType.MANAGED_SERVER) + .then().log().all().assertThat() + .statusCode(200) + .contentType("application/json") + .body("", lenientlyEquals(""" + [ + { + "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" + } + } + ] + """)); + // @formatter:on + } + +} diff --git a/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetValidatorUnitTest.java b/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetValidatorUnitTest.java new file mode 100644 index 00000000..d7f21222 --- /dev/null +++ b/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetValidatorUnitTest.java @@ -0,0 +1,97 @@ +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(); + } +}