add-unix-user-hosting-asset-validation #66
@ -11,6 +11,7 @@ import lombok.NoArgsConstructor;
|
|||||||
import lombok.Setter;
|
import lombok.Setter;
|
||||||
import net.hostsharing.hsadminng.hs.booking.project.HsBookingProjectEntity;
|
import net.hostsharing.hsadminng.hs.booking.project.HsBookingProjectEntity;
|
||||||
import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetEntity;
|
import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetEntity;
|
||||||
|
import net.hostsharing.hsadminng.hs.validation.PropertiesProvider;
|
||||||
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;
|
||||||
@ -42,6 +43,7 @@ import java.util.List;
|
|||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
|
||||||
|
import static java.util.Collections.emptyMap;
|
||||||
import static java.util.Optional.ofNullable;
|
import static java.util.Optional.ofNullable;
|
||||||
import static net.hostsharing.hsadminng.mapper.PostgresDateRange.lowerInclusiveFromPostgresDateRange;
|
import static net.hostsharing.hsadminng.mapper.PostgresDateRange.lowerInclusiveFromPostgresDateRange;
|
||||||
import static net.hostsharing.hsadminng.mapper.PostgresDateRange.toPostgresDateRange;
|
import static net.hostsharing.hsadminng.mapper.PostgresDateRange.toPostgresDateRange;
|
||||||
@ -68,7 +70,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, PropertiesProvider {
|
||||||
|
|
||||||
private static Stringify<HsBookingItemEntity> stringify = stringify(HsBookingItemEntity.class)
|
private static Stringify<HsBookingItemEntity> stringify = stringify(HsBookingItemEntity.class)
|
||||||
.withProp(HsBookingItemEntity::getProject)
|
.withProp(HsBookingItemEntity::getProject)
|
||||||
@ -146,6 +148,23 @@ public class HsBookingItemEntity implements Stringifyable, RbacObject {
|
|||||||
return upperInclusiveFromPostgresDateRange(getValidity());
|
return upperInclusiveFromPostgresDateRange(getValidity());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Map<String, Object> directProps() {
|
||||||
|
return resources;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Object getContextValue(final String propName) {
|
||||||
|
final var v = resources.get(propName);
|
||||||
|
if (v!= null) {
|
||||||
|
return v;
|
||||||
|
}
|
||||||
|
if (parentItem!=null) {
|
||||||
|
return parentItem.getResources().get(propName);
|
||||||
|
}
|
||||||
|
return emptyMap();
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String toString() {
|
public String toString() {
|
||||||
return stringify.apply(this);
|
return stringify.apply(this);
|
||||||
|
@ -29,7 +29,7 @@ public class HsBookingItemEntityValidator extends HsEntityValidator<HsBookingIte
|
|||||||
}
|
}
|
||||||
|
|
||||||
private List<String> validateProperties(final HsBookingItemEntity bookingItem) {
|
private List<String> validateProperties(final HsBookingItemEntity bookingItem) {
|
||||||
return enrich(prefix(bookingItem.toShortString(), "resources"), validateProperties(bookingItem.getResources()));
|
return enrich(prefix(bookingItem.toShortString(), "resources"), super.validateProperties(bookingItem));
|
||||||
}
|
}
|
||||||
|
|
||||||
private static List<String> optionallyValidate(final HsBookingItemEntity bookingItem) {
|
private static List<String> optionallyValidate(final HsBookingItemEntity bookingItem) {
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
package net.hostsharing.hsadminng.hs.hosting.asset;
|
package net.hostsharing.hsadminng.hs.hosting.asset;
|
||||||
|
|
||||||
import net.hostsharing.hsadminng.hs.booking.item.HsBookingItemRepository;
|
import net.hostsharing.hsadminng.hs.booking.item.HsBookingItemRepository;
|
||||||
|
import net.hostsharing.hsadminng.hs.hosting.asset.validators.HsHostingAssetEntityValidatorRegistry;
|
||||||
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;
|
||||||
@ -78,7 +79,7 @@ public class HsHostingAssetController implements HsHostingAssetsApi {
|
|||||||
.path("/api/hs/hosting/assets/{id}")
|
.path("/api/hs/hosting/assets/{id}")
|
||||||
.buildAndExpand(saved.getUuid())
|
.buildAndExpand(saved.getUuid())
|
||||||
.toUri();
|
.toUri();
|
||||||
final var mapped = mapper.map(saved, HsHostingAssetResource.class);
|
final var mapped = mapper.map(saved, HsHostingAssetResource.class, ENTITY_TO_RESOURCE_POSTMAPPER);
|
||||||
return ResponseEntity.created(uri).body(mapped);
|
return ResponseEntity.created(uri).body(mapped);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -94,7 +95,7 @@ public class HsHostingAssetController implements HsHostingAssetsApi {
|
|||||||
final var result = assetRepo.findByUuid(assetUuid);
|
final var result = assetRepo.findByUuid(assetUuid);
|
||||||
return result
|
return result
|
||||||
.map(assetEntity -> ResponseEntity.ok(
|
.map(assetEntity -> ResponseEntity.ok(
|
||||||
mapper.map(assetEntity, HsHostingAssetResource.class)))
|
mapper.map(assetEntity, HsHostingAssetResource.class, ENTITY_TO_RESOURCE_POSTMAPPER)))
|
||||||
.orElseGet(() -> ResponseEntity.notFound().build());
|
.orElseGet(() -> ResponseEntity.notFound().build());
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -126,8 +127,17 @@ public class HsHostingAssetController implements HsHostingAssetsApi {
|
|||||||
|
|
||||||
new HsHostingAssetEntityPatcher(em, current).apply(body);
|
new HsHostingAssetEntityPatcher(em, current).apply(body);
|
||||||
|
|
||||||
|
// TODO.refa: draft for an alternative API
|
||||||
|
// validate(current) // self-validation, hashing passwords etc.
|
||||||
|
// .then(HsHostingAssetEntityValidatorRegistry::prepareForSave) // hashing passwords etc.
|
||||||
|
// .then(assetRepo::save)
|
||||||
|
// .then(HsHostingAssetEntityValidatorRegistry::validateInContext)
|
||||||
|
// // In this last step we need the entity and the mapped resource instance,
|
||||||
|
// // which is exactly what a postmapper takes as arguments.
|
||||||
|
// .then(this::mapToResource) using postProcessProperties to remove write-only + add read-only properties
|
||||||
|
|
||||||
final var saved = validated(assetRepo.save(current));
|
final var saved = validated(assetRepo.save(current));
|
||||||
final var mapped = mapper.map(saved, HsHostingAssetResource.class);
|
final var mapped = mapper.map(saved, HsHostingAssetResource.class, ENTITY_TO_RESOURCE_POSTMAPPER);
|
||||||
return ResponseEntity.ok(mapped);
|
return ResponseEntity.ok(mapped);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -144,4 +154,7 @@ public class HsHostingAssetController implements HsHostingAssetsApi {
|
|||||||
resource.getParentAssetUuid()))));
|
resource.getParentAssetUuid()))));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
final BiConsumer<HsHostingAssetEntity, HsHostingAssetResource> ENTITY_TO_RESOURCE_POSTMAPPER
|
||||||
|
= HsHostingAssetEntityValidatorRegistry::postprocessProperties;
|
||||||
}
|
}
|
||||||
|
@ -9,6 +9,7 @@ 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.office.contact.HsOfficeContactEntity;
|
import net.hostsharing.hsadminng.hs.office.contact.HsOfficeContactEntity;
|
||||||
|
import net.hostsharing.hsadminng.hs.validation.PropertiesProvider;
|
||||||
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;
|
||||||
@ -39,6 +40,7 @@ import java.util.List;
|
|||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
|
||||||
|
import static java.util.Collections.emptyMap;
|
||||||
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.usingDefaultCase;
|
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.ColumnValue.usingDefaultCase;
|
||||||
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.GLOBAL;
|
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.GLOBAL;
|
||||||
@ -63,7 +65,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, PropertiesProvider {
|
||||||
|
|
||||||
private static Stringify<HsHostingAssetEntity> stringify = stringify(HsHostingAssetEntity.class)
|
private static Stringify<HsHostingAssetEntity> stringify = stringify(HsHostingAssetEntity.class)
|
||||||
.withProp(HsHostingAssetEntity::getType)
|
.withProp(HsHostingAssetEntity::getType)
|
||||||
@ -122,7 +124,7 @@ public class HsHostingAssetEntity implements Stringifyable, RbacObject {
|
|||||||
private PatchableMapWrapper<Object> configWrapper;
|
private PatchableMapWrapper<Object> configWrapper;
|
||||||
|
|
||||||
@Transient
|
@Transient
|
||||||
private boolean isLoaded = false;
|
private boolean isLoaded;
|
||||||
|
|
||||||
@PostLoad
|
@PostLoad
|
||||||
public void markAsLoaded() {
|
public void markAsLoaded() {
|
||||||
@ -137,6 +139,28 @@ public class HsHostingAssetEntity implements Stringifyable, RbacObject {
|
|||||||
PatchableMapWrapper.of(configWrapper, (newWrapper) -> {configWrapper = newWrapper; }, config).assign(newConfig);
|
PatchableMapWrapper.of(configWrapper, (newWrapper) -> {configWrapper = newWrapper; }, config).assign(newConfig);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Map<String, Object> directProps() {
|
||||||
|
return config;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Object getContextValue(final String propName) {
|
||||||
|
final var v = config.get(propName);
|
||||||
|
if (v!= null) {
|
||||||
|
return v;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bookingItem!=null) {
|
||||||
|
return bookingItem.getResources().get(propName);
|
||||||
|
}
|
||||||
|
if (parentAsset!=null && parentAsset.getBookingItem()!=null) {
|
||||||
|
return parentAsset.getBookingItem().getResources().get(propName);
|
||||||
|
}
|
||||||
|
return emptyMap();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String toString() {
|
public String toString() {
|
||||||
return stringify.apply(this);
|
return stringify.apply(this);
|
||||||
|
@ -47,7 +47,7 @@ public abstract class HsHostingAssetEntityValidator extends HsEntityValidator<Hs
|
|||||||
@Override
|
@Override
|
||||||
public List<String> validate(final HsHostingAssetEntity assetEntity) {
|
public List<String> validate(final HsHostingAssetEntity assetEntity) {
|
||||||
return sequentiallyValidate(
|
return sequentiallyValidate(
|
||||||
() -> validateEntityReferences(assetEntity),
|
() -> validateEntityReferencesAndProperties(assetEntity),
|
||||||
() -> validateIdentifierPattern(assetEntity), // might need proper parentAsset or billingItem
|
() -> validateIdentifierPattern(assetEntity), // might need proper parentAsset or billingItem
|
||||||
() -> optionallyValidate(assetEntity.getBookingItem()),
|
() -> optionallyValidate(assetEntity.getBookingItem()),
|
||||||
() -> optionallyValidate(assetEntity.getParentAsset()),
|
() -> optionallyValidate(assetEntity.getParentAsset()),
|
||||||
@ -55,7 +55,7 @@ public abstract class HsHostingAssetEntityValidator extends HsEntityValidator<Hs
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<String> validateEntityReferences(final HsHostingAssetEntity assetEntity) {
|
private List<String> validateEntityReferencesAndProperties(final HsHostingAssetEntity assetEntity) {
|
||||||
return Stream.of(
|
return Stream.of(
|
||||||
validateReferencedEntity(assetEntity, "bookingItem", bookingItemValidation::validate),
|
validateReferencedEntity(assetEntity, "bookingItem", bookingItemValidation::validate),
|
||||||
validateReferencedEntity(assetEntity, "parentAsset", parentAssetValidation::validate),
|
validateReferencedEntity(assetEntity, "parentAsset", parentAssetValidation::validate),
|
||||||
@ -76,7 +76,7 @@ public abstract class HsHostingAssetEntityValidator extends HsEntityValidator<Hs
|
|||||||
}
|
}
|
||||||
|
|
||||||
private List<String> validateProperties(final HsHostingAssetEntity assetEntity) {
|
private List<String> validateProperties(final HsHostingAssetEntity assetEntity) {
|
||||||
return enrich(prefix(assetEntity.toShortString(), "config"), validateProperties(assetEntity.getConfig()));
|
return enrich(prefix(assetEntity.toShortString(), "config"), super.validateProperties(assetEntity));
|
||||||
}
|
}
|
||||||
|
|
||||||
private static List<String> optionallyValidate(final HsHostingAssetEntity assetEntity) {
|
private static List<String> optionallyValidate(final HsHostingAssetEntity assetEntity) {
|
||||||
|
@ -2,6 +2,7 @@ package net.hostsharing.hsadminng.hs.hosting.asset.validators;
|
|||||||
|
|
||||||
import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetEntity;
|
import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetEntity;
|
||||||
import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType;
|
import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType;
|
||||||
|
import net.hostsharing.hsadminng.hs.hosting.generated.api.v1.model.HsHostingAssetResource;
|
||||||
import net.hostsharing.hsadminng.hs.validation.HsEntityValidator;
|
import net.hostsharing.hsadminng.hs.validation.HsEntityValidator;
|
||||||
import net.hostsharing.hsadminng.errors.MultiValidationException;
|
import net.hostsharing.hsadminng.errors.MultiValidationException;
|
||||||
|
|
||||||
@ -40,7 +41,8 @@ public class HsHostingAssetEntityValidatorRegistry {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public static List<String> doValidate(final HsHostingAssetEntity hostingAsset) {
|
public static List<String> doValidate(final HsHostingAssetEntity hostingAsset) {
|
||||||
return HsHostingAssetEntityValidatorRegistry.forType(hostingAsset.getType()).validate(hostingAsset);
|
final var validator = HsHostingAssetEntityValidatorRegistry.forType(hostingAsset.getType());
|
||||||
|
return validator.validate(hostingAsset);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static HsHostingAssetEntity validated(final HsHostingAssetEntity entityToSave) {
|
public static HsHostingAssetEntity validated(final HsHostingAssetEntity entityToSave) {
|
||||||
@ -48,4 +50,18 @@ public class HsHostingAssetEntityValidatorRegistry {
|
|||||||
return entityToSave;
|
return entityToSave;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static void postprocessProperties(final HsHostingAssetEntity entity, final HsHostingAssetResource resource) {
|
||||||
|
final var validator = HsHostingAssetEntityValidatorRegistry.forType(entity.getType());
|
||||||
|
final var config = validator.postProcess(entity, asMap(resource));
|
||||||
|
resource.setConfig(config);
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
private static Map<String, Object> asMap(final HsHostingAssetResource resource) {
|
||||||
|
if (resource.getConfig() instanceof Map map) {
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
throw new IllegalArgumentException("expected a Map, but got a " + resource.getConfig().getClass());
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -2,17 +2,35 @@ package net.hostsharing.hsadminng.hs.hosting.asset.validators;
|
|||||||
|
|
||||||
import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetEntity;
|
import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetEntity;
|
||||||
import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType;
|
import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType;
|
||||||
|
import net.hostsharing.hsadminng.hs.validation.PropertiesProvider;
|
||||||
|
|
||||||
import java.util.regex.Pattern;
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
|
import static net.hostsharing.hsadminng.hs.validation.EnumerationProperty.enumerationProperty;
|
||||||
|
import static net.hostsharing.hsadminng.hs.validation.IntegerProperty.integerProperty;
|
||||||
|
import static net.hostsharing.hsadminng.hs.validation.PasswordProperty.passwordProperty;
|
||||||
|
import static net.hostsharing.hsadminng.hs.validation.StringProperty.stringProperty;
|
||||||
|
|
||||||
class HsUnixUserHostingAssetValidator extends HsHostingAssetEntityValidator {
|
class HsUnixUserHostingAssetValidator extends HsHostingAssetEntityValidator {
|
||||||
|
|
||||||
|
private static final int DASH_LENGTH = "-".length();
|
||||||
|
|
||||||
HsUnixUserHostingAssetValidator() {
|
HsUnixUserHostingAssetValidator() {
|
||||||
super(BookingItem.mustBeNull(),
|
super( BookingItem.mustBeNull(),
|
||||||
ParentAsset.mustBeOfType(HsHostingAssetType.MANAGED_WEBSPACE),
|
ParentAsset.mustBeOfType(HsHostingAssetType.MANAGED_WEBSPACE),
|
||||||
AssignedToAsset.mustBeNull(),
|
AssignedToAsset.mustBeNull(),
|
||||||
AlarmContact.isOptional(), // TODO.spec: for quota notifications
|
AlarmContact.isOptional(),
|
||||||
NO_EXTRA_PROPERTIES); // TODO.spec: yet to be specified
|
|
||||||
|
integerProperty("SSD hard quota").unit("GB").maxFrom("SSD").optional(),
|
||||||
|
integerProperty("SSD soft quota").unit("GB").maxFrom("SSD hard quota").optional(),
|
||||||
|
integerProperty("HDD hard quota").unit("GB").maxFrom("HDD").optional(),
|
||||||
|
integerProperty("HDD soft quota").unit("GB").maxFrom("HDD hard quota").optional(),
|
||||||
|
enumerationProperty("shell")
|
||||||
|
.values("/bin/false", "/bin/bash", "/bin/csh", "/bin/dash", "/usr/bin/tcsh", "/usr/bin/zsh", "/usr/bin/passwd")
|
||||||
|
.withDefault("/bin/false"),
|
||||||
|
stringProperty("homedir").readOnly().computedBy(HsUnixUserHostingAssetValidator::computeHomedir),
|
||||||
|
stringProperty("totpKey").matchesRegEx("^0x([0-9A-Fa-f]{2})+$").minLength(20).maxLength(256).undisclosed().writeOnly().optional(),
|
||||||
|
passwordProperty("password").minLength(8).maxLength(40).writeOnly());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -20,4 +38,11 @@ class HsUnixUserHostingAssetValidator extends HsHostingAssetEntityValidator {
|
|||||||
final var webspaceIdentifier = assetEntity.getParentAsset().getIdentifier();
|
final var webspaceIdentifier = assetEntity.getParentAsset().getIdentifier();
|
||||||
return Pattern.compile("^"+webspaceIdentifier+"$|^"+webspaceIdentifier+"-[a-z0-9]+$");
|
return Pattern.compile("^"+webspaceIdentifier+"$|^"+webspaceIdentifier+"-[a-z0-9]+$");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static String computeHomedir(final PropertiesProvider propertiesProvider) {
|
||||||
|
final var entity = (HsHostingAssetEntity) propertiesProvider;
|
||||||
|
final var webspaceName = entity.getParentAsset().getIdentifier();
|
||||||
|
return "/home/pacs/" + webspaceName
|
||||||
|
+ "/users/" + entity.getIdentifier().substring(webspaceName.length()+DASH_LENGTH);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -4,7 +4,7 @@ import lombok.Setter;
|
|||||||
import net.hostsharing.hsadminng.mapper.Array;
|
import net.hostsharing.hsadminng.mapper.Array;
|
||||||
|
|
||||||
import java.util.AbstractMap;
|
import java.util.AbstractMap;
|
||||||
import java.util.ArrayList;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
|
|
||||||
@ -29,9 +29,9 @@ public class BooleanProperty extends ValidatableProperty<Boolean> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void validate(final ArrayList<String> result, final Boolean propValue, final Map<String, Object> props) {
|
protected void validate(final List<String> result, final Boolean propValue, final PropertiesProvider propProvider) {
|
||||||
if (falseIf != null && propValue) {
|
if (falseIf != null && propValue) {
|
||||||
final Object referencedValue = props.get(falseIf.getKey());
|
final Object referencedValue = propProvider.directProps().get(falseIf.getKey());
|
||||||
if (Objects.equals(referencedValue, falseIf.getValue())) {
|
if (Objects.equals(referencedValue, falseIf.getValue())) {
|
||||||
result.add(propertyName + "' is expected to be false because " +
|
result.add(propertyName + "' is expected to be false because " +
|
||||||
falseIf.getKey() + "=" + referencedValue + " but is " + propValue);
|
falseIf.getKey() + "=" + referencedValue + " but is " + propValue);
|
||||||
|
@ -3,9 +3,8 @@ package net.hostsharing.hsadminng.hs.validation;
|
|||||||
import lombok.Setter;
|
import lombok.Setter;
|
||||||
import net.hostsharing.hsadminng.mapper.Array;
|
import net.hostsharing.hsadminng.mapper.Array;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.Map;
|
import java.util.List;
|
||||||
|
|
||||||
import static java.util.Arrays.stream;
|
import static java.util.Arrays.stream;
|
||||||
|
|
||||||
@ -33,25 +32,25 @@ public class EnumerationProperty extends ValidatableProperty<String> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public void deferredInit(final ValidatableProperty<?>[] allProperties) {
|
public void deferredInit(final ValidatableProperty<?>[] allProperties) {
|
||||||
if (deferredInit != null) {
|
if (hasDeferredInit()) {
|
||||||
if (this.values != null) {
|
if (this.values != null) {
|
||||||
throw new IllegalStateException("property " + toString() + " already has values");
|
throw new IllegalStateException("property " + this + " already has values");
|
||||||
}
|
}
|
||||||
this.values = deferredInit.apply(allProperties);
|
this.values = doDeferredInit(allProperties);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public ValidatableProperty<String> valuesFromProperties(final String propertyNamePrefix) {
|
public ValidatableProperty<String> valuesFromProperties(final String propertyNamePrefix) {
|
||||||
this.deferredInit = (ValidatableProperty<?>[] allProperties) -> stream(allProperties)
|
this.setDeferredInit( (ValidatableProperty<?>[] allProperties) -> stream(allProperties)
|
||||||
.map(ValidatableProperty::propertyName)
|
.map(ValidatableProperty::propertyName)
|
||||||
.filter(name -> name.startsWith(propertyNamePrefix))
|
.filter(name -> name.startsWith(propertyNamePrefix))
|
||||||
.map(name -> name.substring(propertyNamePrefix.length()))
|
.map(name -> name.substring(propertyNamePrefix.length()))
|
||||||
.toArray(String[]::new);
|
.toArray(String[]::new));
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void validate(final ArrayList<String> result, final String propValue, final Map<String, Object> props) {
|
protected void validate(final List<String> result, final String propValue, final PropertiesProvider propProvider) {
|
||||||
if (stream(values).noneMatch(v -> v.equals(propValue))) {
|
if (stream(values).noneMatch(v -> v.equals(propValue))) {
|
||||||
result.add(propertyName + "' is expected to be one of " + Arrays.toString(values) + " but is '" + propValue + "'");
|
result.add(propertyName + "' is expected to be one of " + Arrays.toString(values) + " but is '" + propValue + "'");
|
||||||
}
|
}
|
||||||
|
@ -3,6 +3,7 @@ package net.hostsharing.hsadminng.hs.validation;
|
|||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
|
import java.util.HashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.function.Supplier;
|
import java.util.function.Supplier;
|
||||||
@ -10,7 +11,8 @@ import java.util.function.Supplier;
|
|||||||
import static java.util.Arrays.stream;
|
import static java.util.Arrays.stream;
|
||||||
import static java.util.Collections.emptyList;
|
import static java.util.Collections.emptyList;
|
||||||
|
|
||||||
public abstract class HsEntityValidator<E> {
|
// TODO.refa: rename to HsEntityProcessor, also subclasses
|
||||||
|
public abstract class HsEntityValidator<E extends PropertiesProvider> {
|
||||||
|
|
||||||
public final ValidatableProperty<?>[] propertyValidators;
|
public final ValidatableProperty<?>[] propertyValidators;
|
||||||
|
|
||||||
@ -38,16 +40,22 @@ public abstract class HsEntityValidator<E> {
|
|||||||
.toList();
|
.toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
protected ArrayList<String> validateProperties(final Map<String, Object> properties) {
|
protected ArrayList<String> validateProperties(final PropertiesProvider propsProvider) {
|
||||||
final var result = new ArrayList<String>();
|
final var result = new ArrayList<String>();
|
||||||
|
|
||||||
|
// verify that all actually given properties are specified
|
||||||
|
final var properties = propsProvider.directProps();
|
||||||
properties.keySet().forEach( givenPropName -> {
|
properties.keySet().forEach( givenPropName -> {
|
||||||
if (stream(propertyValidators).map(pv -> pv.propertyName).noneMatch(propName -> propName.equals(givenPropName))) {
|
if (stream(propertyValidators).map(pv -> pv.propertyName).noneMatch(propName -> propName.equals(givenPropName))) {
|
||||||
result.add(givenPropName + "' is not expected but is set to '" + properties.get(givenPropName) + "'");
|
result.add(givenPropName + "' is not expected but is set to '" + properties.get(givenPropName) + "'");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// run all property validators
|
||||||
stream(propertyValidators).forEach(pv -> {
|
stream(propertyValidators).forEach(pv -> {
|
||||||
result.addAll(pv.validate(properties));
|
result.addAll(pv.validate(propsProvider));
|
||||||
});
|
});
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -80,4 +88,17 @@ public abstract class HsEntityValidator<E> {
|
|||||||
}
|
}
|
||||||
throw new IllegalArgumentException("Integer value (or null) expected, but got " + value);
|
throw new IllegalArgumentException("Integer value (or null) expected, but got " + value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Map<String, Object> postProcess(final E entity, final Map<String, Object> config) {
|
||||||
|
final var copy = new HashMap<>(config);
|
||||||
|
stream(propertyValidators).forEach(p -> {
|
||||||
|
if ( p.isWriteOnly()) {
|
||||||
|
copy.remove(p.propertyName);
|
||||||
|
}
|
||||||
|
if (p.isComputed()) {
|
||||||
|
copy.put(p.propertyName, p.compute(entity));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return copy;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,21 +2,23 @@ package net.hostsharing.hsadminng.hs.validation;
|
|||||||
|
|
||||||
import lombok.Setter;
|
import lombok.Setter;
|
||||||
import net.hostsharing.hsadminng.mapper.Array;
|
import net.hostsharing.hsadminng.mapper.Array;
|
||||||
|
import org.apache.commons.lang3.Validate;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.List;
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
@Setter
|
@Setter
|
||||||
public class IntegerProperty extends ValidatableProperty<Integer> {
|
public class IntegerProperty extends ValidatableProperty<Integer> {
|
||||||
|
|
||||||
private final static String[] KEY_ORDER = Array.join(
|
private final static String[] KEY_ORDER = Array.join(
|
||||||
ValidatableProperty.KEY_ORDER_HEAD,
|
ValidatableProperty.KEY_ORDER_HEAD,
|
||||||
Array.of("unit", "min", "max", "step"),
|
Array.of("unit", "min", "minFrom", "max", "maxFrom", "step"),
|
||||||
ValidatableProperty.KEY_ORDER_TAIL);
|
ValidatableProperty.KEY_ORDER_TAIL);
|
||||||
|
|
||||||
private String unit;
|
private String unit;
|
||||||
private Integer min;
|
private Integer min;
|
||||||
|
private String minFrom;
|
||||||
private Integer max;
|
private Integer max;
|
||||||
|
private String maxFrom;
|
||||||
private Integer step;
|
private Integer step;
|
||||||
|
|
||||||
public static IntegerProperty integerProperty(final String propertyName) {
|
public static IntegerProperty integerProperty(final String propertyName) {
|
||||||
@ -27,6 +29,22 @@ public class IntegerProperty extends ValidatableProperty<Integer> {
|
|||||||
super(Integer.class, propertyName, KEY_ORDER);
|
super(Integer.class, propertyName, KEY_ORDER);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void deferredInit(final ValidatableProperty<?>[] allProperties) {
|
||||||
|
Validate.isTrue(min == null || minFrom == null, "min and minFrom are exclusive, but both are given");
|
||||||
|
Validate.isTrue(max == null || maxFrom == null, "max and maxFrom are exclusive, but both are given");
|
||||||
|
}
|
||||||
|
|
||||||
|
public IntegerProperty minFrom(final String propertyName) {
|
||||||
|
minFrom = propertyName;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public IntegerProperty maxFrom(final String propertyName) {
|
||||||
|
maxFrom = propertyName;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String unit() {
|
public String unit() {
|
||||||
return unit;
|
return unit;
|
||||||
@ -37,20 +55,34 @@ public class IntegerProperty extends ValidatableProperty<Integer> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void validate(final ArrayList<String> result, final Integer propValue, final Map<String, Object> props) {
|
protected void validate(final List<String> result, final Integer propValue, final PropertiesProvider propProvider) {
|
||||||
if (min != null && propValue < min) {
|
validateMin(result, propertyName, propValue, min);
|
||||||
result.add(propertyName + "' is expected to be >= " + min + " but is " + propValue);
|
validateMax(result, propertyName, propValue, max);
|
||||||
}
|
|
||||||
if (max != null && propValue > max) {
|
|
||||||
result.add(propertyName + "' is expected to be <= " + max + " but is " + propValue);
|
|
||||||
}
|
|
||||||
if (step != null && propValue % step != 0) {
|
if (step != null && propValue % step != 0) {
|
||||||
result.add(propertyName + "' is expected to be multiple of " + step + " but is " + propValue);
|
result.add(propertyName + "' is expected to be multiple of " + step + " but is " + propValue);
|
||||||
}
|
}
|
||||||
|
if (minFrom != null) {
|
||||||
|
validateMin(result, propertyName, propValue, propProvider.getContextValue(minFrom, Integer.class));
|
||||||
|
}
|
||||||
|
if (maxFrom != null) {
|
||||||
|
validateMax(result, propertyName, propValue, propProvider.getContextValue(maxFrom, Integer.class, 0));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected String simpleTypeName() {
|
protected String simpleTypeName() {
|
||||||
return "integer";
|
return "integer";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static void validateMin(final List<String> result, final String propertyName, final Integer propValue, final Integer min) {
|
||||||
|
if (min != null && propValue < min) {
|
||||||
|
result.add(propertyName + "' is expected to be at least " + min + " but is " + propValue);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void validateMax(final List<String> result, final String propertyName, final Integer propValue, final Integer max) {
|
||||||
|
if (max != null && propValue > max) {
|
||||||
|
result.add(propertyName + "' is expected to be at most " + max + " but is " + propValue);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,65 @@
|
|||||||
|
package net.hostsharing.hsadminng.hs.validation;
|
||||||
|
|
||||||
|
import lombok.Setter;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
|
@Setter
|
||||||
|
public class PasswordProperty extends StringProperty {
|
||||||
|
|
||||||
|
private PasswordProperty(final String propertyName) {
|
||||||
|
super(propertyName);
|
||||||
|
undisclosed();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static PasswordProperty passwordProperty(final String propertyName) {
|
||||||
|
return new PasswordProperty(propertyName);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void validate(final List<String> result, final String propValue, final PropertiesProvider propProvider) {
|
||||||
|
super.validate(result, propValue, propProvider);
|
||||||
|
validatePassword(result, propValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO.impl: only a SHA512 hash should be stored in the database, not the password itself
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected String simpleTypeName() {
|
||||||
|
return "password";
|
||||||
|
}
|
||||||
|
|
||||||
|
private void validatePassword(final List<String> result, final String password) {
|
||||||
|
boolean hasLowerCase = false;
|
||||||
|
boolean hasUpperCase = false;
|
||||||
|
boolean hasDigit = false;
|
||||||
|
boolean hasSpecialChar = false;
|
||||||
|
boolean containsColon = false;
|
||||||
|
|
||||||
|
for (char c : password.toCharArray()) {
|
||||||
|
if (Character.isLowerCase(c)) {
|
||||||
|
hasLowerCase = true;
|
||||||
|
} else if (Character.isUpperCase(c)) {
|
||||||
|
hasUpperCase = true;
|
||||||
|
} else if (Character.isDigit(c)) {
|
||||||
|
hasDigit = true;
|
||||||
|
} else if (!Character.isLetterOrDigit(c)) {
|
||||||
|
hasSpecialChar = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (c == ':') {
|
||||||
|
containsColon = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final long groupsCovered = Stream.of(hasLowerCase, hasUpperCase, hasDigit, hasSpecialChar).filter(v->v).count();
|
||||||
|
if ( groupsCovered < 3) {
|
||||||
|
result.add(propertyName + "' must contain at least one character of at least 3 of the following groups: upper case letters, lower case letters, digits, special characters");
|
||||||
|
}
|
||||||
|
if (containsColon) {
|
||||||
|
result.add(propertyName + "' must not contain colon (':')");
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,31 @@
|
|||||||
|
package net.hostsharing.hsadminng.hs.validation;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
public interface PropertiesProvider {
|
||||||
|
|
||||||
|
Map<String, Object> directProps();
|
||||||
|
Object getContextValue(final String propName);
|
||||||
|
|
||||||
|
default <T> T getDirectValue(final String propName, final Class<T> clazz) {
|
||||||
|
return cast(propName, directProps().get(propName), clazz, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
default <T> T getContextValue(final String propName, final Class<T> clazz) {
|
||||||
|
return cast(propName, getContextValue(propName), clazz, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
default <T> T getContextValue(final String propName, final Class<T> clazz, final T defaultValue) {
|
||||||
|
return cast(propName, getContextValue(propName), clazz, defaultValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static <T> T cast( final String propName, final Object value, final Class<T> clazz, final T defaultValue) {
|
||||||
|
if (value == null && defaultValue != null) {
|
||||||
|
return defaultValue;
|
||||||
|
}
|
||||||
|
if (value == null || clazz.isInstance(value)) {
|
||||||
|
return clazz.cast(value);
|
||||||
|
}
|
||||||
|
throw new IllegalStateException(propName + " expected to be an "+clazz.getSimpleName()+", but got '" + value + "'");
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,79 @@
|
|||||||
|
package net.hostsharing.hsadminng.hs.validation;
|
||||||
|
|
||||||
|
import lombok.Setter;
|
||||||
|
import net.hostsharing.hsadminng.mapper.Array;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
|
@Setter
|
||||||
|
public class StringProperty extends ValidatableProperty<String> {
|
||||||
|
|
||||||
|
private static final String[] KEY_ORDER = Array.join(
|
||||||
|
ValidatableProperty.KEY_ORDER_HEAD,
|
||||||
|
Array.of("matchesRegEx", "minLength", "maxLength"),
|
||||||
|
ValidatableProperty.KEY_ORDER_TAIL,
|
||||||
|
Array.of("undisclosed"));
|
||||||
|
private Pattern matchesRegEx;
|
||||||
|
private Integer minLength;
|
||||||
|
private Integer maxLength;
|
||||||
|
private boolean undisclosed;
|
||||||
|
|
||||||
|
protected StringProperty(final String propertyName) {
|
||||||
|
super(String.class, propertyName, KEY_ORDER);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static StringProperty stringProperty(final String propertyName) {
|
||||||
|
return new StringProperty(propertyName);
|
||||||
|
}
|
||||||
|
|
||||||
|
public StringProperty minLength(final int minLength) {
|
||||||
|
this.minLength = minLength;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public StringProperty maxLength(final int maxLength) {
|
||||||
|
this.maxLength = maxLength;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public StringProperty matchesRegEx(final String regExPattern) {
|
||||||
|
this.matchesRegEx = Pattern.compile(regExPattern);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The property value is not disclosed in error messages.
|
||||||
|
*
|
||||||
|
* @return this;
|
||||||
|
*/
|
||||||
|
public StringProperty undisclosed() {
|
||||||
|
this.undisclosed = true;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void validate(final List<String> result, final String propValue, final PropertiesProvider propProvider) {
|
||||||
|
if (minLength != null && propValue.length()<minLength) {
|
||||||
|
result.add(propertyName + "' length is expected to be at min " + minLength + " but length of " + display(propValue) + " is " + propValue.length());
|
||||||
|
}
|
||||||
|
if (maxLength != null && propValue.length()>maxLength) {
|
||||||
|
result.add(propertyName + "' length is expected to be at max " + maxLength + " but length of " + display(propValue) + " is " + propValue.length());
|
||||||
|
}
|
||||||
|
if (matchesRegEx != null && !matchesRegEx.matcher(propValue).matches()) {
|
||||||
|
result.add(propertyName + "' is expected to be match " + matchesRegEx + " but " + display(propValue) + " does not match");
|
||||||
|
}
|
||||||
|
if (isReadOnly() && propValue != null) {
|
||||||
|
result.add(propertyName + "' is readonly but given as " + display(propValue));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String display(final String propValue) {
|
||||||
|
return undisclosed ? "provided value" : ("'" + propValue + "'");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected String simpleTypeName() {
|
||||||
|
return "string";
|
||||||
|
}
|
||||||
|
}
|
@ -1,6 +1,8 @@
|
|||||||
package net.hostsharing.hsadminng.hs.validation;
|
package net.hostsharing.hsadminng.hs.validation;
|
||||||
|
|
||||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||||
|
import lombok.experimental.Accessors;
|
||||||
|
import lombok.Getter;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import lombok.SneakyThrows;
|
import lombok.SneakyThrows;
|
||||||
import net.hostsharing.hsadminng.hs.booking.item.HsBookingItemEntity;
|
import net.hostsharing.hsadminng.hs.booking.item.HsBookingItemEntity;
|
||||||
@ -21,19 +23,37 @@ import static java.lang.Boolean.TRUE;
|
|||||||
import static java.util.Collections.emptyList;
|
import static java.util.Collections.emptyList;
|
||||||
import static java.util.Optional.ofNullable;
|
import static java.util.Optional.ofNullable;
|
||||||
|
|
||||||
|
@Getter
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
public abstract class ValidatableProperty<T> {
|
public abstract class ValidatableProperty<T> {
|
||||||
|
|
||||||
protected static final String[] KEY_ORDER_HEAD = Array.of("propertyName");
|
protected static final String[] KEY_ORDER_HEAD = Array.of("propertyName");
|
||||||
protected static final String[] KEY_ORDER_TAIL = Array.of("required", "defaultValue", "isTotalsValidator", "thresholdPercentage");
|
protected static final String[] KEY_ORDER_TAIL = Array.of("required", "defaultValue", "readOnly", "writeOnly", "computed", "isTotalsValidator", "thresholdPercentage");
|
||||||
|
|
||||||
final Class<T> type;
|
final Class<T> type;
|
||||||
final String propertyName;
|
final String propertyName;
|
||||||
|
|
||||||
|
@JsonIgnore
|
||||||
private final String[] keyOrder;
|
private final String[] keyOrder;
|
||||||
|
|
||||||
private Boolean required;
|
private Boolean required;
|
||||||
private T defaultValue;
|
private T defaultValue;
|
||||||
protected Function<ValidatableProperty<?>[], T[]> deferredInit;
|
|
||||||
|
@JsonIgnore
|
||||||
|
private Function<PropertiesProvider, T> computedBy;
|
||||||
|
|
||||||
|
@Accessors(makeFinal = true, chain = true, fluent = false)
|
||||||
|
private boolean computed; // used in descriptor, because computedBy cannot be rendered to a text string
|
||||||
|
|
||||||
|
@Accessors(makeFinal = true, chain = true, fluent = false)
|
||||||
|
private boolean readOnly;
|
||||||
|
|
||||||
|
@Accessors(makeFinal = true, chain = true, fluent = false)
|
||||||
|
private boolean writeOnly;
|
||||||
|
|
||||||
|
private Function<ValidatableProperty<?>[], T[]> deferredInit;
|
||||||
private boolean isTotalsValidator = false;
|
private boolean isTotalsValidator = false;
|
||||||
|
|
||||||
@JsonIgnore
|
@JsonIgnore
|
||||||
private List<Function<HsBookingItemEntity, List<String>>> asTotalLimitValidators; // TODO.impl: move to BookingItemIntegerProperty
|
private List<Function<HsBookingItemEntity, List<String>>> asTotalLimitValidators; // TODO.impl: move to BookingItemIntegerProperty
|
||||||
|
|
||||||
@ -43,6 +63,30 @@ public abstract class ValidatableProperty<T> {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected void setDeferredInit(final Function<ValidatableProperty<?>[], T[]> function) {
|
||||||
|
this.deferredInit = function;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean hasDeferredInit() {
|
||||||
|
return deferredInit != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public T[] doDeferredInit(final ValidatableProperty<?>[] allProperties) {
|
||||||
|
return deferredInit.apply(allProperties);
|
||||||
|
}
|
||||||
|
|
||||||
|
public ValidatableProperty<T> writeOnly() {
|
||||||
|
this.writeOnly = true;
|
||||||
|
optional();
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ValidatableProperty<T> readOnly() {
|
||||||
|
this.readOnly = true;
|
||||||
|
optional();
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
public ValidatableProperty<T> required() {
|
public ValidatableProperty<T> required() {
|
||||||
required = TRUE;
|
required = TRUE;
|
||||||
return this;
|
return this;
|
||||||
@ -116,8 +160,9 @@ public abstract class ValidatableProperty<T> {
|
|||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public final List<String> validate(final Map<String, Object> props) {
|
public final List<String> validate(final PropertiesProvider propsProvider) {
|
||||||
final var result = new ArrayList<String>();
|
final var result = new ArrayList<String>();
|
||||||
|
final var props = propsProvider.directProps();
|
||||||
final var propValue = props.get(propertyName);
|
final var propValue = props.get(propertyName);
|
||||||
if (propValue == null) {
|
if (propValue == null) {
|
||||||
if (required) {
|
if (required) {
|
||||||
@ -127,7 +172,7 @@ public abstract class ValidatableProperty<T> {
|
|||||||
if (propValue != null){
|
if (propValue != null){
|
||||||
if ( type.isInstance(propValue)) {
|
if ( type.isInstance(propValue)) {
|
||||||
//noinspection unchecked
|
//noinspection unchecked
|
||||||
validate(result, (T) propValue, props);
|
validate(result, (T) propValue, propsProvider);
|
||||||
} else {
|
} else {
|
||||||
result.add(propertyName + "' is expected to be of type " + type + ", " +
|
result.add(propertyName + "' is expected to be of type " + type + ", " +
|
||||||
"but is of type '" + propValue.getClass().getSimpleName() + "'");
|
"but is of type '" + propValue.getClass().getSimpleName() + "'");
|
||||||
@ -136,7 +181,7 @@ public abstract class ValidatableProperty<T> {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected abstract void validate(final ArrayList<String> result, final T propValue, final Map<String, Object> props);
|
protected abstract void validate(final List<String> result, final T propValue, final PropertiesProvider propProvider);
|
||||||
|
|
||||||
public void verifyConsistency(final Map.Entry<? extends Enum<?>, ?> typeDef) {
|
public void verifyConsistency(final Map.Entry<? extends Enum<?>, ?> typeDef) {
|
||||||
if (required == null ) {
|
if (required == null ) {
|
||||||
@ -158,26 +203,32 @@ public abstract class ValidatableProperty<T> {
|
|||||||
// Add entries according to the given order
|
// Add entries according to the given order
|
||||||
for (String key : keyOrder) {
|
for (String key : keyOrder) {
|
||||||
final Optional<Object> propValue = getPropertyValue(key);
|
final Optional<Object> propValue = getPropertyValue(key);
|
||||||
propValue.ifPresent(o -> sortedMap.put(key, o));
|
propValue.filter(ValidatableProperty::isToBeRendered).ifPresent(o -> sortedMap.put(key, o));
|
||||||
}
|
}
|
||||||
|
|
||||||
return sortedMap;
|
return sortedMap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static boolean isToBeRendered(final Object v) {
|
||||||
|
return !(v instanceof Boolean b) || b;
|
||||||
|
}
|
||||||
|
|
||||||
@SneakyThrows
|
@SneakyThrows
|
||||||
private Optional<Object> getPropertyValue(final String key) {
|
private Optional<Object> getPropertyValue(final String key) {
|
||||||
|
return getPropertyValue(getClass(), key);
|
||||||
|
}
|
||||||
|
|
||||||
|
@SneakyThrows
|
||||||
|
private Optional<Object> getPropertyValue(final Class<?> clazz, final String key) {
|
||||||
try {
|
try {
|
||||||
final var field = getClass().getDeclaredField(key);
|
final var field = clazz.getDeclaredField(key);
|
||||||
field.setAccessible(true);
|
field.setAccessible(true);
|
||||||
return Optional.ofNullable(arrayToList(field.get(this)));
|
return Optional.ofNullable(arrayToList(field.get(this)));
|
||||||
} catch (final NoSuchFieldException e1) {
|
} catch (final NoSuchFieldException exc) {
|
||||||
try {
|
if (clazz.getSuperclass() != null) {
|
||||||
final var field = getClass().getSuperclass().getDeclaredField(key);
|
return getPropertyValue(clazz.getSuperclass(), key);
|
||||||
field.setAccessible(true);
|
|
||||||
return Optional.ofNullable(arrayToList(field.get(this)));
|
|
||||||
} catch (final NoSuchFieldException e2) {
|
|
||||||
return Optional.empty();
|
|
||||||
}
|
}
|
||||||
|
throw exc;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -198,4 +249,14 @@ public abstract class ValidatableProperty<T> {
|
|||||||
.flatMap(Collection::stream)
|
.flatMap(Collection::stream)
|
||||||
.toList();
|
.toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public ValidatableProperty<T> computedBy(final Function<PropertiesProvider, T> compute) {
|
||||||
|
this.computedBy = compute;
|
||||||
|
this.computed = true;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public <E extends PropertiesProvider> T compute(final E entity) {
|
||||||
|
return computedBy.apply(entity);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
package net.hostsharing.hsadminng.mapper;
|
package net.hostsharing.hsadminng.mapper;
|
||||||
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
@ -43,4 +44,8 @@ public class Array {
|
|||||||
.toArray(String[]::new);
|
.toArray(String[]::new);
|
||||||
return joined;
|
return joined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static <T> T[] emptyArray() {
|
||||||
|
return of();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -53,13 +53,20 @@ public class PatchableMapWrapper<T> implements Map<String, T> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public String toString() {
|
public String toString() {
|
||||||
return "{ "
|
return "{\n"
|
||||||
+ (
|
+ (
|
||||||
keySet().stream().sorted()
|
keySet().stream().sorted()
|
||||||
.map(k -> k + ": " + get(k)))
|
.map(k -> " \"" + k + "\": " + optionallyQuoted(get(k))))
|
||||||
.collect(joining(", ")
|
.collect(joining(",\n")
|
||||||
)
|
)
|
||||||
+ " }";
|
+ "\n}\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
private Object optionallyQuoted(final Object value) {
|
||||||
|
if ( value instanceof Number || value instanceof Boolean ) {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
return "\"" + value + "\"";
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- below just delegating methods --------------------------------
|
// --- below just delegating methods --------------------------------
|
||||||
|
@ -53,7 +53,7 @@ class HsBookingItemEntityUnitTest {
|
|||||||
void toStringContainsAllPropertiesAndResourcesSortedByKey() {
|
void toStringContainsAllPropertiesAndResourcesSortedByKey() {
|
||||||
final var result = givenBookingItem.toString();
|
final var result = givenBookingItem.toString();
|
||||||
|
|
||||||
assertThat(result).isEqualTo("HsBookingItemEntity(D-1234500:test project, CLOUD_SERVER, [2020-01-01,2031-01-01), some caption, { CPUs: 2, HDD-storage: 2048, SSD-storage: 512 })");
|
assertThat(result).isEqualToIgnoringWhitespace("HsBookingItemEntity(D-1234500:test project, CLOUD_SERVER, [2020-01-01,2031-01-01), some caption, { \"CPUs\": 2, \"HDD-storage\": 2048, \"SSD-storage\": 512 })");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
@ -170,9 +170,9 @@ class HsBookingItemRepositoryIntegrationTest extends ContextBasedTestWithCleanup
|
|||||||
// then
|
// then
|
||||||
allTheseBookingItemsAreReturned(
|
allTheseBookingItemsAreReturned(
|
||||||
result,
|
result,
|
||||||
"HsBookingItemEntity(D-1000212:D-1000212 default project, MANAGED_WEBSPACE, [2022-10-01,), separate ManagedWebspace, { Daemons: 0, Multi: 1, SSD: 100, Traffic: 50 })",
|
"HsBookingItemEntity(D-1000212:D-1000212 default project, MANAGED_WEBSPACE, [2022-10-01,), separate ManagedWebspace, { Daemons: 0, Multi: 1, SSD: 100, Traffic: 50 } )",
|
||||||
"HsBookingItemEntity(D-1000212:D-1000212 default project, MANAGED_SERVER, [2022-10-01,), separate ManagedServer, { CPUs: 2, RAM: 8, SSD: 500, Traffic: 500 })",
|
"HsBookingItemEntity(D-1000212:D-1000212 default project, MANAGED_SERVER, [2022-10-01,), separate ManagedServer, { CPUs: 2, RAM: 8, SSD: 500, Traffic: 500 } )",
|
||||||
"HsBookingItemEntity(D-1000212:D-1000212 default project, PRIVATE_CLOUD, [2024-04-01,), some PrivateCloud, { CPUs: 10, HDD: 10000, RAM: 32, SSD: 4000, Traffic: 2000 })");
|
"HsBookingItemEntity(D-1000212:D-1000212 default project, PRIVATE_CLOUD, [2024-04-01,), some PrivateCloud, { CPUs: 10, HDD: 10000, RAM: 32, SSD: 4000, Traffic: 2000 } )");
|
||||||
assertThat(result.stream().filter(bi -> bi.getRelatedHostingAsset()!=null).findAny())
|
assertThat(result.stream().filter(bi -> bi.getRelatedHostingAsset()!=null).findAny())
|
||||||
.as("at least one relatedProject expected, but none found => fetching relatedProject does not work")
|
.as("at least one relatedProject expected, but none found => fetching relatedProject does not work")
|
||||||
.isNotEmpty();
|
.isNotEmpty();
|
||||||
@ -193,9 +193,9 @@ class HsBookingItemRepositoryIntegrationTest extends ContextBasedTestWithCleanup
|
|||||||
// then:
|
// then:
|
||||||
exactlyTheseBookingItemsAreReturned(
|
exactlyTheseBookingItemsAreReturned(
|
||||||
result,
|
result,
|
||||||
"HsBookingItemEntity(D-1000111:D-1000111 default project, MANAGED_SERVER, [2022-10-01,), separate ManagedServer, { CPUs: 2, RAM: 8, SSD: 500, Traffic: 500 })",
|
"HsBookingItemEntity(D-1000111:D-1000111 default project, MANAGED_WEBSPACE, [2022-10-01,), separate ManagedWebspace, { Daemons: 0, Multi: 1, SSD: 100, Traffic: 50 } )",
|
||||||
"HsBookingItemEntity(D-1000111:D-1000111 default project, MANAGED_WEBSPACE, [2022-10-01,), separate ManagedWebspace, { Daemons: 0, Multi: 1, SSD: 100, Traffic: 50 })",
|
"HsBookingItemEntity(D-1000111:D-1000111 default project, MANAGED_SERVER, [2022-10-01,), separate ManagedServer, { CPUs: 2, RAM: 8, SSD: 500, Traffic: 500 } )",
|
||||||
"HsBookingItemEntity(D-1000111:D-1000111 default project, PRIVATE_CLOUD, [2024-04-01,), some PrivateCloud, { CPUs: 10, HDD: 10000, RAM: 32, SSD: 4000, Traffic: 2000 })");
|
"HsBookingItemEntity(D-1000111:D-1000111 default project, PRIVATE_CLOUD, [2024-04-01,), some PrivateCloud, { CPUs: 10, HDD: 10000, RAM: 32, SSD: 4000, Traffic: 2000 } )");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -348,13 +348,17 @@ class HsBookingItemRepositoryIntegrationTest extends ContextBasedTestWithCleanup
|
|||||||
final List<HsBookingItemEntity> actualResult,
|
final List<HsBookingItemEntity> actualResult,
|
||||||
final String... bookingItemNames) {
|
final String... bookingItemNames) {
|
||||||
assertThat(actualResult)
|
assertThat(actualResult)
|
||||||
.extracting(bookingItemEntity -> bookingItemEntity.toString())
|
.extracting(HsBookingItemEntity::toString)
|
||||||
|
.extracting(string-> string.replaceAll("\\s+", " "))
|
||||||
|
.extracting(string-> string.replaceAll("\"", ""))
|
||||||
.containsExactlyInAnyOrder(bookingItemNames);
|
.containsExactlyInAnyOrder(bookingItemNames);
|
||||||
}
|
}
|
||||||
|
|
||||||
void allTheseBookingItemsAreReturned(final List<HsBookingItemEntity> actualResult, final String... bookingItemNames) {
|
void allTheseBookingItemsAreReturned(final List<HsBookingItemEntity> actualResult, final String... bookingItemNames) {
|
||||||
assertThat(actualResult)
|
assertThat(actualResult)
|
||||||
.extracting(bookingItemEntity -> bookingItemEntity.toString())
|
.extracting(HsBookingItemEntity::toString)
|
||||||
|
.extracting(string -> string.replaceAll("\\s+", " "))
|
||||||
|
.extracting(string -> string.replaceAll("\"", ""))
|
||||||
.contains(bookingItemNames);
|
.contains(bookingItemNames);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -12,21 +12,35 @@ import static net.hostsharing.hsadminng.hs.booking.project.TestHsBookingProject.
|
|||||||
@UtilityClass
|
@UtilityClass
|
||||||
public class TestHsBookingItem {
|
public class TestHsBookingItem {
|
||||||
|
|
||||||
public static final HsBookingItemEntity TEST_MANAGED_SERVER_BOOKING_ITEM = HsBookingItemEntity.builder()
|
|
||||||
.project(TEST_PROJECT)
|
|
||||||
.type(HsBookingItemType.MANAGED_SERVER)
|
|
||||||
.caption("test project booking item")
|
|
||||||
.resources(Map.ofEntries(
|
|
||||||
entry("someThing", 1),
|
|
||||||
entry("anotherThing", "blue")
|
|
||||||
))
|
|
||||||
.validity(Range.closedInfinite(LocalDate.of(2020, 1, 15)))
|
|
||||||
.build();
|
|
||||||
|
|
||||||
public static final HsBookingItemEntity TEST_CLOUD_SERVER_BOOKING_ITEM = HsBookingItemEntity.builder()
|
public static final HsBookingItemEntity TEST_CLOUD_SERVER_BOOKING_ITEM = HsBookingItemEntity.builder()
|
||||||
.project(TEST_PROJECT)
|
.project(TEST_PROJECT)
|
||||||
.type(HsBookingItemType.CLOUD_SERVER)
|
.type(HsBookingItemType.CLOUD_SERVER)
|
||||||
.caption("test cloud server booking item")
|
.caption("test cloud server booking item")
|
||||||
.validity(Range.closedInfinite(LocalDate.of(2020, 1, 15)))
|
.validity(Range.closedInfinite(LocalDate.of(2020, 1, 15)))
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
|
public static final HsBookingItemEntity TEST_MANAGED_SERVER_BOOKING_ITEM = HsBookingItemEntity.builder()
|
||||||
|
.project(TEST_PROJECT)
|
||||||
|
.type(HsBookingItemType.MANAGED_SERVER)
|
||||||
|
.caption("test project booking item")
|
||||||
|
.resources(Map.ofEntries(
|
||||||
|
entry("CPUs", 2),
|
||||||
|
entry("RAM", 4),
|
||||||
|
entry("SSD", 50),
|
||||||
|
entry("Traffic", 250)
|
||||||
|
))
|
||||||
|
.validity(Range.closedInfinite(LocalDate.of(2020, 1, 15)))
|
||||||
|
.build();
|
||||||
|
|
||||||
|
public static final HsBookingItemEntity TEST_MANAGED_WEBSPACE_BOOKING_ITEM = HsBookingItemEntity.builder()
|
||||||
|
.parentItem(TEST_MANAGED_SERVER_BOOKING_ITEM)
|
||||||
|
.type(HsBookingItemType.MANAGED_WEBSPACE)
|
||||||
|
.caption("test managed webspace item")
|
||||||
|
.resources(Map.ofEntries(
|
||||||
|
entry("SSD", 50),
|
||||||
|
entry("Traffic", 250)
|
||||||
|
))
|
||||||
|
.validity(Range.closedInfinite(LocalDate.of(2020, 1, 15)))
|
||||||
|
.build();
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -25,12 +25,14 @@ import org.springframework.transaction.annotation.Transactional;
|
|||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
import java.util.function.Supplier;
|
||||||
|
|
||||||
import static java.util.Map.entry;
|
import static java.util.Map.entry;
|
||||||
import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.MANAGED_SERVER;
|
import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.MANAGED_SERVER;
|
||||||
import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.MANAGED_WEBSPACE;
|
import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.MANAGED_WEBSPACE;
|
||||||
import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.UNIX_USER;
|
import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.UNIX_USER;
|
||||||
import static net.hostsharing.hsadminng.rbac.test.JsonMatcher.lenientlyEquals;
|
import static net.hostsharing.hsadminng.rbac.test.JsonMatcher.lenientlyEquals;
|
||||||
|
import static net.hostsharing.hsadminng.rbac.test.JsonMatcher.strictlyEquals;
|
||||||
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;
|
||||||
|
|
||||||
@ -73,7 +75,7 @@ class HsHostingAssetControllerAcceptanceTest extends ContextBasedTestWithCleanup
|
|||||||
// given
|
// given
|
||||||
context("superuser-alex@hostsharing.net");
|
context("superuser-alex@hostsharing.net");
|
||||||
final var givenProject = projectRepo.findByCaption("D-1000111 default project").stream()
|
final var givenProject = projectRepo.findByCaption("D-1000111 default project").stream()
|
||||||
.findAny().orElseThrow();
|
.findAny().orElseThrow();
|
||||||
|
|
||||||
RestAssured // @formatter:off
|
RestAssured // @formatter:off
|
||||||
.given()
|
.given()
|
||||||
@ -264,7 +266,8 @@ class HsHostingAssetControllerAcceptanceTest extends ContextBasedTestWithCleanup
|
|||||||
void propertyValidationsArePerformend_whenAddingAsset() {
|
void propertyValidationsArePerformend_whenAddingAsset() {
|
||||||
|
|
||||||
context.define("superuser-alex@hostsharing.net");
|
context.define("superuser-alex@hostsharing.net");
|
||||||
final var givenBookingItem = givenSomeNewBookingItem("D-1000111 default project",
|
final var givenBookingItem = givenSomeNewBookingItem(
|
||||||
|
"D-1000111 default project",
|
||||||
HsBookingItemType.MANAGED_SERVER,
|
HsBookingItemType.MANAGED_SERVER,
|
||||||
"some PrivateCloud");
|
"some PrivateCloud");
|
||||||
|
|
||||||
@ -292,14 +295,13 @@ class HsHostingAssetControllerAcceptanceTest extends ContextBasedTestWithCleanup
|
|||||||
"statusPhrase": "Bad Request",
|
"statusPhrase": "Bad Request",
|
||||||
"message": "[
|
"message": "[
|
||||||
<<<'MANAGED_SERVER:vm1400.config.extra' is not expected but is set to '42',
|
<<<'MANAGED_SERVER:vm1400.config.extra' is not expected but is set to '42',
|
||||||
<<<'MANAGED_SERVER:vm1400.config.monit_max_cpu_usage' is expected to be <= 100 but is 101,
|
<<<'MANAGED_SERVER:vm1400.config.monit_max_cpu_usage' is expected to be at most 100 but is 101,
|
||||||
<<<'MANAGED_SERVER:vm1400.config.monit_max_ssd_usage' is expected to be >= 10 but is 0
|
<<<'MANAGED_SERVER:vm1400.config.monit_max_ssd_usage' is expected to be at least 10 but is 0
|
||||||
<<<]"
|
<<<]"
|
||||||
}
|
}
|
||||||
""".replaceAll(" +<<<", ""))); // @formatter:on
|
""".replaceAll(" +<<<", ""))); // @formatter:on
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void totalsLimitValidationsArePerformend_whenAddingAsset() {
|
void totalsLimitValidationsArePerformend_whenAddingAsset() {
|
||||||
|
|
||||||
@ -311,7 +313,7 @@ class HsHostingAssetControllerAcceptanceTest extends ContextBasedTestWithCleanup
|
|||||||
|
|
||||||
jpaAttempt.transacted(() -> {
|
jpaAttempt.transacted(() -> {
|
||||||
context.define("superuser-alex@hostsharing.net");
|
context.define("superuser-alex@hostsharing.net");
|
||||||
for (int n = 0; n < 25; ++n ) {
|
for (int n = 0; n < 25; ++n) {
|
||||||
toCleanup(assetRepo.save(
|
toCleanup(assetRepo.save(
|
||||||
HsHostingAssetEntity.builder()
|
HsHostingAssetEntity.builder()
|
||||||
.type(UNIX_USER)
|
.type(UNIX_USER)
|
||||||
@ -358,8 +360,8 @@ class HsHostingAssetControllerAcceptanceTest extends ContextBasedTestWithCleanup
|
|||||||
void globalAdmin_canGetArbitraryAsset() {
|
void globalAdmin_canGetArbitraryAsset() {
|
||||||
context.define("superuser-alex@hostsharing.net");
|
context.define("superuser-alex@hostsharing.net");
|
||||||
final var givenAssetUuid = assetRepo.findByIdentifier("vm1011").stream()
|
final var givenAssetUuid = assetRepo.findByIdentifier("vm1011").stream()
|
||||||
.filter(bi -> bi.getBookingItem().getProject().getCaption().equals("D-1000111 default project"))
|
.filter(bi -> bi.getBookingItem().getProject().getCaption().equals("D-1000111 default project"))
|
||||||
.findAny().orElseThrow().getUuid();
|
.findAny().orElseThrow().getUuid();
|
||||||
|
|
||||||
RestAssured // @formatter:off
|
RestAssured // @formatter:off
|
||||||
.given()
|
.given()
|
||||||
@ -429,8 +431,23 @@ class HsHostingAssetControllerAcceptanceTest extends ContextBasedTestWithCleanup
|
|||||||
@Test
|
@Test
|
||||||
void globalAdmin_canPatchAllUpdatablePropertiesOfAsset() {
|
void globalAdmin_canPatchAllUpdatablePropertiesOfAsset() {
|
||||||
|
|
||||||
final var givenAsset = givenSomeTemporaryHostingAsset("2001", MANAGED_SERVER,
|
final var givenAsset = givenSomeTemporaryHostingAsset(() ->
|
||||||
config("monit_max_ssd_usage", 80), config("monit_max_hdd_usage", 90), config("monit_max_cpu_usage", 90), config("monit_max_ram_usage", 70));
|
HsHostingAssetEntity.builder()
|
||||||
|
.uuid(UUID.randomUUID())
|
||||||
|
.bookingItem(givenSomeNewBookingItem(
|
||||||
|
"D-1000111 default project",
|
||||||
|
HsBookingItemType.MANAGED_SERVER,
|
||||||
|
"temp ManagedServer"))
|
||||||
|
.type(MANAGED_SERVER)
|
||||||
|
.identifier("vm2001")
|
||||||
|
.caption("some test-asset")
|
||||||
|
.config(Map.ofEntries(
|
||||||
|
Map.<String, Object>entry("monit_max_ssd_usage", 80),
|
||||||
|
Map.<String, Object>entry("monit_max_hdd_usage", 90),
|
||||||
|
Map.<String, Object>entry("monit_max_cpu_usage", 90),
|
||||||
|
Map.<String, Object>entry("monit_max_ram_usage", 70)
|
||||||
|
))
|
||||||
|
.build());
|
||||||
final var alarmContactUuid = givenContact().getUuid();
|
final var alarmContactUuid = givenContact().getUuid();
|
||||||
|
|
||||||
RestAssured // @formatter:off
|
RestAssured // @formatter:off
|
||||||
@ -459,9 +476,10 @@ class HsHostingAssetControllerAcceptanceTest extends ContextBasedTestWithCleanup
|
|||||||
"identifier": "vm2001",
|
"identifier": "vm2001",
|
||||||
"caption": "some test-asset",
|
"caption": "some test-asset",
|
||||||
"alarmContact": {
|
"alarmContact": {
|
||||||
"uuid": "%s",
|
|
||||||
"caption": "second contact",
|
"caption": "second contact",
|
||||||
"emailAddresses": { "main": "contact-admin@secondcontact.example.com" }
|
"emailAddresses": {
|
||||||
|
"main": "contact-admin@secondcontact.example.com"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"config": {
|
"config": {
|
||||||
"monit_max_cpu_usage": 90,
|
"monit_max_cpu_usage": 90,
|
||||||
@ -470,27 +488,101 @@ class HsHostingAssetControllerAcceptanceTest extends ContextBasedTestWithCleanup
|
|||||||
"monit_min_free_ssd": 5
|
"monit_min_free_ssd": 5
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
""".formatted(alarmContactUuid)));
|
"""));
|
||||||
// @formatter:on
|
// @formatter:on
|
||||||
|
|
||||||
// finally, the asset is actually updated
|
// finally, the asset is actually updated
|
||||||
|
em.clear();
|
||||||
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.getAlarmContact().toString()).isEqualTo(
|
assertThat(asset.getAlarmContact()).isNotNull()
|
||||||
"contact(caption='second contact', emailAddresses='{ main: contact-admin@secondcontact.example.com }')");
|
.extracting(c -> c.getEmailAddresses().get("main"))
|
||||||
assertThat(asset.getConfig().toString()).isEqualTo(
|
.isEqualTo("contact-admin@secondcontact.example.com");
|
||||||
"{ monit_max_cpu_usage: 90, monit_max_ram_usage: 70, monit_max_ssd_usage: 85, monit_min_free_ssd: 5 }");
|
assertThat(asset.getConfig().toString())
|
||||||
|
.isEqualToIgnoringWhitespace("""
|
||||||
|
{
|
||||||
|
"monit_max_cpu_usage": 90,
|
||||||
|
"monit_max_ram_usage": 70,
|
||||||
|
"monit_max_ssd_usage": 85,
|
||||||
|
"monit_min_free_ssd": 5
|
||||||
|
}
|
||||||
|
""");
|
||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
private HsOfficeContactEntity givenContact() {
|
@Test
|
||||||
return jpaAttempt.transacted(() -> {
|
void assetAdmin_canPatchAllUpdatablePropertiesOfAsset() {
|
||||||
context.define("superuser-alex@hostsharing.net");
|
|
||||||
return contactRepo.findContactByOptionalCaptionLike("second").stream().findFirst().orElseThrow();
|
final var givenAsset = givenSomeTemporaryHostingAsset(() ->
|
||||||
}).returnedValue();
|
HsHostingAssetEntity.builder()
|
||||||
|
.uuid(UUID.randomUUID())
|
||||||
|
.type(UNIX_USER)
|
||||||
|
.parentAsset(givenHostingAsset(MANAGED_WEBSPACE, "fir01"))
|
||||||
|
.identifier("fir01-temp")
|
||||||
|
.caption("some test-unix-user")
|
||||||
|
.build());
|
||||||
|
|
||||||
|
RestAssured // @formatter:off
|
||||||
|
.given()
|
||||||
|
.header("current-user", "superuser-alex@hostsharing.net")
|
||||||
|
//.header("assumed-roles", "hs_hosting_asset#vm2001:ADMIN")
|
||||||
|
.contentType(ContentType.JSON)
|
||||||
|
.body("""
|
||||||
|
{
|
||||||
|
"caption": "some patched test-unix-user",
|
||||||
|
"config": {
|
||||||
|
"shell": "/bin/bash",
|
||||||
|
"totpKey": "0x1234567890abcdef0123456789abcdef",
|
||||||
|
"password": "Ein Passwort mit 4 Zeichengruppen!"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
""")
|
||||||
|
.port(port)
|
||||||
|
.when()
|
||||||
|
.patch("http://localhost/api/hs/hosting/assets/" + givenAsset.getUuid())
|
||||||
|
.then().log().all().assertThat()
|
||||||
|
.statusCode(200)
|
||||||
|
.contentType(ContentType.JSON)
|
||||||
|
.body("", lenientlyEquals("""
|
||||||
|
{
|
||||||
|
"type": "UNIX_USER",
|
||||||
|
"identifier": "fir01-temp",
|
||||||
|
"caption": "some patched test-unix-user",
|
||||||
|
"config": {
|
||||||
|
"homedir": "/home/pacs/fir01/users/temp",
|
||||||
|
"shell": "/bin/bash"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""))
|
||||||
|
// the config separately but not-leniently to make sure that no write-only-properties are listed
|
||||||
|
.body("config", strictlyEquals("""
|
||||||
|
{
|
||||||
|
"homedir": "/home/pacs/fir01/users/temp",
|
||||||
|
"shell": "/bin/bash"
|
||||||
|
}
|
||||||
|
"""))
|
||||||
|
;
|
||||||
|
// @formatter:on
|
||||||
|
|
||||||
|
// finally, the asset is actually updated
|
||||||
|
assertThat(jpaAttempt.transacted(() -> {
|
||||||
|
context.define("superuser-alex@hostsharing.net");
|
||||||
|
return assetRepo.findByUuid(givenAsset.getUuid());
|
||||||
|
}).returnedValue()).isPresent().get()
|
||||||
|
.matches(asset -> {
|
||||||
|
assertThat(asset.getCaption()).isEqualTo("some patched test-unix-user");
|
||||||
|
assertThat(asset.getConfig().toString()).isEqualTo("""
|
||||||
|
{
|
||||||
|
"password": "Ein Passwort mit 4 Zeichengruppen!",
|
||||||
|
"shell": "/bin/bash",
|
||||||
|
"totpKey": "0x1234567890abcdef0123456789abcdef"
|
||||||
|
}
|
||||||
|
""");
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Nested
|
@Nested
|
||||||
@ -500,9 +592,23 @@ 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 = givenSomeTemporaryHostingAsset("1002", MANAGED_SERVER,
|
final var givenAsset = givenSomeTemporaryHostingAsset(() ->
|
||||||
config("monit_max_ssd_usage", 80), config("monit_max_hdd_usage", 90), config("monit_max_cpu_usage", 90), config("monit_max_ram_usage", 70));
|
HsHostingAssetEntity.builder()
|
||||||
|
.uuid(UUID.randomUUID())
|
||||||
|
.bookingItem(givenSomeNewBookingItem(
|
||||||
|
"D-1000111 default project",
|
||||||
|
HsBookingItemType.MANAGED_SERVER,
|
||||||
|
"temp ManagedServer"))
|
||||||
|
.type(MANAGED_SERVER)
|
||||||
|
.identifier("vm1002")
|
||||||
|
.caption("some test-asset")
|
||||||
|
.config(Map.ofEntries(
|
||||||
|
Map.<String, Object>entry("monit_max_ssd_usage", 80),
|
||||||
|
Map.<String, Object>entry("monit_max_hdd_usage", 90),
|
||||||
|
Map.<String, Object>entry("monit_max_cpu_usage", 90),
|
||||||
|
Map.<String, Object>entry("monit_max_ram_usage", 70)
|
||||||
|
))
|
||||||
|
.build());
|
||||||
RestAssured // @formatter:off
|
RestAssured // @formatter:off
|
||||||
.given()
|
.given()
|
||||||
.header("current-user", "superuser-alex@hostsharing.net")
|
.header("current-user", "superuser-alex@hostsharing.net")
|
||||||
@ -519,9 +625,23 @@ 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 = givenSomeTemporaryHostingAsset("1003", MANAGED_SERVER,
|
final var givenAsset = givenSomeTemporaryHostingAsset(() ->
|
||||||
config("monit_max_ssd_usage", 80), config("monit_max_hdd_usage", 90), config("monit_max_cpu_usage", 90), config("monit_max_ram_usage", 70));
|
HsHostingAssetEntity.builder()
|
||||||
|
.uuid(UUID.randomUUID())
|
||||||
|
.bookingItem(givenSomeNewBookingItem(
|
||||||
|
"D-1000111 default project",
|
||||||
|
HsBookingItemType.MANAGED_SERVER,
|
||||||
|
"temp ManagedServer"))
|
||||||
|
.type(MANAGED_SERVER)
|
||||||
|
.identifier("vm1003")
|
||||||
|
.caption("some test-asset")
|
||||||
|
.config(Map.ofEntries(
|
||||||
|
Map.<String, Object>entry("monit_max_ssd_usage", 80),
|
||||||
|
Map.<String, Object>entry("monit_max_hdd_usage", 90),
|
||||||
|
Map.<String, Object>entry("monit_max_cpu_usage", 90),
|
||||||
|
Map.<String, Object>entry("monit_max_ram_usage", 70)
|
||||||
|
))
|
||||||
|
.build());
|
||||||
RestAssured // @formatter:off
|
RestAssured // @formatter:off
|
||||||
.given()
|
.given()
|
||||||
.header("current-user", "selfregistered-user-drew@hostsharing.org")
|
.header("current-user", "selfregistered-user-drew@hostsharing.org")
|
||||||
@ -538,7 +658,7 @@ class HsHostingAssetControllerAcceptanceTest extends ContextBasedTestWithCleanup
|
|||||||
|
|
||||||
HsHostingAssetEntity givenHostingAsset(final HsHostingAssetType type, final String identifier) {
|
HsHostingAssetEntity givenHostingAsset(final HsHostingAssetType type, final String identifier) {
|
||||||
return assetRepo.findByIdentifier(identifier).stream()
|
return assetRepo.findByIdentifier(identifier).stream()
|
||||||
.filter(ha -> ha.getType()==type)
|
.filter(ha -> ha.getType() == type)
|
||||||
.findAny().orElseThrow();
|
.findAny().orElseThrow();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -559,12 +679,18 @@ class HsHostingAssetControllerAcceptanceTest extends ContextBasedTestWithCleanup
|
|||||||
}).assertSuccessful().returnedValue();
|
}).assertSuccessful().returnedValue();
|
||||||
}
|
}
|
||||||
|
|
||||||
HsBookingItemEntity givenSomeNewBookingItem(final String projectCaption, final HsBookingItemType bookingItemType, final String bookingItemCaption) {
|
HsBookingItemEntity givenSomeNewBookingItem(
|
||||||
|
final String projectCaption,
|
||||||
|
final HsBookingItemType bookingItemType,
|
||||||
|
final String bookingItemCaption) {
|
||||||
return jpaAttempt.transacted(() -> {
|
return jpaAttempt.transacted(() -> {
|
||||||
context.define("superuser-alex@hostsharing.net");
|
context.define("superuser-alex@hostsharing.net");
|
||||||
final var project = projectRepo.findByCaption(projectCaption).getFirst();
|
final var project = projectRepo.findByCaption(projectCaption).getFirst();
|
||||||
final var resources = switch (bookingItemType) {
|
final var resources = switch (bookingItemType) {
|
||||||
case MANAGED_SERVER -> Map.<String, Object>ofEntries(entry("CPUs", 1), entry("RAM", 20), entry("SSD", 25), entry("Traffic", 250));
|
case MANAGED_SERVER -> Map.<String, Object>ofEntries(entry("CPUs", 1),
|
||||||
|
entry("RAM", 20),
|
||||||
|
entry("SSD", 25),
|
||||||
|
entry("Traffic", 250));
|
||||||
default -> new HashMap<String, Object>();
|
default -> new HashMap<String, Object>();
|
||||||
};
|
};
|
||||||
final var newBookingItem = HsBookingItemEntity.builder()
|
final var newBookingItem = HsBookingItemEntity.builder()
|
||||||
@ -584,33 +710,18 @@ class HsHostingAssetControllerAcceptanceTest extends ContextBasedTestWithCleanup
|
|||||||
return givenAsset;
|
return givenAsset;
|
||||||
}
|
}
|
||||||
|
|
||||||
@SafeVarargs
|
private HsHostingAssetEntity givenSomeTemporaryHostingAsset(final Supplier<HsHostingAssetEntity> newAsset) {
|
||||||
private HsHostingAssetEntity givenSomeTemporaryHostingAsset(final String identifierSuffix,
|
|
||||||
final HsHostingAssetType hostingAssetType,
|
|
||||||
final Map.Entry<String, Object>... config) {
|
|
||||||
return jpaAttempt.transacted(() -> {
|
return jpaAttempt.transacted(() -> {
|
||||||
context.define("superuser-alex@hostsharing.net");
|
context.define("superuser-alex@hostsharing.net");
|
||||||
final var bookingItemType = switch (hostingAssetType) {
|
return toCleanup(assetRepo.save(newAsset.get()));
|
||||||
case CLOUD_SERVER -> HsBookingItemType.CLOUD_SERVER;
|
|
||||||
case MANAGED_SERVER -> HsBookingItemType.MANAGED_SERVER;
|
|
||||||
case MANAGED_WEBSPACE -> HsBookingItemType.MANAGED_WEBSPACE;
|
|
||||||
default -> null;
|
|
||||||
};
|
|
||||||
final var newBookingItem = givenSomeNewBookingItem("D-1000111 default project", bookingItemType, "temp ManagedServer");
|
|
||||||
final var newAsset = HsHostingAssetEntity.builder()
|
|
||||||
.uuid(UUID.randomUUID())
|
|
||||||
.bookingItem(newBookingItem)
|
|
||||||
.type(hostingAssetType)
|
|
||||||
.identifier("vm" + identifierSuffix)
|
|
||||||
.caption("some test-asset")
|
|
||||||
.config(Map.ofEntries(config))
|
|
||||||
.build();
|
|
||||||
|
|
||||||
return assetRepo.save(newAsset);
|
|
||||||
}).assertSuccessful().returnedValue();
|
}).assertSuccessful().returnedValue();
|
||||||
}
|
}
|
||||||
|
|
||||||
private Map.Entry<String, Object> config(final String key, final Object value) {
|
private HsOfficeContactEntity givenContact() {
|
||||||
return entry(key, value);
|
return jpaAttempt.transacted(() -> {
|
||||||
|
context.define("superuser-alex@hostsharing.net");
|
||||||
|
return contactRepo.findContactByOptionalCaptionLike("second").stream().findFirst().orElseThrow();
|
||||||
|
}).returnedValue();
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -57,14 +57,14 @@ class HsHostingAssetEntityUnitTest {
|
|||||||
@Test
|
@Test
|
||||||
void toStringContainsAllPropertiesAndResourcesSortedByKey() {
|
void toStringContainsAllPropertiesAndResourcesSortedByKey() {
|
||||||
|
|
||||||
assertThat(givenWebspace.toString()).isEqualTo(
|
assertThat(givenWebspace.toString()).isEqualToIgnoringWhitespace(
|
||||||
"HsHostingAssetEntity(MANAGED_WEBSPACE, xyz00, some managed webspace, MANAGED_SERVER:vm1234, D-1234500:test project:test cloud server booking item, { CPUs: 2, HDD-storage: 2048, SSD-storage: 512 })");
|
"HsHostingAssetEntity(MANAGED_WEBSPACE, xyz00, some managed webspace, MANAGED_SERVER:vm1234, D-1234500:test project:test cloud server booking item, { \"CPUs\": 2, \"HDD-storage\": 2048, \"SSD-storage\": 512 })");
|
||||||
|
|
||||||
assertThat(givenUnixUser.toString()).isEqualTo(
|
assertThat(givenUnixUser.toString()).isEqualToIgnoringWhitespace(
|
||||||
"HsHostingAssetEntity(UNIX_USER, xyz00-web, some unix-user, MANAGED_WEBSPACE:xyz00, { HDD-hard-quota: 512, HDD-soft-quota: 256, SSD-hard-quota: 256, SSD-soft-quota: 128 })");
|
"HsHostingAssetEntity(UNIX_USER, xyz00-web, some unix-user, MANAGED_WEBSPACE:xyz00, { \"HDD-hard-quota\": 512, \"HDD-soft-quota\": 256, \"SSD-hard-quota\": 256, \"SSD-soft-quota\": 128 })");
|
||||||
|
|
||||||
assertThat(givenDomainHttpSetup.toString()).isEqualTo(
|
assertThat(givenDomainHttpSetup.toString()).isEqualToIgnoringWhitespace(
|
||||||
"HsHostingAssetEntity(DOMAIN_HTTP_SETUP, example.org, some domain setup, MANAGED_WEBSPACE:xyz00, UNIX_USER:xyz00-web, { option-htdocsfallback: true, use-fcgiphpbin: /usr/lib/cgi-bin/php, validsubdomainnames: * })");
|
"HsHostingAssetEntity(DOMAIN_HTTP_SETUP, example.org, some domain setup, MANAGED_WEBSPACE:xyz00, UNIX_USER:xyz00-web, { \"option-htdocsfallback\": true, \"use-fcgiphpbin\": \"/usr/lib/cgi-bin/php\", \"validsubdomainnames\": \"*\" })");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
@ -59,9 +59,7 @@ class HsHostingAssetPropsControllerAcceptanceTest {
|
|||||||
"unit": "%",
|
"unit": "%",
|
||||||
"min": 10,
|
"min": 10,
|
||||||
"max": 100,
|
"max": 100,
|
||||||
"required": false,
|
"defaultValue": 92
|
||||||
"defaultValue": 92,
|
|
||||||
"isTotalsValidator": false
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "integer",
|
"type": "integer",
|
||||||
@ -69,9 +67,7 @@ class HsHostingAssetPropsControllerAcceptanceTest {
|
|||||||
"unit": "%",
|
"unit": "%",
|
||||||
"min": 10,
|
"min": 10,
|
||||||
"max": 100,
|
"max": 100,
|
||||||
"required": false,
|
"defaultValue": 92
|
||||||
"defaultValue": 92,
|
|
||||||
"isTotalsValidator": false
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "integer",
|
"type": "integer",
|
||||||
@ -79,18 +75,14 @@ class HsHostingAssetPropsControllerAcceptanceTest {
|
|||||||
"unit": "%",
|
"unit": "%",
|
||||||
"min": 10,
|
"min": 10,
|
||||||
"max": 100,
|
"max": 100,
|
||||||
"required": false,
|
"defaultValue": 98
|
||||||
"defaultValue": 98,
|
|
||||||
"isTotalsValidator": false
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "integer",
|
"type": "integer",
|
||||||
"propertyName": "monit_min_free_ssd",
|
"propertyName": "monit_min_free_ssd",
|
||||||
"min": 1,
|
"min": 1,
|
||||||
"max": 1000,
|
"max": 1000,
|
||||||
"required": false,
|
"defaultValue": 5
|
||||||
"defaultValue": 5,
|
|
||||||
"isTotalsValidator": false
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "integer",
|
"type": "integer",
|
||||||
@ -98,32 +90,24 @@ class HsHostingAssetPropsControllerAcceptanceTest {
|
|||||||
"unit": "%",
|
"unit": "%",
|
||||||
"min": 10,
|
"min": 10,
|
||||||
"max": 100,
|
"max": 100,
|
||||||
"required": false,
|
"defaultValue": 95
|
||||||
"defaultValue": 95,
|
|
||||||
"isTotalsValidator": false
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "integer",
|
"type": "integer",
|
||||||
"propertyName": "monit_min_free_hdd",
|
"propertyName": "monit_min_free_hdd",
|
||||||
"min": 1,
|
"min": 1,
|
||||||
"max": 4000,
|
"max": 4000,
|
||||||
"required": false,
|
"defaultValue": 10
|
||||||
"defaultValue": 10,
|
|
||||||
"isTotalsValidator": false
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "boolean",
|
"type": "boolean",
|
||||||
"propertyName": "software-pgsql",
|
"propertyName": "software-pgsql",
|
||||||
"required": false,
|
"defaultValue": true
|
||||||
"defaultValue": true,
|
|
||||||
"isTotalsValidator": false
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "boolean",
|
"type": "boolean",
|
||||||
"propertyName": "software-mariadb",
|
"propertyName": "software-mariadb",
|
||||||
"required": false,
|
"defaultValue": true
|
||||||
"defaultValue": true,
|
|
||||||
"isTotalsValidator": false
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "enumeration",
|
"type": "enumeration",
|
||||||
@ -139,114 +123,70 @@ class HsHostingAssetPropsControllerAcceptanceTest {
|
|||||||
"8.1",
|
"8.1",
|
||||||
"8.2"
|
"8.2"
|
||||||
],
|
],
|
||||||
"required": false,
|
"defaultValue": "8.2"
|
||||||
"defaultValue": "8.2",
|
|
||||||
"isTotalsValidator": false
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "boolean",
|
"type": "boolean",
|
||||||
"propertyName": "software-php-5.6",
|
"propertyName": "software-php-5.6"
|
||||||
"required": false,
|
|
||||||
"defaultValue": false,
|
|
||||||
"isTotalsValidator": false
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "boolean",
|
"type": "boolean",
|
||||||
"propertyName": "software-php-7.0",
|
"propertyName": "software-php-7.0"
|
||||||
"required": false,
|
|
||||||
"defaultValue": false,
|
|
||||||
"isTotalsValidator": false
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "boolean",
|
"type": "boolean",
|
||||||
"propertyName": "software-php-7.1",
|
"propertyName": "software-php-7.1"
|
||||||
"required": false,
|
|
||||||
"defaultValue": false,
|
|
||||||
"isTotalsValidator": false
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "boolean",
|
"type": "boolean",
|
||||||
"propertyName": "software-php-7.2",
|
"propertyName": "software-php-7.2"
|
||||||
"required": false,
|
|
||||||
"defaultValue": false,
|
|
||||||
"isTotalsValidator": false
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "boolean",
|
"type": "boolean",
|
||||||
"propertyName": "software-php-7.3",
|
"propertyName": "software-php-7.3"
|
||||||
"required": false,
|
|
||||||
"defaultValue": false,
|
|
||||||
"isTotalsValidator": false
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "boolean",
|
"type": "boolean",
|
||||||
"propertyName": "software-php-7.4",
|
"propertyName": "software-php-7.4",
|
||||||
"required": false,
|
"defaultValue": true
|
||||||
"defaultValue": true,
|
|
||||||
"isTotalsValidator": false
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "boolean",
|
"type": "boolean",
|
||||||
"propertyName": "software-php-8.0",
|
"propertyName": "software-php-8.0"
|
||||||
"required": false,
|
|
||||||
"defaultValue": false,
|
|
||||||
"isTotalsValidator": false
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "boolean",
|
"type": "boolean",
|
||||||
"propertyName": "software-php-8.1",
|
"propertyName": "software-php-8.1"
|
||||||
"required": false,
|
|
||||||
"defaultValue": false,
|
|
||||||
"isTotalsValidator": false
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "boolean",
|
"type": "boolean",
|
||||||
"propertyName": "software-php-8.2",
|
"propertyName": "software-php-8.2",
|
||||||
"required": false,
|
"defaultValue": true
|
||||||
"defaultValue": true,
|
|
||||||
"isTotalsValidator": false
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "boolean",
|
"type": "boolean",
|
||||||
"propertyName": "software-postfix-tls-1.0",
|
"propertyName": "software-postfix-tls-1.0"
|
||||||
"required": false,
|
|
||||||
"defaultValue": false,
|
|
||||||
"isTotalsValidator": false
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "boolean",
|
"type": "boolean",
|
||||||
"propertyName": "software-dovecot-tls-1.0",
|
"propertyName": "software-dovecot-tls-1.0"
|
||||||
"required": false,
|
|
||||||
"defaultValue": false,
|
|
||||||
"isTotalsValidator": false
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "boolean",
|
"type": "boolean",
|
||||||
"propertyName": "software-clamav",
|
"propertyName": "software-clamav",
|
||||||
"required": false,
|
"defaultValue": true
|
||||||
"defaultValue": true,
|
|
||||||
"isTotalsValidator": false
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "boolean",
|
"type": "boolean",
|
||||||
"propertyName": "software-collabora",
|
"propertyName": "software-collabora"
|
||||||
"required": false,
|
|
||||||
"defaultValue": false,
|
|
||||||
"isTotalsValidator": false
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "boolean",
|
"type": "boolean",
|
||||||
"propertyName": "software-libreoffice",
|
"propertyName": "software-libreoffice"
|
||||||
"required": false,
|
|
||||||
"defaultValue": false,
|
|
||||||
"isTotalsValidator": false
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "boolean",
|
"type": "boolean",
|
||||||
"propertyName": "software-imagemagick-ghostscript",
|
"propertyName": "software-imagemagick-ghostscript"
|
||||||
"required": false,
|
|
||||||
"defaultValue": false,
|
|
||||||
"isTotalsValidator": false
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
"""));
|
"""));
|
||||||
|
@ -195,7 +195,7 @@ class HsHostingAssetRepositoryIntegrationTest extends ContextBasedTestWithCleanu
|
|||||||
exactlyTheseAssetsAreReturned(
|
exactlyTheseAssetsAreReturned(
|
||||||
result,
|
result,
|
||||||
"HsHostingAssetEntity(MANAGED_WEBSPACE, fir01, some Webspace, MANAGED_SERVER:vm1011, D-1000111:D-1000111 default project:separate ManagedWebspace)",
|
"HsHostingAssetEntity(MANAGED_WEBSPACE, fir01, some Webspace, MANAGED_SERVER:vm1011, D-1000111:D-1000111 default project:separate ManagedWebspace)",
|
||||||
"HsHostingAssetEntity(MANAGED_SERVER, vm1011, some ManagedServer, D-1000111:D-1000111 default project:separate ManagedServer, { monit_max_cpu_usage: 90, monit_max_ram_usage: 80, monit_max_ssd_usage: 70 })");
|
"HsHostingAssetEntity(MANAGED_SERVER, vm1011, some ManagedServer, D-1000111:D-1000111 default project:separate ManagedServer, { monit_max_cpu_usage: 90, monit_max_ram_usage: 80, monit_max_ssd_usage: 70 } )");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@ -407,6 +407,8 @@ class HsHostingAssetRepositoryIntegrationTest extends ContextBasedTestWithCleanu
|
|||||||
final String... serverNames) {
|
final String... serverNames) {
|
||||||
assertThat(actualResult)
|
assertThat(actualResult)
|
||||||
.extracting(HsHostingAssetEntity::toString)
|
.extracting(HsHostingAssetEntity::toString)
|
||||||
|
.extracting(input -> input.replaceAll("\\s+", " "))
|
||||||
|
.extracting(input -> input.replaceAll("\"", ""))
|
||||||
.containsExactlyInAnyOrder(serverNames);
|
.containsExactlyInAnyOrder(serverNames);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -37,8 +37,8 @@ class HsManagedServerHostingAssetValidatorUnitTest {
|
|||||||
assertThat(result).containsExactlyInAnyOrder(
|
assertThat(result).containsExactlyInAnyOrder(
|
||||||
"'MANAGED_SERVER:vm1234.parentAsset' must be null but is set to D-???????-?:null",
|
"'MANAGED_SERVER:vm1234.parentAsset' must be null but is set to D-???????-?:null",
|
||||||
"'MANAGED_SERVER:vm1234.assignedToAsset' must be null but is set to D-???????-?:null",
|
"'MANAGED_SERVER:vm1234.assignedToAsset' must be null but is set to D-???????-?:null",
|
||||||
"'MANAGED_SERVER:vm1234.config.monit_max_cpu_usage' is expected to be >= 10 but is 2",
|
"'MANAGED_SERVER:vm1234.config.monit_max_cpu_usage' is expected to be at least 10 but is 2",
|
||||||
"'MANAGED_SERVER:vm1234.config.monit_max_ram_usage' is expected to be <= 100 but is 101",
|
"'MANAGED_SERVER:vm1234.config.monit_max_ram_usage' is expected to be at most 100 but is 101",
|
||||||
"'MANAGED_SERVER:vm1234.config.monit_max_hdd_usage' is expected to be of type class java.lang.Integer, but is of type 'String'");
|
"'MANAGED_SERVER:vm1234.config.monit_max_hdd_usage' is expected to be of type class java.lang.Integer, but is of type 'String'");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,14 +1,95 @@
|
|||||||
package net.hostsharing.hsadminng.hs.hosting.asset.validators;
|
package net.hostsharing.hsadminng.hs.hosting.asset.validators;
|
||||||
|
|
||||||
import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetEntity;
|
import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetEntity;
|
||||||
|
import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import static net.hostsharing.hsadminng.hs.booking.item.TestHsBookingItem.TEST_MANAGED_SERVER_BOOKING_ITEM;
|
||||||
|
import static net.hostsharing.hsadminng.hs.booking.item.TestHsBookingItem.TEST_MANAGED_WEBSPACE_BOOKING_ITEM;
|
||||||
import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.MANAGED_WEBSPACE;
|
import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.MANAGED_WEBSPACE;
|
||||||
import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.UNIX_USER;
|
import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.UNIX_USER;
|
||||||
|
import static net.hostsharing.hsadminng.mapper.PatchMap.entry;
|
||||||
import static org.assertj.core.api.Assertions.assertThat;
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
|
||||||
class HsUnixUserHostingAssetValidatorUnitTest {
|
class HsUnixUserHostingAssetValidatorUnitTest {
|
||||||
|
|
||||||
|
private final HsHostingAssetEntity TEST_MANAGED_SERVER_HOSTING_ASSET = HsHostingAssetEntity.builder()
|
||||||
|
.type(HsHostingAssetType.MANAGED_SERVER)
|
||||||
|
.identifier("vm1234")
|
||||||
|
.caption("some managed server")
|
||||||
|
.bookingItem(TEST_MANAGED_SERVER_BOOKING_ITEM)
|
||||||
|
.build();
|
||||||
|
private HsHostingAssetEntity TEST_MANAGED_WEBSPACE_HOSTING_ASSET = HsHostingAssetEntity.builder()
|
||||||
|
.type(MANAGED_WEBSPACE)
|
||||||
|
.bookingItem(TEST_MANAGED_WEBSPACE_BOOKING_ITEM)
|
||||||
|
.parentAsset(TEST_MANAGED_SERVER_HOSTING_ASSET)
|
||||||
|
.identifier("abc00")
|
||||||
|
.build();;
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void validatesValidUnixUser() {
|
||||||
|
// given
|
||||||
|
final var unixUserHostingAsset = HsHostingAssetEntity.builder()
|
||||||
|
.type(UNIX_USER)
|
||||||
|
.parentAsset(TEST_MANAGED_WEBSPACE_HOSTING_ASSET)
|
||||||
|
.identifier("abc00-temp")
|
||||||
|
.caption("some valid test UnixUser")
|
||||||
|
.config(Map.ofEntries(
|
||||||
|
entry("SSD hard quota", 50),
|
||||||
|
entry("SSD soft quota", 40),
|
||||||
|
entry("totpKey", "0x123456789abcdef01234"),
|
||||||
|
entry("password", "Hallo Computer, lass mich rein!")
|
||||||
|
))
|
||||||
|
.build();
|
||||||
|
final var validator = HsHostingAssetEntityValidatorRegistry.forType(unixUserHostingAsset.getType());
|
||||||
|
|
||||||
|
// when
|
||||||
|
final var result = validator.validate(unixUserHostingAsset);
|
||||||
|
|
||||||
|
// then
|
||||||
|
assertThat(result).isEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void validatesUnixUserProperties() {
|
||||||
|
// given
|
||||||
|
final var unixUserHostingAsset = HsHostingAssetEntity.builder()
|
||||||
|
.type(UNIX_USER)
|
||||||
|
.parentAsset(TEST_MANAGED_WEBSPACE_HOSTING_ASSET)
|
||||||
|
.identifier("abc00-temp")
|
||||||
|
.caption("some test UnixUser with invalid properties")
|
||||||
|
.config(Map.ofEntries(
|
||||||
|
entry("SSD hard quota", 100),
|
||||||
|
entry("SSD soft quota", 200),
|
||||||
|
entry("HDD hard quota", 100),
|
||||||
|
entry("HDD soft quota", 200),
|
||||||
|
entry("shell", "/is/invalid"),
|
||||||
|
entry("homedir", "/is/read-only"),
|
||||||
|
entry("totpKey", "should be a hex number"),
|
||||||
|
entry("password", "short")
|
||||||
|
))
|
||||||
|
.build();
|
||||||
|
final var validator = HsHostingAssetEntityValidatorRegistry.forType(unixUserHostingAsset.getType());
|
||||||
|
|
||||||
|
// when
|
||||||
|
final var result = validator.validate(unixUserHostingAsset);
|
||||||
|
|
||||||
|
// then
|
||||||
|
assertThat(result).containsExactlyInAnyOrder(
|
||||||
|
"'UNIX_USER:abc00-temp.config.SSD hard quota' is expected to be at most 50 but is 100",
|
||||||
|
"'UNIX_USER:abc00-temp.config.SSD soft quota' is expected to be at most 100 but is 200",
|
||||||
|
"'UNIX_USER:abc00-temp.config.HDD hard quota' is expected to be at most 0 but is 100",
|
||||||
|
"'UNIX_USER:abc00-temp.config.HDD soft quota' is expected to be at most 100 but is 200",
|
||||||
|
"'UNIX_USER:abc00-temp.config.shell' is expected to be one of [/bin/false, /bin/bash, /bin/csh, /bin/dash, /usr/bin/tcsh, /usr/bin/zsh, /usr/bin/passwd] but is '/is/invalid'",
|
||||||
hsh-michaelhoennig marked this conversation as resolved
|
|||||||
|
"'UNIX_USER:abc00-temp.config.homedir' is readonly but given as '/is/read-only'",
|
||||||
|
"'UNIX_USER:abc00-temp.config.totpKey' is expected to be match ^0x([0-9A-Fa-f]{2})+$ but provided value does not match",
|
||||||
|
"'UNIX_USER:abc00-temp.config.password' length is expected to be at min 8 but length of provided value is 5",
|
||||||
|
"'UNIX_USER:abc00-temp.config.password' must contain at least one character of at least 3 of the following groups: upper case letters, lower case letters, digits, special characters"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void validatesInvalidIdentifier() {
|
void validatesInvalidIdentifier() {
|
||||||
// given
|
// given
|
||||||
@ -19,7 +100,6 @@ class HsUnixUserHostingAssetValidatorUnitTest {
|
|||||||
.build();
|
.build();
|
||||||
final var validator = HsHostingAssetEntityValidatorRegistry.forType(unixUserHostingAsset.getType());
|
final var validator = HsHostingAssetEntityValidatorRegistry.forType(unixUserHostingAsset.getType());
|
||||||
|
|
||||||
|
|
||||||
// when
|
// when
|
||||||
final var result = validator.validate(unixUserHostingAsset);
|
final var result = validator.validate(unixUserHostingAsset);
|
||||||
|
|
||||||
@ -27,4 +107,25 @@ class HsUnixUserHostingAssetValidatorUnitTest {
|
|||||||
assertThat(result).containsExactly(
|
assertThat(result).containsExactly(
|
||||||
"'identifier' expected to match '^abc00$|^abc00-[a-z0-9]+$', but is 'xyz99-temp'");
|
"'identifier' expected to match '^abc00$|^abc00-[a-z0-9]+$', but is 'xyz99-temp'");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void describesItsProperties() {
|
||||||
|
// given
|
||||||
|
final var validator = HsHostingAssetEntityValidatorRegistry.forType(UNIX_USER);
|
||||||
|
|
||||||
|
// when
|
||||||
|
final var props = validator.properties();
|
||||||
|
|
||||||
|
// then
|
||||||
|
assertThat(props).extracting(Object::toString).containsExactlyInAnyOrder(
|
||||||
|
"{type=integer, propertyName=SSD hard quota, unit=GB, maxFrom=SSD}",
|
||||||
|
"{type=integer, propertyName=SSD soft quota, unit=GB, maxFrom=SSD hard quota}",
|
||||||
|
"{type=integer, propertyName=HDD hard quota, unit=GB, maxFrom=HDD}",
|
||||||
|
"{type=integer, propertyName=HDD soft quota, unit=GB, maxFrom=HDD hard quota}",
|
||||||
|
"{type=enumeration, propertyName=shell, values=[/bin/false, /bin/bash, /bin/csh, /bin/dash, /usr/bin/tcsh, /usr/bin/zsh, /usr/bin/passwd], defaultValue=/bin/false}",
|
||||||
|
"{type=string, propertyName=homedir, readOnly=true, computed=true}",
|
||||||
|
"{type=string, propertyName=totpKey, matchesRegEx=^0x([0-9A-Fa-f]{2})+$, minLength=20, maxLength=256, writeOnly=true, undisclosed=true}",
|
||||||
|
"{type=password, propertyName=password, minLength=8, maxLength=40, writeOnly=true, undisclosed=true}"
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,92 @@
|
|||||||
|
package net.hostsharing.hsadminng.hs.validation;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.params.ParameterizedTest;
|
||||||
|
import org.junit.jupiter.params.provider.ValueSource;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import static net.hostsharing.hsadminng.hs.validation.PasswordProperty.passwordProperty;
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
|
||||||
|
class PasswordPropertyUnitTest {
|
||||||
|
|
||||||
|
private final ValidatableProperty<String> passwordProp = passwordProperty("password").minLength(8).maxLength(40).writeOnly();
|
||||||
|
private final List<String> violations = new ArrayList<>();
|
||||||
|
|
||||||
|
@ParameterizedTest
|
||||||
|
@ValueSource(strings = {
|
||||||
|
"lowerUpperAndDigit1",
|
||||||
|
"lowerUpperAndSpecial!",
|
||||||
|
"digit1LowerAndSpecial!",
|
||||||
|
"digit1special!lower",
|
||||||
|
"DIGIT1SPECIAL!UPPER" })
|
||||||
|
void shouldValidateValidPassword(final String password) {
|
||||||
|
// when
|
||||||
|
passwordProp.validate(violations, password, null);
|
||||||
|
|
||||||
|
// then
|
||||||
|
assertThat(violations).isEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
@ParameterizedTest
|
||||||
|
@ValueSource(strings = {
|
||||||
|
"noDigitNoSpecial",
|
||||||
|
"!!!!!!12345",
|
||||||
|
"nolower-nodigit",
|
||||||
|
"nolower1nospecial",
|
||||||
|
"NOLOWER-NODIGIT",
|
||||||
|
"NOLOWER1NOSPECIAL"
|
||||||
|
})
|
||||||
|
void shouldRecognizeMissingCharacterGroup(final String givenPassword) {
|
||||||
|
// when
|
||||||
|
passwordProp.validate(violations, givenPassword, null);
|
||||||
|
|
||||||
|
// then
|
||||||
|
assertThat(violations)
|
||||||
|
.contains("password' must contain at least one character of at least 3 of the following groups: upper case letters, lower case letters, digits, special characters")
|
||||||
|
.doesNotContain(givenPassword);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldRecognizeTooShortPassword() {
|
||||||
|
// given
|
||||||
|
final String givenPassword = "0123456";
|
||||||
|
|
||||||
|
// when
|
||||||
|
passwordProp.validate(violations, givenPassword, null);
|
||||||
|
|
||||||
|
// then
|
||||||
|
assertThat(violations)
|
||||||
|
.contains("password' length is expected to be at min 8 but length of provided value is 7")
|
||||||
|
.doesNotContain(givenPassword);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldRecognizeTooLongPassowrd() {
|
||||||
|
// given
|
||||||
|
final String givenPassword = "password' length is expected to be at max 40 but is 41";
|
||||||
|
|
||||||
|
// when
|
||||||
|
passwordProp.validate(violations, givenPassword, null);
|
||||||
|
|
||||||
|
// then
|
||||||
|
assertThat(violations).contains("password' length is expected to be at max 40 but length of provided value is 54")
|
||||||
|
.doesNotContain(givenPassword);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldRecognizeColonInPassword() {
|
||||||
|
// given
|
||||||
|
final String givenPassword = "lowerUpper:1234";
|
||||||
|
|
||||||
|
// when
|
||||||
|
passwordProp.validate(violations, givenPassword, null);
|
||||||
|
|
||||||
|
// then
|
||||||
|
assertThat(violations)
|
||||||
|
.contains("password' must not contain colon (':')")
|
||||||
|
.doesNotContain(givenPassword);
|
||||||
|
}
|
||||||
|
}
|
@ -9,13 +9,15 @@ import org.json.JSONException;
|
|||||||
import org.skyscreamer.jsonassert.JSONAssert;
|
import org.skyscreamer.jsonassert.JSONAssert;
|
||||||
import org.skyscreamer.jsonassert.JSONCompareMode;
|
import org.skyscreamer.jsonassert.JSONCompareMode;
|
||||||
|
|
||||||
|
import static com.fasterxml.jackson.databind.SerializationFeature.INDENT_OUTPUT;
|
||||||
|
|
||||||
public class JsonMatcher extends BaseMatcher<CharSequence> {
|
public class JsonMatcher extends BaseMatcher<CharSequence> {
|
||||||
|
|
||||||
private final String expected;
|
private final String expectedJson;
|
||||||
private JSONCompareMode compareMode;
|
private JSONCompareMode compareMode;
|
||||||
|
|
||||||
public JsonMatcher(final String expected, final JSONCompareMode compareMode) {
|
public JsonMatcher(final String expectedJson, final JSONCompareMode compareMode) {
|
||||||
this.expected = expected;
|
this.expectedJson = expectedJson;
|
||||||
this.compareMode = compareMode;
|
this.compareMode = compareMode;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -47,8 +49,8 @@ public class JsonMatcher extends BaseMatcher<CharSequence> {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
final var actualJson = new ObjectMapper().writeValueAsString(actual);
|
final var actualJson = new ObjectMapper().enable(INDENT_OUTPUT).writeValueAsString(actual);
|
||||||
JSONAssert.assertEquals(expected, actualJson, compareMode);
|
JSONAssert.assertEquals(expectedJson, actualJson, compareMode);
|
||||||
return true;
|
return true;
|
||||||
} catch (final JSONException | JsonProcessingException e) {
|
} catch (final JSONException | JsonProcessingException e) {
|
||||||
throw new AssertionError(e);
|
throw new AssertionError(e);
|
||||||
@ -59,5 +61,4 @@ public class JsonMatcher extends BaseMatcher<CharSequence> {
|
|||||||
public void describeTo(final Description description) {
|
public void describeTo(final Description description) {
|
||||||
description.appendText("leniently matches JSON");
|
description.appendText("leniently matches JSON");
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user
evtl. nur false, bash, csh, ...