diff --git a/src/main/java/net/hostsharing/hsadminng/hs/booking/item/validators/HsDomainSetupBookingItemValidator.java b/src/main/java/net/hostsharing/hsadminng/hs/booking/item/validators/HsDomainSetupBookingItemValidator.java index a48ed4a5..f3062fc2 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/booking/item/validators/HsDomainSetupBookingItemValidator.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/booking/item/validators/HsDomainSetupBookingItemValidator.java @@ -1,10 +1,33 @@ package net.hostsharing.hsadminng.hs.booking.item.validators; +import net.hostsharing.hsadminng.mapper.Array; + +import static net.hostsharing.hsadminng.hs.validation.StringProperty.stringProperty; + class HsDomainSetupBookingItemValidator extends HsBookingItemEntityValidator { + public static final String FQDN_REGEX = "^((?!-)[A-Za-z0-9-]{1,63}(?> extends ValidatableProp protected static final String[] KEY_ORDER = Array.join( ValidatableProperty.KEY_ORDER_HEAD, - Array.of("matchesRegEx", "minLength", "maxLength", "provided"), + Array.of("matchesRegEx", "notMatchesRegEx", "minLength", "maxLength", "provided"), ValidatableProperty.KEY_ORDER_TAIL, Array.of("undisclosed")); private String[] provided; private Pattern[] matchesRegEx; + private String matchesRegExDescription; + private Pattern[] notMatchesRegEx; + private String notMatchesRegExDescription; + @Setter(AccessLevel.PRIVATE) + private Consumer describedAsConsumer; private Integer minLength; private Integer maxLength; private boolean undisclosed; @@ -56,10 +63,23 @@ public class StringProperty

> extends ValidatableProp public P matchesRegEx(final String... regExPattern) { this.matchesRegEx = stream(regExPattern).map(Pattern::compile).toArray(Pattern[]::new); + this.describedAsConsumer = violationMessage -> matchesRegExDescription = violationMessage; return self(); } - /// predifined values, similar to fixed values in a combobox + public P notMatchesRegEx(final String... regExPattern) { + this.notMatchesRegEx = stream(regExPattern).map(Pattern::compile).toArray(Pattern[]::new); + this.describedAsConsumer = violationMessage -> notMatchesRegExDescription = violationMessage; + return self(); + } + + public P describedAs(final String violationMessage) { + describedAsConsumer.accept(violationMessage); + describedAsConsumer = null; + return self(); + } + + /// predefined values, similar to fixed values in a combobox public P provided(final String... provided) { this.provided = provided; return self(); @@ -78,16 +98,10 @@ public class StringProperty

> extends ValidatableProp @Override protected void validate(final List result, final String propValue, final PropertiesProvider propProvider) { super.validate(result, propValue, propProvider); - if (minLength != 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 && - 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" + (matchesRegEx.length>1?" any":"")); - } + validateMinLength(result, propValue); + validateMaxLength(result, propValue); + validateMatchesRegEx(result, propValue); + validateNotMatchesRegEx(result, propValue); } @Override @@ -99,4 +113,47 @@ public class StringProperty

> extends ValidatableProp protected String simpleTypeName() { return "string"; } + + private void validateMinLength(final List result, final String propValue) { + if (minLength != null && propValue.length() result, final String propValue) { + 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()); + } + } + + private void validateMatchesRegEx(final List result, final String propValue) { + if (matchesRegEx != null && + stream(matchesRegEx).map(p -> p.matcher(propValue)).noneMatch(Matcher::matches)) { + if (matchesRegExDescription != null) { + result.add(propertyName + "' = " + display(propValue) + " " + matchesRegExDescription); + } else if (matchesRegEx.length>1) { + result.add(propertyName + "' is expected to match any of " + Arrays.toString(matchesRegEx) + + " but " + display(propValue) + " does not match any"); + } else { + result.add(propertyName + "' is expected to match " + Arrays.toString(matchesRegEx) + " but " + display( + propValue) + + " does not match"); + } + } + } + + private void validateNotMatchesRegEx(final List result, final String propValue) { + if (notMatchesRegEx != null && + stream(notMatchesRegEx).map(p -> p.matcher(propValue)).anyMatch(Matcher::matches)) { + if (notMatchesRegExDescription != null) { + result.add(propertyName + "' = " + display(propValue) + " " + notMatchesRegExDescription); + } else if (notMatchesRegEx.length>1) { + result.add(propertyName + "' is expected not to match any of " + Arrays.toString(notMatchesRegEx) + + " but " + display(propValue) + " does match at least one"); + } else { + result.add(propertyName + "' is expected not to match " + Arrays.toString(notMatchesRegEx) + + " but " + display(propValue) + " does match"); + } + } + } } diff --git a/src/test/java/net/hostsharing/hsadminng/hs/booking/item/validators/HsDomainSetupBookingItemValidatorUnitTest.java b/src/test/java/net/hostsharing/hsadminng/hs/booking/item/validators/HsDomainSetupBookingItemValidatorUnitTest.java new file mode 100644 index 00000000..2a83f418 --- /dev/null +++ b/src/test/java/net/hostsharing/hsadminng/hs/booking/item/validators/HsDomainSetupBookingItemValidatorUnitTest.java @@ -0,0 +1,117 @@ +package net.hostsharing.hsadminng.hs.booking.item.validators; + +import net.hostsharing.hsadminng.hs.booking.debitor.HsBookingDebitorEntity; +import net.hostsharing.hsadminng.hs.booking.item.HsBookingItemRealEntity; +import net.hostsharing.hsadminng.hs.booking.project.HsBookingProjectRealEntity; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +import jakarta.persistence.EntityManager; +import java.util.Map; + +import static java.util.Map.entry; +import static net.hostsharing.hsadminng.hs.booking.item.HsBookingItemType.CLOUD_SERVER; +import static net.hostsharing.hsadminng.hs.booking.item.HsBookingItemType.DOMAIN_SETUP; +import static org.assertj.core.api.Assertions.assertThat; + +class HsDomainSetupBookingItemValidatorUnitTest { + + final HsBookingDebitorEntity debitor = HsBookingDebitorEntity.builder() + .debitorNumber(12345) + .build(); + final HsBookingProjectRealEntity project = HsBookingProjectRealEntity.builder() + .debitor(debitor) + .caption("Test-Project") + .build(); + private EntityManager em; + + @Test + void acceptsUnregisteredDomain() { + // given + final var cloudServerBookingItemEntity = HsBookingItemRealEntity.builder() + .type(DOMAIN_SETUP) + .project(project) + .caption("Test-Domain") + .resources(Map.ofEntries( + entry("domainName", "example.org") // TODO.test: amend once we check registration + )) + .build(); + + // when + final var result = HsBookingItemEntityValidatorRegistry.doValidate(em, cloudServerBookingItemEntity); + + // then + assertThat(result).isEmpty(); + } + + @Test + void rejectsTopLevelDomain() { + // given + final var cloudServerBookingItemEntity = HsBookingItemRealEntity.builder() + .type(DOMAIN_SETUP) + .project(project) + .caption("Test-Domain") + .resources(Map.ofEntries( + entry("domainName", "org") + )) + .build(); + + // when + final var result = HsBookingItemEntityValidatorRegistry.doValidate(em, cloudServerBookingItemEntity); + + // then + assertThat(result).containsExactly("'D-12345:Test-Project:Test-Domain.resources.domainName' = 'org' is not a (non-top-level) fully qualified domain name"); + } + + @ParameterizedTest + @ValueSource(strings = { + "co.uk", "org.uk", "gov.uk", "ac.uk", "sch.uk", + "com.au", "net.au", "org.au", "edu.au", "gov.au", "asn.au", "id.au", + "co.jp", "ne.jp", "or.jp", "ac.jp", "go.jp", + "com.cn", "net.cn", "org.cn", "gov.cn", "edu.cn", "ac.cn", + "com.br", "net.br", "org.br", "gov.br", "edu.br", "mil.br", "art.br", + "co.in", "net.in", "org.in", "gen.in", "firm.in", "ind.in", + "com.mx", "net.mx", "org.mx", "gob.mx", "edu.mx", + "gov.it", "edu.it", + "co.nz", "net.nz", "org.nz", "govt.nz", "ac.nz", "school.nz", "geek.nz", "kiwi.nz", + "co.kr", "ne.kr", "or.kr", "go.kr", "re.kr", "pe.kr" + }) + void reject2ndLevelRegistrarDomain(final String secondLevelRegistrarDomain) { + // given + final var cloudServerBookingItemEntity = HsBookingItemRealEntity.builder() + .type(DOMAIN_SETUP) + .project(project) + .caption("Test-Domain") + .resources(Map.ofEntries( + entry("domainName", secondLevelRegistrarDomain) + )) + .build(); + + // when + final var result = HsBookingItemEntityValidatorRegistry.doValidate(em, cloudServerBookingItemEntity); + + // then + assertThat(result).containsExactly( + "'D-12345:Test-Project:Test-Domain.resources.domainName' = '" + + secondLevelRegistrarDomain + + "' is a forbidden registrar-level domain name"); + } + + @Test + void containsAllValidations() { + // when + final var validator = HsBookingItemEntityValidatorRegistry.forType(CLOUD_SERVER); + + // then + assertThat(validator.properties()).map(Map::toString).containsExactlyInAnyOrder( + "{type=boolean, propertyName=active, defaultValue=true}", + "{type=integer, propertyName=CPU, min=1, max=32, required=true}", + "{type=integer, propertyName=RAM, unit=GB, min=1, max=8192, required=true}", + "{type=integer, propertyName=SSD, unit=GB, min=25, max=1000, step=25, requiresAtLeastOneOf=[SDD, HDD]}", + "{type=integer, propertyName=HDD, unit=GB, min=250, max=4000, step=250, requiresAtLeastOneOf=[SSD, HDD]}", + "{type=integer, propertyName=Traffic, unit=GB, min=250, max=10000, step=250, requiresAtMaxOneOf=[Bandwidth, Traffic]}", + "{type=integer, propertyName=Bandwidth, unit=GB, min=250, max=10000, step=250, requiresAtMaxOneOf=[Bandwidth, Traffic]}", + "{type=enumeration, propertyName=SLA-Infrastructure, values=[BASIC, EXT8H, EXT4H, EXT2H]}"); + } +} diff --git a/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsDomainHttpSetupHostingAssetValidatorUnitTest.java b/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsDomainHttpSetupHostingAssetValidatorUnitTest.java index 4705a99e..91fecdd5 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsDomainHttpSetupHostingAssetValidatorUnitTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsDomainHttpSetupHostingAssetValidatorUnitTest.java @@ -156,9 +156,9 @@ class HsDomainHttpSetupHostingAssetValidatorUnitTest { // then assertThat(result).containsExactlyInAnyOrder( "'DOMAIN_HTTP_SETUP:example.org|HTTP.config.htdocsfallback' is expected to be of type Boolean, but is of type String", - "'DOMAIN_HTTP_SETUP:example.org|HTTP.config.fcgi-php-bin' is expected to match any of [^/.*] but 'false' does not match", - "'DOMAIN_HTTP_SETUP:example.org|HTTP.config.subdomains' is expected to match any of [(\\*|(?!-)[A-Za-z0-9-]{1,63}(?