From bdb85e9be1cf16044c4719daa9cf7bb50326d3b7 Mon Sep 17 00:00:00 2001 From: Michael Hoennig Date: Tue, 2 Jul 2024 12:37:03 +0200 Subject: [PATCH 1/6] add EMail-Alias hosting asset validation (WIP) --- .../HsEMailAliasHostingAssetValidator.java | 33 +++++ ...HsHostingAssetEntityValidatorRegistry.java | 1 + .../hs/validation/ArrayProperty.java | 60 +++++++++ .../hs/validation/PasswordProperty.java | 4 +- .../hs/validation/StringProperty.java | 14 +- .../hs/validation/ValidatableProperty.java | 1 + .../hostsharing/hsadminng/mapper/Array.java | 7 +- .../7018-hs-hosting-asset-test-data.sql | 1 + ...sHostingAssetControllerAcceptanceTest.java | 40 ++---- ...ingAssetPropsControllerAcceptanceTest.java | 3 +- ...ailAliasHostingAssetValidatorUnitTest.java | 125 ++++++++++++++++++ 11 files changed, 251 insertions(+), 38 deletions(-) create mode 100644 src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsEMailAliasHostingAssetValidator.java create mode 100644 src/main/java/net/hostsharing/hsadminng/hs/validation/ArrayProperty.java create mode 100644 src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsEMailAliasHostingAssetValidatorUnitTest.java diff --git a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsEMailAliasHostingAssetValidator.java b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsEMailAliasHostingAssetValidator.java new file mode 100644 index 00000000..c405270a --- /dev/null +++ b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsEMailAliasHostingAssetValidator.java @@ -0,0 +1,33 @@ +package net.hostsharing.hsadminng.hs.hosting.asset.validators; + +import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetEntity; +import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType; + +import java.util.regex.Pattern; + +import static net.hostsharing.hsadminng.hs.validation.ArrayProperty.arrayOf; +import static net.hostsharing.hsadminng.hs.validation.StringProperty.stringProperty; + +class HsEMailAliasHostingAssetValidator extends HsHostingAssetEntityValidator { + + private static final String UNIX_USER_REGEX = "^[a-z]{3}[0-9]{2}(-[a-z0-9]+)?$"; + private static final String EMAIL_ADDRESS_REGEX = "^[a-zA-Z0-9_.±]+@[a-zA-Z0-9-]+.[a-zA-Z0-9-.]+$"; + public static final int EMAIL_ADDRESS_MAX_LENGTH = 320; // according to RFC 5321 and RFC 5322 + + HsEMailAliasHostingAssetValidator() { + super( BookingItem.mustBeNull(), + ParentAsset.mustBeOfType(HsHostingAssetType.MANAGED_WEBSPACE), + AssignedToAsset.mustBeNull(), + AlarmContact.isOptional(), + + arrayOf( + stringProperty("target").maxLength(EMAIL_ADDRESS_MAX_LENGTH).matchesRegEx(UNIX_USER_REGEX, EMAIL_ADDRESS_REGEX) + ).required().minLength(1)); + } + + @Override + protected Pattern identifierPattern(final HsHostingAssetEntity assetEntity) { + final var webspaceIdentifier = assetEntity.getParentAsset().getIdentifier(); + return Pattern.compile("^"+webspaceIdentifier+"$|^"+webspaceIdentifier+"-[a-z0-9]+$"); + } +} diff --git a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsHostingAssetEntityValidatorRegistry.java b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsHostingAssetEntityValidatorRegistry.java index a6c30712..a30108e7 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsHostingAssetEntityValidatorRegistry.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsHostingAssetEntityValidatorRegistry.java @@ -19,6 +19,7 @@ public class HsHostingAssetEntityValidatorRegistry { register(MANAGED_SERVER, new HsManagedServerHostingAssetValidator()); register(MANAGED_WEBSPACE, new HsManagedWebspaceHostingAssetValidator()); register(UNIX_USER, new HsUnixUserHostingAssetValidator()); + register(EMAIL_ALIAS, new HsEMailAliasHostingAssetValidator()); } private static void register(final Enum type, final HsEntityValidator validator) { diff --git a/src/main/java/net/hostsharing/hsadminng/hs/validation/ArrayProperty.java b/src/main/java/net/hostsharing/hsadminng/hs/validation/ArrayProperty.java new file mode 100644 index 00000000..6d01b4ba --- /dev/null +++ b/src/main/java/net/hostsharing/hsadminng/hs/validation/ArrayProperty.java @@ -0,0 +1,60 @@ +package net.hostsharing.hsadminng.hs.validation; + +import lombok.Setter; + +import java.util.Arrays; +import java.util.List; + +import static java.util.Arrays.stream; +import static net.hostsharing.hsadminng.mapper.Array.insertAfterEntries; + +@Setter +public class ArrayProperty

, E> extends ValidatableProperty, E[]> { + + private static final String[] KEY_ORDER = + insertAfterEntries(ValidatableProperty.KEY_ORDER, "required", "minLength" ,"maxLength"); + private final ValidatableProperty elementProperty; + private Integer minLength; + private Integer maxLength; + + private ArrayProperty(final ValidatableProperty elementProperty) { + //noinspection unchecked + super((Class) elementProperty.type.arrayType(), elementProperty.propertyName, KEY_ORDER); + this.elementProperty = elementProperty; + } + + public static ArrayProperty arrayOf(final ValidatableProperty elementProperty) { + //noinspection unchecked + return (ArrayProperty) new ArrayProperty<>(elementProperty); + } + + public ValidatableProperty minLength(final int minLength) { + this.minLength = minLength; + return self(); + } + + public ValidatableProperty maxLength(final int maxLength) { + this.maxLength = maxLength; + return self(); + } + + @Override + protected void validate(final List result, final E[] 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); + } + stream(propValue).forEach(e -> elementProperty.validate(result, e, propProvider)); + } + + @Override + protected String simpleTypeName() { + return elementProperty.simpleTypeName() + "[]"; + } + + private String display(final E... propValue) { + return "[" + Arrays.toString(propValue) + "]"; + } +} diff --git a/src/main/java/net/hostsharing/hsadminng/hs/validation/PasswordProperty.java b/src/main/java/net/hostsharing/hsadminng/hs/validation/PasswordProperty.java index 37a8146f..ac37a89c 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/validation/PasswordProperty.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/validation/PasswordProperty.java @@ -8,12 +8,12 @@ import java.util.stream.Stream; import static java.util.Optional.ofNullable; import static net.hostsharing.hsadminng.hash.LinuxEtcShadowHashGenerator.hash; -import static net.hostsharing.hsadminng.mapper.Array.insertAfterEntry; +import static net.hostsharing.hsadminng.mapper.Array.insertAfterEntries; @Setter public class PasswordProperty extends StringProperty { - private static final String[] KEY_ORDER = insertAfterEntry(StringProperty.KEY_ORDER, "computed", "hashedUsing"); + private static final String[] KEY_ORDER = insertAfterEntries(StringProperty.KEY_ORDER, "computed", "hashedUsing"); private Algorithm hashedUsing; diff --git a/src/main/java/net/hostsharing/hsadminng/hs/validation/StringProperty.java b/src/main/java/net/hostsharing/hsadminng/hs/validation/StringProperty.java index a8e8b359..28e67f87 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/validation/StringProperty.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/validation/StringProperty.java @@ -3,9 +3,12 @@ package net.hostsharing.hsadminng.hs.validation; import lombok.Setter; import net.hostsharing.hsadminng.mapper.Array; +import java.util.Arrays; import java.util.List; +import java.util.regex.Matcher; import java.util.regex.Pattern; +import static java.util.Arrays.stream; @Setter public class StringProperty

> extends ValidatableProperty { @@ -15,7 +18,7 @@ public class StringProperty

> extends ValidatableProp Array.of("matchesRegEx", "minLength", "maxLength"), ValidatableProperty.KEY_ORDER_TAIL, Array.of("undisclosed")); - private Pattern matchesRegEx; + private Pattern[] matchesRegEx; private Integer minLength; private Integer maxLength; private boolean undisclosed; @@ -42,8 +45,8 @@ public class StringProperty

> extends ValidatableProp return self(); } - public P matchesRegEx(final String regExPattern) { - this.matchesRegEx = Pattern.compile(regExPattern); + public P matchesRegEx(final String... regExPattern) { + this.matchesRegEx = stream(regExPattern).map(Pattern::compile).toArray(Pattern[]::new); return self(); } @@ -65,8 +68,9 @@ public class StringProperty

> extends ValidatableProp 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 (matchesRegEx != null && + stream(matchesRegEx).map(p -> p.matcher(propValue)).map(Matcher::matches).findAny().isEmpty()) { + result.add(propertyName + "' is expected to match any of " + Arrays.toString(matchesRegEx) + " but " + display(propValue) + " does not match any"); } if (isReadOnly() && propValue != null) { result.add(propertyName + "' is readonly but given as " + display(propValue)); diff --git a/src/main/java/net/hostsharing/hsadminng/hs/validation/ValidatableProperty.java b/src/main/java/net/hostsharing/hsadminng/hs/validation/ValidatableProperty.java index 76fc451e..d2bf7056 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/validation/ValidatableProperty.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/validation/ValidatableProperty.java @@ -29,6 +29,7 @@ public abstract class ValidatableProperty

, T protected static final String[] KEY_ORDER_HEAD = Array.of("propertyName"); protected static final String[] KEY_ORDER_TAIL = Array.of("required", "defaultValue", "readOnly", "writeOnly", "computed", "isTotalsValidator", "thresholdPercentage"); + protected static final String[] KEY_ORDER = Array.join(KEY_ORDER_HEAD, KEY_ORDER_TAIL); final Class type; final String propertyName; diff --git a/src/main/java/net/hostsharing/hsadminng/mapper/Array.java b/src/main/java/net/hostsharing/hsadminng/mapper/Array.java index 57e76381..777bd0b3 100644 --- a/src/main/java/net/hostsharing/hsadminng/mapper/Array.java +++ b/src/main/java/net/hostsharing/hsadminng/mapper/Array.java @@ -51,13 +51,16 @@ public class Array { return of(); } - public static T[] insertAfterEntry(final T[] array, final T entryToFind, final T newEntry) { + @SafeVarargs + public static T[] insertAfterEntries(final T[] array, final T entryToFind, final T... newEntries) { final var arrayList = new ArrayList<>(asList(array)); final var index = arrayList.indexOf(entryToFind); if (index < 0) { throw new IllegalArgumentException("entry "+ entryToFind + " not found in " + Arrays.toString(array)); } - arrayList.add(index + 1, newEntry); + for (int n = 0; n < newEntries.length; ++n) { + arrayList.add(index +n + 1, newEntries[n]); + } @SuppressWarnings("unchecked") final var extendedArray = (T[]) java.lang.reflect.Array.newInstance(array.getClass().getComponentType(), array.length); diff --git a/src/main/resources/db/changelog/7-hs-hosting/701-hosting-asset/7018-hs-hosting-asset-test-data.sql b/src/main/resources/db/changelog/7-hs-hosting/701-hosting-asset/7018-hs-hosting-asset-test-data.sql index c82bd768..32f2804a 100644 --- a/src/main/resources/db/changelog/7-hs-hosting/701-hosting-asset/7018-hs-hosting-asset-test-data.sql +++ b/src/main/resources/db/changelog/7-hs-hosting/701-hosting-asset/7018-hs-hosting-asset-test-data.sql @@ -73,6 +73,7 @@ begin values (managedServerUuid, relatedManagedServerBookingItem.uuid, 'MANAGED_SERVER', null, null, 'vm10' || debitorNumberSuffix, 'some ManagedServer', '{ "monit_max_cpu_usage": 90, "monit_max_ram_usage": 80, "monit_max_ssd_usage": 70 }'::jsonb), (uuid_generate_v4(), relatedCloudServerBookingItem.uuid, 'CLOUD_SERVER', null, null, 'vm20' || debitorNumberSuffix, 'another CloudServer', '{}'::jsonb), (managedWebspaceUuid, relatedManagedWebspaceBookingItem.uuid, 'MANAGED_WEBSPACE', managedServerUuid, null, defaultPrefix || '01', 'some Webspace', '{}'::jsonb), + (uuid_generate_v4(), null, 'EMAIL_ALIAS', managedWebspaceUuid, null, defaultPrefix || '01-web', 'some E-Mail-Alias', '{ "target": [ "office@example.org", "archive@example.com" ] }'::jsonb), (webUnixUserUuid, null, 'UNIX_USER', managedWebspaceUuid, null, defaultPrefix || '01-web', 'some UnixUser for Website', '{ "SSD-soft-quota": "128", "SSD-hard-quota": "256", "HDD-soft-quota": "512", "HDD-hard-quota": "1024"}'::jsonb), (uuid_generate_v4(), null, 'DOMAIN_HTTP_SETUP', managedWebspaceUuid, webUnixUserUuid, defaultPrefix || '.example.org', 'some Domain-HTTP-Setup', '{ "option-htdocsfallback": true, "use-fcgiphpbin": "/usr/lib/cgi-bin/php", "validsubdomainnames": "*"}'::jsonb); end; $$; diff --git a/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetControllerAcceptanceTest.java b/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetControllerAcceptanceTest.java index 89de41bc..20ebd989 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetControllerAcceptanceTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetControllerAcceptanceTest.java @@ -29,6 +29,7 @@ import java.util.UUID; import java.util.function.Supplier; import static java.util.Map.entry; +import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.EMAIL_ALIAS; 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.UNIX_USER; @@ -101,7 +102,7 @@ class HsHostingAssetControllerAcceptanceTest extends ContextBasedTestWithCleanup } @Test - void globalAdmin_canViewAllAssetsByType() { + void webspaceAgent_canViewAllAssetsByType() { // given context("superuser-alex@hostsharing.net"); @@ -109,42 +110,25 @@ class HsHostingAssetControllerAcceptanceTest extends ContextBasedTestWithCleanup RestAssured // @formatter:off .given() .header("current-user", "superuser-alex@hostsharing.net") + .header("assumed-roles", "hs_hosting_asset#fir01:AGENT") .port(port) .when() - . get("http://localhost/api/hs/hosting/assets?type=" + MANAGED_SERVER) + . get("http://localhost/api/hs/hosting/assets?type=" + EMAIL_ALIAS) .then().log().all().assertThat() .statusCode(200) .contentType("application/json") .body("", lenientlyEquals(""" [ { - "type": "MANAGED_SERVER", - "identifier": "vm1011", - "caption": "some ManagedServer", + "type": "EMAIL_ALIAS", + "identifier": "fir01-web", + "caption": "some E-Mail-Alias", + "alarmContact": null, "config": { - "monit_max_cpu_usage": 90, - "monit_max_ram_usage": 80, - "monit_max_ssd_usage": 70 - } - }, - { - "type": "MANAGED_SERVER", - "identifier": "vm1012", - "caption": "some ManagedServer", - "config": { - "monit_max_cpu_usage": 90, - "monit_max_ram_usage": 80, - "monit_max_ssd_usage": 70 - } - }, - { - "type": "MANAGED_SERVER", - "identifier": "vm1013", - "caption": "some ManagedServer", - "config": { - "monit_max_cpu_usage": 90, - "monit_max_ram_usage": 80, - "monit_max_ssd_usage": 70 + "target": [ + "office@example.org", + "archive@example.com" + ] } } ] diff --git a/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetPropsControllerAcceptanceTest.java b/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetPropsControllerAcceptanceTest.java index 7910408c..e8323839 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetPropsControllerAcceptanceTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetPropsControllerAcceptanceTest.java @@ -34,7 +34,8 @@ class HsHostingAssetPropsControllerAcceptanceTest { "MANAGED_SERVER", "MANAGED_WEBSPACE", "CLOUD_SERVER", - "UNIX_USER" + "UNIX_USER", + "EMAIL_ALIAS" ] """)); // @formatter:on diff --git a/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsEMailAliasHostingAssetValidatorUnitTest.java b/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsEMailAliasHostingAssetValidatorUnitTest.java new file mode 100644 index 00000000..0a3768d8 --- /dev/null +++ b/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsEMailAliasHostingAssetValidatorUnitTest.java @@ -0,0 +1,125 @@ +package net.hostsharing.hsadminng.hs.hosting.asset.validators; + +import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetEntity; +import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType; +import net.hostsharing.hsadminng.mapper.Array; +import org.junit.jupiter.api.Test; + +import java.util.Map; + +import static java.util.Map.entry; +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.EMAIL_ALIAS; +import static org.assertj.core.api.Assertions.assertThat; + +class HsEMailAliasHostingAssetValidatorUnitTest { + + 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 final HsHostingAssetEntity TEST_MANAGED_WEBSPACE_HOSTING_ASSET = HsHostingAssetEntity.builder() + .type(HsHostingAssetType.MANAGED_WEBSPACE) + .identifier("xyz00") + .caption("some managed webspace") + .bookingItem(TEST_MANAGED_WEBSPACE_BOOKING_ITEM) + .build(); + + @Test + void containsAllValidations() { + // when + final var validator = HsHostingAssetEntityValidatorRegistry.forType(EMAIL_ALIAS); + + // then + assertThat(validator.properties()).map(Map::toString).containsExactlyInAnyOrder( + "{type=string[], propertyName=target, required=true, minLength=1}"); + } + + @Test + void validatesValidEntity() { + // given + final var emailAliasHostingAssetEntity = HsHostingAssetEntity.builder() + .type(EMAIL_ALIAS) + .parentAsset(TEST_MANAGED_WEBSPACE_HOSTING_ASSET) + .identifier("xyz00-office") + .config(Map.ofEntries( + entry("target", Array.of("xyz00", "xyz00-abc", "office@example.com")) + )) + .build(); + final var validator = HsHostingAssetEntityValidatorRegistry.forType(emailAliasHostingAssetEntity.getType()); + + // when + final var result = validator.validateEntity(emailAliasHostingAssetEntity); + + // then + assertThat(result).isEmpty(); + } + + @Test + void validatesProperties() { + // given + final var emailAliasHostingAssetEntity = HsHostingAssetEntity.builder() + .type(EMAIL_ALIAS) + .parentAsset(TEST_MANAGED_WEBSPACE_HOSTING_ASSET) + .identifier("xyz00-office") + .config(Map.ofEntries( + entry("target", Array.of("xyz00", "xyz00-abc", "garbage", "office@example.com")) + )) + .build(); + final var validator = HsHostingAssetEntityValidatorRegistry.forType(emailAliasHostingAssetEntity.getType()); + + // when + final var result = validator.validateEntity(emailAliasHostingAssetEntity); + + // then + assertThat(result).containsExactlyInAnyOrder( + "'EMAIL_ALIAS:xyz00-office.parentAsset' must be of type MANAGED_WEBSPACE but is of type MANAGED_SERVER"); + } + + @Test + void validatesInvalidIdentifier() { + // given + final var emailAliasHostingAssetEntity = HsHostingAssetEntity.builder() + .type(EMAIL_ALIAS) + .parentAsset(TEST_MANAGED_WEBSPACE_HOSTING_ASSET) + .identifier("abc00-office") + .config(Map.ofEntries( + entry("target", Array.of("xyz00", "xyz00-abc", "garbage", "office@example.com")) + )) + .build(); + final var validator = HsHostingAssetEntityValidatorRegistry.forType(emailAliasHostingAssetEntity.getType()); + + // when + final var result = validator.validateEntity(emailAliasHostingAssetEntity); + + // then + assertThat(result).containsExactlyInAnyOrder( + "'identifier' expected to match '^xyz00$|^xyz00-[a-z0-9]+$', but is 'abc00-office'"); + } + + @Test + void validatesInvalidReferences() { + // given + final var emailAliasHostingAssetEntity = HsHostingAssetEntity.builder() + .type(EMAIL_ALIAS) + .bookingItem(TEST_MANAGED_SERVER_BOOKING_ITEM) + .parentAsset(TEST_MANAGED_SERVER_HOSTING_ASSET) + .identifier("abc00-office") + .config(Map.ofEntries( + entry("target", Array.of("xyz00", "xyz00-abc", "garbage", "office@example.com")) + )) + .build(); + final var validator = HsHostingAssetEntityValidatorRegistry.forType(emailAliasHostingAssetEntity.getType()); + + // when + final var result = validator.validateEntity(emailAliasHostingAssetEntity); + + // then + assertThat(result).containsExactlyInAnyOrder( + "'identifier' expected to match '^xyz00$|^xyz00-[a-z0-9]+$', but is 'abc00-office'"); + } +} -- 2.39.5 From fcbbd13a7e363ebf620f80cc9793fb2a00ec1b56 Mon Sep 17 00:00:00 2001 From: Michael Hoennig Date: Tue, 2 Jul 2024 16:09:51 +0200 Subject: [PATCH 2/6] implements EMailAlias-Property with ArrayProperty+multi-RegEx --- .../hs/validation/ArrayProperty.java | 21 +++++++++++-------- .../hs/validation/StringProperty.java | 2 +- .../hs/validation/ValidatableProperty.java | 13 +++++++++--- ...ailAliasHostingAssetValidatorUnitTest.java | 13 +++++++----- ...gAssetEntityValidatorRegistryUnitTest.java | 3 ++- ...UnixUserHostingAssetValidatorUnitTest.java | 4 ++-- 6 files changed, 35 insertions(+), 21 deletions(-) diff --git a/src/main/java/net/hostsharing/hsadminng/hs/validation/ArrayProperty.java b/src/main/java/net/hostsharing/hsadminng/hs/validation/ArrayProperty.java index 6d01b4ba..31503968 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/validation/ArrayProperty.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/validation/ArrayProperty.java @@ -12,20 +12,22 @@ import static net.hostsharing.hsadminng.mapper.Array.insertAfterEntries; public class ArrayProperty

, E> extends ValidatableProperty, E[]> { private static final String[] KEY_ORDER = - insertAfterEntries(ValidatableProperty.KEY_ORDER, "required", "minLength" ,"maxLength"); - private final ValidatableProperty elementProperty; + insertAfterEntries( + insertAfterEntries(ValidatableProperty.KEY_ORDER, "required", "minLength" ,"maxLength"), + "propertyName", "elementProperty"); + private final ValidatableProperty elementsOf; private Integer minLength; private Integer maxLength; - private ArrayProperty(final ValidatableProperty elementProperty) { + private ArrayProperty(final ValidatableProperty elementsOf) { //noinspection unchecked - super((Class) elementProperty.type.arrayType(), elementProperty.propertyName, KEY_ORDER); - this.elementProperty = elementProperty; + super((Class) elementsOf.type.arrayType(), elementsOf.propertyName, KEY_ORDER); + this.elementsOf = elementsOf; } - public static ArrayProperty arrayOf(final ValidatableProperty elementProperty) { + public static ArrayProperty arrayOf(final ValidatableProperty elementsOf) { //noinspection unchecked - return (ArrayProperty) new ArrayProperty<>(elementProperty); + return (ArrayProperty) new ArrayProperty<>(elementsOf); } public ValidatableProperty minLength(final int minLength) { @@ -46,14 +48,15 @@ public class ArrayProperty

, E> extends Valid 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); } - stream(propValue).forEach(e -> elementProperty.validate(result, e, propProvider)); + stream(propValue).forEach(e -> elementsOf.validate(result, e, propProvider)); } @Override protected String simpleTypeName() { - return elementProperty.simpleTypeName() + "[]"; + return elementsOf.simpleTypeName() + "[]"; } + @SafeVarargs private String display(final E... propValue) { return "[" + Arrays.toString(propValue) + "]"; } diff --git a/src/main/java/net/hostsharing/hsadminng/hs/validation/StringProperty.java b/src/main/java/net/hostsharing/hsadminng/hs/validation/StringProperty.java index 28e67f87..a92af7f8 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/validation/StringProperty.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/validation/StringProperty.java @@ -69,7 +69,7 @@ public class StringProperty

> extends ValidatableProp result.add(propertyName + "' length is expected to be at max " + maxLength + " but length of " + display(propValue) + " is " + propValue.length()); } if (matchesRegEx != null && - stream(matchesRegEx).map(p -> p.matcher(propValue)).map(Matcher::matches).findAny().isEmpty()) { + stream(matchesRegEx).map(p -> p.matcher(propValue)).noneMatch(Matcher::matches)) { result.add(propertyName + "' is expected to match any of " + Arrays.toString(matchesRegEx) + " but " + display(propValue) + " does not match any"); } if (isReadOnly() && propValue != null) { diff --git a/src/main/java/net/hostsharing/hsadminng/hs/validation/ValidatableProperty.java b/src/main/java/net/hostsharing/hsadminng/hs/validation/ValidatableProperty.java index d2bf7056..346ee08b 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/validation/ValidatableProperty.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/validation/ValidatableProperty.java @@ -1,15 +1,16 @@ package net.hostsharing.hsadminng.hs.validation; import com.fasterxml.jackson.annotation.JsonIgnore; -import lombok.experimental.Accessors; import lombok.Getter; import lombok.RequiredArgsConstructor; import lombok.SneakyThrows; +import lombok.experimental.Accessors; import net.hostsharing.hsadminng.hs.booking.item.HsBookingItemEntity; import net.hostsharing.hsadminng.mapper.Array; import org.apache.commons.lang3.function.TriFunction; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collection; import java.util.LinkedHashMap; import java.util.List; @@ -22,6 +23,7 @@ import static java.lang.Boolean.FALSE; import static java.lang.Boolean.TRUE; import static java.util.Collections.emptyList; import static java.util.Optional.ofNullable; +import static org.apache.commons.lang3.ObjectUtils.isArray; @Getter @RequiredArgsConstructor @@ -239,8 +241,8 @@ protected void setDeferredInit(final Function[], T[]> } private Object arrayToList(final Object value) { - if ( value instanceof String[]) { - return List.of((String[])value); + if (isArray(value)) { + return Arrays.stream((Object[])value).map(Object::toString).toList(); } return value; } @@ -265,4 +267,9 @@ protected void setDeferredInit(final Function[], T[]> public T compute(final E entity) { return computedBy.apply(entity); } + + @Override + public String toString() { + return toOrderedMap().toString(); + } } diff --git a/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsEMailAliasHostingAssetValidatorUnitTest.java b/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsEMailAliasHostingAssetValidatorUnitTest.java index 0a3768d8..e9611845 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsEMailAliasHostingAssetValidatorUnitTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsEMailAliasHostingAssetValidatorUnitTest.java @@ -36,7 +36,7 @@ class HsEMailAliasHostingAssetValidatorUnitTest { // then assertThat(validator.properties()).map(Map::toString).containsExactlyInAnyOrder( - "{type=string[], propertyName=target, required=true, minLength=1}"); + "{type=string[], propertyName=target, elementProperty={type=string, propertyName=target, matchesRegEx=[^[a-z]{3}[0-9]{2}(-[a-z0-9]+)?$, ^[a-zA-Z0-9_.±]+@[a-zA-Z0-9-]+.[a-zA-Z0-9-.]+$], maxLength=320}, required=true, minLength=1}"); } @Test @@ -77,7 +77,7 @@ class HsEMailAliasHostingAssetValidatorUnitTest { // then assertThat(result).containsExactlyInAnyOrder( - "'EMAIL_ALIAS:xyz00-office.parentAsset' must be of type MANAGED_WEBSPACE but is of type MANAGED_SERVER"); + "'EMAIL_ALIAS:xyz00-office.config.target' is expected to match any of [^[a-z]{3}[0-9]{2}(-[a-z0-9]+)?$, ^[a-zA-Z0-9_.±]+@[a-zA-Z0-9-]+.[a-zA-Z0-9-.]+$] but 'garbage' does not match any"); } @Test @@ -88,7 +88,7 @@ class HsEMailAliasHostingAssetValidatorUnitTest { .parentAsset(TEST_MANAGED_WEBSPACE_HOSTING_ASSET) .identifier("abc00-office") .config(Map.ofEntries( - entry("target", Array.of("xyz00", "xyz00-abc", "garbage", "office@example.com")) + entry("target", Array.of("office@example.com")) )) .build(); final var validator = HsHostingAssetEntityValidatorRegistry.forType(emailAliasHostingAssetEntity.getType()); @@ -108,9 +108,10 @@ class HsEMailAliasHostingAssetValidatorUnitTest { .type(EMAIL_ALIAS) .bookingItem(TEST_MANAGED_SERVER_BOOKING_ITEM) .parentAsset(TEST_MANAGED_SERVER_HOSTING_ASSET) + .assignedToAsset(TEST_MANAGED_SERVER_HOSTING_ASSET) .identifier("abc00-office") .config(Map.ofEntries( - entry("target", Array.of("xyz00", "xyz00-abc", "garbage", "office@example.com")) + entry("target", Array.of("office@example.com")) )) .build(); final var validator = HsHostingAssetEntityValidatorRegistry.forType(emailAliasHostingAssetEntity.getType()); @@ -120,6 +121,8 @@ class HsEMailAliasHostingAssetValidatorUnitTest { // then assertThat(result).containsExactlyInAnyOrder( - "'identifier' expected to match '^xyz00$|^xyz00-[a-z0-9]+$', but is 'abc00-office'"); + "'EMAIL_ALIAS:abc00-office.bookingItem' must be null but is set to D-1234500:test project:test project booking item", + "'EMAIL_ALIAS:abc00-office.parentAsset' must be of type MANAGED_WEBSPACE but is of type MANAGED_SERVER", + "'EMAIL_ALIAS:abc00-office.assignedToAsset' must be null but is set to D-1234500:test project:test project booking item"); } } diff --git a/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsHostingAssetEntityValidatorRegistryUnitTest.java b/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsHostingAssetEntityValidatorRegistryUnitTest.java index 881b5c5f..b24a035c 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsHostingAssetEntityValidatorRegistryUnitTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsHostingAssetEntityValidatorRegistryUnitTest.java @@ -32,7 +32,8 @@ class HsHostingAssetEntityValidatorRegistryUnitTest { HsHostingAssetType.CLOUD_SERVER, HsHostingAssetType.MANAGED_SERVER, HsHostingAssetType.MANAGED_WEBSPACE, - HsHostingAssetType.UNIX_USER + HsHostingAssetType.UNIX_USER, + HsHostingAssetType.EMAIL_ALIAS ); } } diff --git a/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsUnixUserHostingAssetValidatorUnitTest.java b/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsUnixUserHostingAssetValidatorUnitTest.java index 5ef61da9..ce1b5a1d 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsUnixUserHostingAssetValidatorUnitTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsUnixUserHostingAssetValidatorUnitTest.java @@ -110,7 +110,7 @@ class HsUnixUserHostingAssetValidatorUnitTest { "'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'", "'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.totpKey' is expected to match any of [^0x([0-9A-Fa-f]{2})+$] but provided value does not match any", "'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" ); @@ -168,7 +168,7 @@ class HsUnixUserHostingAssetValidatorUnitTest { "{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=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, computed=true, hashedUsing=SHA512, undisclosed=true}" ); } -- 2.39.5 From c3fda4e4b264281b6455c36ac843894398300374 Mon Sep 17 00:00:00 2001 From: Michael Hoennig Date: Tue, 2 Jul 2024 17:31:54 +0200 Subject: [PATCH 3/6] introduced HsHostingAssetsControllerRestTest --- .../HsHostingAssetsControllerRestTest.java | 107 ++++++++++++++++++ 1 file changed, 107 insertions(+) create mode 100644 src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetsControllerRestTest.java diff --git a/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetsControllerRestTest.java b/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetsControllerRestTest.java new file mode 100644 index 00000000..98ffeeec --- /dev/null +++ b/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetsControllerRestTest.java @@ -0,0 +1,107 @@ +package net.hostsharing.hsadminng.hs.hosting.asset; + +import static net.hostsharing.hsadminng.rbac.test.JsonMatcher.lenientlyEquals; + +import net.hostsharing.hsadminng.context.Context; +import net.hostsharing.hsadminng.hs.booking.item.HsBookingItemRepository; +import net.hostsharing.hsadminng.mapper.Mapper; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.EnumSource; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.context.annotation.Import; +import org.springframework.http.MediaType; +import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; + +import jakarta.persistence.EntityManager; +import jakarta.persistence.EntityManagerFactory; +import jakarta.persistence.SynchronizationType; + +import java.util.Map; + +import static org.hamcrest.Matchers.is; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.when; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@WebMvcTest(HsHostingAssetController.class) +@Import(Mapper.class) +@RunWith(SpringRunner.class) +public class HsHostingAssetsControllerRestTest { + + @Autowired + MockMvc mockMvc; + + @MockBean + Context contextMock; + + @MockBean + Mapper mapper; + + @Mock + private EntityManager em; + + @MockBean + EntityManagerFactory emf; + + @MockBean + private HsBookingItemRepository bookingItemRepo; + + @MockBean + private HsHostingAssetRepository hostingAssetRepo; + + enum ListTestCases { + MANAGED_SERVER( + "?type=EMAIL_ALIAS", + """ + { + "transactionType": "DEPOSIT", + "assetValue": 128.00, + "valueDate": "2022-10-13", + "reference": "valid reference", + "comment": "valid comment" + } + """); + + final String queryString; + final String expectedResponse; + + ListTestCases(final String queryString, final String expectedResponse) { + this.queryString = queryString; + this.expectedResponse = expectedResponse; + } + } + + @BeforeEach + void init() { + when(emf.createEntityManager()).thenReturn(em); + when(emf.createEntityManager(any(Map.class))).thenReturn(em); + when(emf.createEntityManager(any(SynchronizationType.class))).thenReturn(em); + when(emf.createEntityManager(any(SynchronizationType.class), any(Map.class))).thenReturn(em); + } + + @ParameterizedTest + @EnumSource(HsHostingAssetsControllerRestTest.ListTestCases.class) + void shouldListAssets(final HsHostingAssetsControllerRestTest.ListTestCases testCase) throws Exception { + + // when + mockMvc.perform(MockMvcRequestBuilders + .get("/api/hs/hosting/assets"+testCase.queryString) + .header("current-user", "superuser-alex@hostsharing.net") + //.contentType(MediaType.APPLICATION_JSON) + .accept(MediaType.APPLICATION_JSON)) + + // then + .andExpect(status().is2xxSuccessful()) + .andExpect(jsonPath("statusCode", is(200))) + .andExpect(jsonPath("body", lenientlyEquals(testCase.expectedResponse))); + } + +} -- 2.39.5 From d22d81e71cf2247963e100fe5fa8cc939dcc2d4d Mon Sep 17 00:00:00 2001 From: Michael Hoennig Date: Wed, 3 Jul 2024 10:26:31 +0200 Subject: [PATCH 4/6] added some other asset types --- .../asset/HsHostingAssetController.java | 4 +- .../HsHostingAssetControllerRestTest.java | 234 ++++++++++++++++++ .../HsHostingAssetsControllerRestTest.java | 107 -------- .../asset/TestHsHostingAssetEntities.java | 22 ++ ...ailAliasHostingAssetValidatorUnitTest.java | 18 +- 5 files changed, 260 insertions(+), 125 deletions(-) create mode 100644 src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetControllerRestTest.java delete mode 100644 src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetsControllerRestTest.java create mode 100644 src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/TestHsHostingAssetEntities.java diff --git a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetController.java b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetController.java index ca4c4a3e..d9b6492f 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetController.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetController.java @@ -159,6 +159,6 @@ public class HsHostingAssetController implements HsHostingAssetsApi { @SuppressWarnings("unchecked") final BiConsumer ENTITY_TO_RESOURCE_POSTMAPPER = (entity, resource) - -> HsHostingAssetEntityValidatorRegistry.forType(entity.getType()) - .revampProperties(entity, (Map) resource.getConfig()); + -> resource.setConfig(HsHostingAssetEntityValidatorRegistry.forType(entity.getType()) + .revampProperties(entity, (Map) resource.getConfig())); } diff --git a/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetControllerRestTest.java b/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetControllerRestTest.java new file mode 100644 index 00000000..67ac8aba --- /dev/null +++ b/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetControllerRestTest.java @@ -0,0 +1,234 @@ +package net.hostsharing.hsadminng.hs.hosting.asset; + +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.SneakyThrows; +import net.hostsharing.hsadminng.context.Context; +import net.hostsharing.hsadminng.hs.booking.item.HsBookingItemRepository; +import net.hostsharing.hsadminng.mapper.Array; +import net.hostsharing.hsadminng.mapper.Mapper; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.EnumSource; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.context.annotation.Import; +import org.springframework.http.MediaType; +import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; + +import jakarta.persistence.EntityManager; +import jakarta.persistence.EntityManagerFactory; +import jakarta.persistence.SynchronizationType; +import java.util.List; +import java.util.Map; + +import static java.util.Map.entry; +import static net.hostsharing.hsadminng.hs.booking.item.TestHsBookingItem.TEST_CLOUD_SERVER_BOOKING_ITEM; +import static net.hostsharing.hsadminng.hs.booking.item.TestHsBookingItem.TEST_MANAGED_SERVER_BOOKING_ITEM; +import static net.hostsharing.hsadminng.hs.hosting.asset.TestHsHostingAssetEntities.TEST_MANAGED_WEBSPACE_HOSTING_ASSET; +import static net.hostsharing.hsadminng.hs.office.contact.TestHsOfficeContact.TEST_CONTACT; +import static net.hostsharing.hsadminng.rbac.test.JsonMatcher.lenientlyEquals; +import static net.hostsharing.hsadminng.rbac.test.JsonMatcher.strictlyEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.when; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@WebMvcTest(HsHostingAssetController.class) +@Import(Mapper.class) +@RunWith(SpringRunner.class) +public class HsHostingAssetControllerRestTest { + + @Autowired + MockMvc mockMvc; + + @MockBean + Context contextMock; + + @Autowired + Mapper mapper; + + @Mock + private EntityManager em; + + @MockBean + EntityManagerFactory emf; + + @MockBean + @SuppressWarnings("unused") // bean needs to be present for HsHostingAssetController + private HsBookingItemRepository bookingItemRepo; + + @MockBean + private HsHostingAssetRepository hostingAssetRepo; + + enum ListTestCases { + CLOUD_SERVER( + List.of( + HsHostingAssetEntity.builder() + .type(HsHostingAssetType.CLOUD_SERVER) + .bookingItem(TEST_CLOUD_SERVER_BOOKING_ITEM) + .identifier("vm1234") + .caption("some fake cloud-server") + .alarmContact(TEST_CONTACT) + .build()), + """ + [ + { + "type": "CLOUD_SERVER", + "identifier": "vm1234", + "caption": "some fake cloud-server", + "alarmContact": { + "caption": "some contact", + "postalAddress": "address of some contact", + "emailAddresses": { + "main": "some-contact@example.com" + } + }, + "config": {} + } + ] + """), + MANAGED_SERVER( + List.of( + HsHostingAssetEntity.builder() + .type(HsHostingAssetType.MANAGED_SERVER) + .bookingItem(TEST_MANAGED_SERVER_BOOKING_ITEM) + .identifier("vm1234") + .caption("some fake managed-server") + .alarmContact(TEST_CONTACT) + .config(Map.ofEntries( + entry("monit_max_ssd_usage", 70), + entry("monit_max_cpu_usage", 80), + entry("monit_max_ram_usage", 90) + )) + .build()), + """ + [ + { + "type": "MANAGED_SERVER", + "identifier": "vm1234", + "caption": "some fake managed-server", + "alarmContact": { + "caption": "some contact", + "postalAddress": "address of some contact", + "emailAddresses": { + "main": "some-contact@example.com" + } + }, + "config": { + "monit_max_ssd_usage": 70, + "monit_max_cpu_usage": 80, + "monit_max_ram_usage": 90 + } + } + ] + """), + UNIX_USER( + List.of( + HsHostingAssetEntity.builder() + .type(HsHostingAssetType.UNIX_USER) + .parentAsset(TEST_MANAGED_WEBSPACE_HOSTING_ASSET) + .identifier("xyz00-office") + .caption("some fake Unix-User") + .config(Map.ofEntries( + entry("password", "$6$salt$hashed-salted-password"), + entry("totpKey", "0x0123456789abcdef"), + entry("shell", "/bin/bash"), + entry("SSD-soft-quota", 128), + entry("SSD-hard-quota", 256), + entry("HDD-soft-quota", 256), + entry("HDD-hard-quota", 512))) + .build()), + """ + [ + { + "type": "UNIX_USER", + "identifier": "xyz00-office", + "caption": "some fake Unix-User", + "alarmContact": null, + "config": { + "SSD-soft-quota": 128, + "SSD-hard-quota": 256, + "HDD-soft-quota": 256, + "HDD-hard-quota": 512, + "shell": "/bin/bash", + "homedir": "/home/pacs/xyz00/users/office" + } + } + ] + """), + EMAIL_ALIAS( + List.of( + HsHostingAssetEntity.builder() + .type(HsHostingAssetType.EMAIL_ALIAS) + .parentAsset(TEST_MANAGED_WEBSPACE_HOSTING_ASSET) + .identifier("xyz00-office") + .caption("some fake EMail-Alias") + .config(Map.ofEntries( + entry("target", Array.of("xyz00", "xyz00-abc", "office@example.com")) + )) + .build()), + """ + [ + { + "type": "EMAIL_ALIAS", + "identifier": "xyz00-office", + "caption": "some fake EMail-Alias", + "alarmContact": null, + "config": { + "target": ["xyz00","xyz00-abc","office@example.com"] + } + } + ] + """); + + final HsHostingAssetType assetType; + final List givenHostingAssetsOfType; + final String expectedResponse; + + ListTestCases( + final List givenHostingAssetsOfType, + final String expectedResponse) { + this.assetType = HsHostingAssetType.valueOf(name()); + this.givenHostingAssetsOfType = givenHostingAssetsOfType; + this.expectedResponse = expectedResponse; + } + + @SneakyThrows + String expectedConfig() { + // FIXME: iterate all indexes, not just 0 + return new ObjectMapper().readTree(expectedResponse).get(0).path("config").toString(); + } + } + + @BeforeEach + void init() { + when(emf.createEntityManager()).thenReturn(em); + when(emf.createEntityManager(any(Map.class))).thenReturn(em); + when(emf.createEntityManager(any(SynchronizationType.class))).thenReturn(em); + when(emf.createEntityManager(any(SynchronizationType.class), any(Map.class))).thenReturn(em); + } + + @ParameterizedTest + @EnumSource(HsHostingAssetControllerRestTest.ListTestCases.class) + void shouldListAssets(final HsHostingAssetControllerRestTest.ListTestCases testCase) throws Exception { + // given + when(hostingAssetRepo.findAllByCriteria(null, null, testCase.assetType)) + .thenReturn(testCase.givenHostingAssetsOfType); + + // when + mockMvc.perform(MockMvcRequestBuilders + .get("/api/hs/hosting/assets?type="+testCase.name()) + .header("current-user", "superuser-alex@hostsharing.net") + .accept(MediaType.APPLICATION_JSON)) + + // then + .andExpect(status().is2xxSuccessful()) + .andExpect(jsonPath("$", lenientlyEquals(testCase.expectedResponse))) + .andExpect(jsonPath("[0].config", strictlyEquals(testCase.expectedConfig()))); + } +} diff --git a/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetsControllerRestTest.java b/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetsControllerRestTest.java deleted file mode 100644 index 98ffeeec..00000000 --- a/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetsControllerRestTest.java +++ /dev/null @@ -1,107 +0,0 @@ -package net.hostsharing.hsadminng.hs.hosting.asset; - -import static net.hostsharing.hsadminng.rbac.test.JsonMatcher.lenientlyEquals; - -import net.hostsharing.hsadminng.context.Context; -import net.hostsharing.hsadminng.hs.booking.item.HsBookingItemRepository; -import net.hostsharing.hsadminng.mapper.Mapper; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.EnumSource; -import org.junit.runner.RunWith; -import org.mockito.Mock; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; -import org.springframework.boot.test.mock.mockito.MockBean; -import org.springframework.context.annotation.Import; -import org.springframework.http.MediaType; -import org.springframework.test.context.junit4.SpringRunner; -import org.springframework.test.web.servlet.MockMvc; -import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; - -import jakarta.persistence.EntityManager; -import jakarta.persistence.EntityManagerFactory; -import jakarta.persistence.SynchronizationType; - -import java.util.Map; - -import static org.hamcrest.Matchers.is; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.when; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; - -@WebMvcTest(HsHostingAssetController.class) -@Import(Mapper.class) -@RunWith(SpringRunner.class) -public class HsHostingAssetsControllerRestTest { - - @Autowired - MockMvc mockMvc; - - @MockBean - Context contextMock; - - @MockBean - Mapper mapper; - - @Mock - private EntityManager em; - - @MockBean - EntityManagerFactory emf; - - @MockBean - private HsBookingItemRepository bookingItemRepo; - - @MockBean - private HsHostingAssetRepository hostingAssetRepo; - - enum ListTestCases { - MANAGED_SERVER( - "?type=EMAIL_ALIAS", - """ - { - "transactionType": "DEPOSIT", - "assetValue": 128.00, - "valueDate": "2022-10-13", - "reference": "valid reference", - "comment": "valid comment" - } - """); - - final String queryString; - final String expectedResponse; - - ListTestCases(final String queryString, final String expectedResponse) { - this.queryString = queryString; - this.expectedResponse = expectedResponse; - } - } - - @BeforeEach - void init() { - when(emf.createEntityManager()).thenReturn(em); - when(emf.createEntityManager(any(Map.class))).thenReturn(em); - when(emf.createEntityManager(any(SynchronizationType.class))).thenReturn(em); - when(emf.createEntityManager(any(SynchronizationType.class), any(Map.class))).thenReturn(em); - } - - @ParameterizedTest - @EnumSource(HsHostingAssetsControllerRestTest.ListTestCases.class) - void shouldListAssets(final HsHostingAssetsControllerRestTest.ListTestCases testCase) throws Exception { - - // when - mockMvc.perform(MockMvcRequestBuilders - .get("/api/hs/hosting/assets"+testCase.queryString) - .header("current-user", "superuser-alex@hostsharing.net") - //.contentType(MediaType.APPLICATION_JSON) - .accept(MediaType.APPLICATION_JSON)) - - // then - .andExpect(status().is2xxSuccessful()) - .andExpect(jsonPath("statusCode", is(200))) - .andExpect(jsonPath("body", lenientlyEquals(testCase.expectedResponse))); - } - -} diff --git a/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/TestHsHostingAssetEntities.java b/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/TestHsHostingAssetEntities.java new file mode 100644 index 00000000..e409306b --- /dev/null +++ b/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/TestHsHostingAssetEntities.java @@ -0,0 +1,22 @@ +package net.hostsharing.hsadminng.hs.hosting.asset; + +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; + +public class TestHsHostingAssetEntities { + + public static 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(); + + public static final HsHostingAssetEntity TEST_MANAGED_WEBSPACE_HOSTING_ASSET = HsHostingAssetEntity.builder() + .type(HsHostingAssetType.MANAGED_WEBSPACE) + .identifier("xyz00") + .caption("some managed webspace") + .bookingItem(TEST_MANAGED_WEBSPACE_BOOKING_ITEM) + .build(); + +} diff --git a/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsEMailAliasHostingAssetValidatorUnitTest.java b/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsEMailAliasHostingAssetValidatorUnitTest.java index e9611845..fa715e6c 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsEMailAliasHostingAssetValidatorUnitTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsEMailAliasHostingAssetValidatorUnitTest.java @@ -1,7 +1,6 @@ package net.hostsharing.hsadminng.hs.hosting.asset.validators; import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetEntity; -import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType; import net.hostsharing.hsadminng.mapper.Array; import org.junit.jupiter.api.Test; @@ -9,26 +8,13 @@ import java.util.Map; import static java.util.Map.entry; 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.EMAIL_ALIAS; +import static net.hostsharing.hsadminng.hs.hosting.asset.TestHsHostingAssetEntities.TEST_MANAGED_SERVER_HOSTING_ASSET; +import static net.hostsharing.hsadminng.hs.hosting.asset.TestHsHostingAssetEntities.TEST_MANAGED_WEBSPACE_HOSTING_ASSET; import static org.assertj.core.api.Assertions.assertThat; class HsEMailAliasHostingAssetValidatorUnitTest { - 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 final HsHostingAssetEntity TEST_MANAGED_WEBSPACE_HOSTING_ASSET = HsHostingAssetEntity.builder() - .type(HsHostingAssetType.MANAGED_WEBSPACE) - .identifier("xyz00") - .caption("some managed webspace") - .bookingItem(TEST_MANAGED_WEBSPACE_BOOKING_ITEM) - .build(); - @Test void containsAllValidations() { // when -- 2.39.5 From 274b74514c6a3667a68c1ce2b807d1a6f447d00c Mon Sep 17 00:00:00 2001 From: Michael Hoennig Date: Wed, 3 Jul 2024 11:24:55 +0200 Subject: [PATCH 5/6] amendmends according to code-review --- .../validators/HsEMailAliasHostingAssetValidator.java | 4 ++-- .../hsadminng/hs/validation/ArrayProperty.java | 8 ++++---- .../hsadminng/hs/validation/PasswordProperty.java | 4 ++-- src/main/java/net/hostsharing/hsadminng/mapper/Array.java | 2 +- .../HsEMailAliasHostingAssetValidatorUnitTest.java | 4 ++-- 5 files changed, 11 insertions(+), 11 deletions(-) diff --git a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsEMailAliasHostingAssetValidator.java b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsEMailAliasHostingAssetValidator.java index c405270a..d151b49d 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsEMailAliasHostingAssetValidator.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsEMailAliasHostingAssetValidator.java @@ -10,8 +10,8 @@ import static net.hostsharing.hsadminng.hs.validation.StringProperty.stringPrope class HsEMailAliasHostingAssetValidator extends HsHostingAssetEntityValidator { - private static final String UNIX_USER_REGEX = "^[a-z]{3}[0-9]{2}(-[a-z0-9]+)?$"; - private static final String EMAIL_ADDRESS_REGEX = "^[a-zA-Z0-9_.±]+@[a-zA-Z0-9-]+.[a-zA-Z0-9-.]+$"; + private static final String UNIX_USER_REGEX = "^[a-z][a-z0-9]{2}[0-9]{2}(-[a-z0-9]+)?$"; // also accepts legacy pac-names + private static final String EMAIL_ADDRESS_REGEX = "^[a-zA-Z0-9_!#$%&'*+/=?`{|}~^.-]+@[a-zA-Z0-9.-]+$"; // RFC 5322 public static final int EMAIL_ADDRESS_MAX_LENGTH = 320; // according to RFC 5321 and RFC 5322 HsEMailAliasHostingAssetValidator() { diff --git a/src/main/java/net/hostsharing/hsadminng/hs/validation/ArrayProperty.java b/src/main/java/net/hostsharing/hsadminng/hs/validation/ArrayProperty.java index 31503968..9001ea81 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/validation/ArrayProperty.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/validation/ArrayProperty.java @@ -6,15 +6,15 @@ import java.util.Arrays; import java.util.List; import static java.util.Arrays.stream; -import static net.hostsharing.hsadminng.mapper.Array.insertAfterEntries; +import static net.hostsharing.hsadminng.mapper.Array.insertNewEntriesAfterExistingEntry; @Setter public class ArrayProperty

, E> extends ValidatableProperty, E[]> { private static final String[] KEY_ORDER = - insertAfterEntries( - insertAfterEntries(ValidatableProperty.KEY_ORDER, "required", "minLength" ,"maxLength"), - "propertyName", "elementProperty"); + insertNewEntriesAfterExistingEntry( + insertNewEntriesAfterExistingEntry(ValidatableProperty.KEY_ORDER, "required", "minLength" ,"maxLength"), + "propertyName", "elementsOf"); private final ValidatableProperty elementsOf; private Integer minLength; private Integer maxLength; diff --git a/src/main/java/net/hostsharing/hsadminng/hs/validation/PasswordProperty.java b/src/main/java/net/hostsharing/hsadminng/hs/validation/PasswordProperty.java index ac37a89c..83cdf975 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/validation/PasswordProperty.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/validation/PasswordProperty.java @@ -8,12 +8,12 @@ import java.util.stream.Stream; import static java.util.Optional.ofNullable; import static net.hostsharing.hsadminng.hash.LinuxEtcShadowHashGenerator.hash; -import static net.hostsharing.hsadminng.mapper.Array.insertAfterEntries; +import static net.hostsharing.hsadminng.mapper.Array.insertNewEntriesAfterExistingEntry; @Setter public class PasswordProperty extends StringProperty { - private static final String[] KEY_ORDER = insertAfterEntries(StringProperty.KEY_ORDER, "computed", "hashedUsing"); + private static final String[] KEY_ORDER = insertNewEntriesAfterExistingEntry(StringProperty.KEY_ORDER, "computed", "hashedUsing"); private Algorithm hashedUsing; diff --git a/src/main/java/net/hostsharing/hsadminng/mapper/Array.java b/src/main/java/net/hostsharing/hsadminng/mapper/Array.java index 777bd0b3..80970aa4 100644 --- a/src/main/java/net/hostsharing/hsadminng/mapper/Array.java +++ b/src/main/java/net/hostsharing/hsadminng/mapper/Array.java @@ -52,7 +52,7 @@ public class Array { } @SafeVarargs - public static T[] insertAfterEntries(final T[] array, final T entryToFind, final T... newEntries) { + public static T[] insertNewEntriesAfterExistingEntry(final T[] array, final T entryToFind, final T... newEntries) { final var arrayList = new ArrayList<>(asList(array)); final var index = arrayList.indexOf(entryToFind); if (index < 0) { diff --git a/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsEMailAliasHostingAssetValidatorUnitTest.java b/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsEMailAliasHostingAssetValidatorUnitTest.java index fa715e6c..6c35078b 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsEMailAliasHostingAssetValidatorUnitTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsEMailAliasHostingAssetValidatorUnitTest.java @@ -22,7 +22,7 @@ class HsEMailAliasHostingAssetValidatorUnitTest { // then assertThat(validator.properties()).map(Map::toString).containsExactlyInAnyOrder( - "{type=string[], propertyName=target, elementProperty={type=string, propertyName=target, matchesRegEx=[^[a-z]{3}[0-9]{2}(-[a-z0-9]+)?$, ^[a-zA-Z0-9_.±]+@[a-zA-Z0-9-]+.[a-zA-Z0-9-.]+$], maxLength=320}, required=true, minLength=1}"); + "{type=string[], propertyName=target, elementsOf={type=string, propertyName=target, matchesRegEx=[^[a-z][a-z0-9]{2}[0-9]{2}(-[a-z0-9]+)?$, ^[a-zA-Z0-9_!#$%&'*+/=?`{|}~^.-]+@[a-zA-Z0-9.-]+$], maxLength=320}, required=true, minLength=1}"); } @Test @@ -63,7 +63,7 @@ class HsEMailAliasHostingAssetValidatorUnitTest { // then assertThat(result).containsExactlyInAnyOrder( - "'EMAIL_ALIAS:xyz00-office.config.target' is expected to match any of [^[a-z]{3}[0-9]{2}(-[a-z0-9]+)?$, ^[a-zA-Z0-9_.±]+@[a-zA-Z0-9-]+.[a-zA-Z0-9-.]+$] but 'garbage' does not match any"); + "'EMAIL_ALIAS:xyz00-office.config.target' is expected to match any of [^[a-z][a-z0-9]{2}[0-9]{2}(-[a-z0-9]+)?$, ^[a-zA-Z0-9_!#$%&'*+/=?`{|}~^.-]+@[a-zA-Z0-9.-]+$] but 'garbage' does not match any"); } @Test -- 2.39.5 From 28400d1933454d46148a803500e126fd065e8921 Mon Sep 17 00:00:00 2001 From: Michael Hoennig Date: Wed, 3 Jul 2024 11:41:42 +0200 Subject: [PATCH 6/6] strictly verify config props of all returned elements --- .../HsHostingAssetControllerRestTest.java | 21 +++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetControllerRestTest.java b/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetControllerRestTest.java index 67ac8aba..529d34cd 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetControllerRestTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetControllerRestTest.java @@ -1,5 +1,6 @@ package net.hostsharing.hsadminng.hs.hosting.asset; +import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import lombok.SneakyThrows; import net.hostsharing.hsadminng.context.Context; @@ -32,7 +33,7 @@ import static net.hostsharing.hsadminng.hs.booking.item.TestHsBookingItem.TEST_M import static net.hostsharing.hsadminng.hs.hosting.asset.TestHsHostingAssetEntities.TEST_MANAGED_WEBSPACE_HOSTING_ASSET; import static net.hostsharing.hsadminng.hs.office.contact.TestHsOfficeContact.TEST_CONTACT; 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.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.when; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; @@ -189,19 +190,21 @@ public class HsHostingAssetControllerRestTest { final HsHostingAssetType assetType; final List givenHostingAssetsOfType; final String expectedResponse; + final JsonNode expectedResponseJson; + @SneakyThrows ListTestCases( final List givenHostingAssetsOfType, final String expectedResponse) { this.assetType = HsHostingAssetType.valueOf(name()); this.givenHostingAssetsOfType = givenHostingAssetsOfType; this.expectedResponse = expectedResponse; + this.expectedResponseJson = new ObjectMapper().readTree(expectedResponse); } @SneakyThrows - String expectedConfig() { - // FIXME: iterate all indexes, not just 0 - return new ObjectMapper().readTree(expectedResponse).get(0).path("config").toString(); + JsonNode expectedConfig(final int n) { + return expectedResponseJson.get(n).path("config"); } } @@ -221,7 +224,7 @@ public class HsHostingAssetControllerRestTest { .thenReturn(testCase.givenHostingAssetsOfType); // when - mockMvc.perform(MockMvcRequestBuilders + final var result = mockMvc.perform(MockMvcRequestBuilders .get("/api/hs/hosting/assets?type="+testCase.name()) .header("current-user", "superuser-alex@hostsharing.net") .accept(MediaType.APPLICATION_JSON)) @@ -229,6 +232,12 @@ public class HsHostingAssetControllerRestTest { // then .andExpect(status().is2xxSuccessful()) .andExpect(jsonPath("$", lenientlyEquals(testCase.expectedResponse))) - .andExpect(jsonPath("[0].config", strictlyEquals(testCase.expectedConfig()))); + .andReturn(); + + // and the config properties do match not just leniently but even strictly + final var resultBody = new ObjectMapper().readTree(result.getResponse().getContentAsString()); + for (int n = 0; n < resultBody.size(); ++n) { + assertThat(resultBody.get(n).path("config")).isEqualTo(testCase.expectedConfig(n)); + } } } -- 2.39.5