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 38234c07..3d62b765 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 @@ -20,6 +20,7 @@ class HsDomainSetupBookingItemValidator extends HsBookingItemEntityValidator { HsDomainSetupBookingItemValidator() { super( stringProperty(DOMAIN_NAME_PROPERTY_NAME).writeOnce() + .maxLength(253) .matchesRegEx(FQDN_REGEX).describedAs("is not a (non-top-level) fully qualified domain name") .notMatchesRegEx(REGISTRAR_LEVEL_DOMAINS).describedAs("is a forbidden registrar-level domain name") .required(), @@ -34,7 +35,7 @@ class HsDomainSetupBookingItemValidator extends HsBookingItemEntityValidator { final var violations = new ArrayList(); final var domainName = bookingItem.getDirectValue(DOMAIN_NAME_PROPERTY_NAME, String.class); if (!bookingItem.isLoaded() && - domainName.matches("hostsharing.(com|net|org|coop)")) { + domainName.matches("hostsharing.(com|net|org|coop|de)")) { violations.add("'" + bookingItem.toShortString() + ".resources." + DOMAIN_NAME_PROPERTY_NAME + "' = '" + domainName + "' is a forbidden Hostsharing domain name"); } diff --git a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/Dns.java b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/Dns.java index d3a3bb3a..037b95c0 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/Dns.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/Dns.java @@ -49,16 +49,25 @@ public class Dns { return Optional.empty(); } - public static boolean isRegistrarLevel(final String domainName) { + public static boolean isRegistrarLevelDomain(final String domainName) { return stream(REGISTRAR_LEVEL_DOMAIN_PATTERN) .anyMatch(p -> p.matcher(domainName).matches()); } + /** + * @param domainName a fully qualified domain name + * @return true if `domainName` can be registered at a registrar, false if it's a subdomain of such or a registrar-level domain itself + */ + public static boolean isRegistrableDomain(final String domainName) { + return !isRegistrarLevelDomain(domainName) && + superDomain(domainName).map(Dns::isRegistrarLevelDomain).orElse(false); + } + public static void fakeResultForDomain(final String domainName, final Result fakeResult) { fakeResults.put(domainName, fakeResult); } - static void resetFakeResults() { + public static void resetFakeResults() { fakeResults.clear(); } diff --git a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsDomainSetupHostingAssetValidator.java b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsDomainSetupHostingAssetValidator.java index 392eba16..40530ad1 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsDomainSetupHostingAssetValidator.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsDomainSetupHostingAssetValidator.java @@ -2,7 +2,6 @@ package net.hostsharing.hsadminng.hs.hosting.asset.validators; import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAsset; -import java.util.ArrayList; import java.util.List; import java.util.Optional; import java.util.function.Supplier; @@ -27,8 +26,12 @@ class HsDomainSetupHostingAssetValidator extends HostingAssetEntityValidator { @Override public List validateEntity(final HsHostingAsset assetEntity) { + final var violations = // new ArrayList(); + super.validateEntity(assetEntity); + if (!violations.isEmpty()) { + return violations; + } - final var violations = new ArrayList(); final var domainName = assetEntity.getIdentifier(); final var dnsResult = new Dns(domainName).fetchRecordsOfType("TXT"); final Supplier getCode = () -> assetEntity.getBookingItem().getDirectValue("verificationCode", String.class); @@ -50,10 +53,8 @@ class HsDomainSetupHostingAssetValidator extends HostingAssetEntityValidator { } case Dns.Status.NAME_NOT_FOUND: { - final var superDomain = superDomain(domainName); - final var verificationRequired = !superDomain.map(Dns::isRegistrarLevel).orElse(false) - && assetEntity.getBookingItem() != null; // FIXME: or getParentAsset() == nuĺl? or extract method - if (verificationRequired) { + if (isDnsVerificationRequiredForUnregisteredDomain(assetEntity)) { + final var superDomain = superDomain(domainName); final var expectedTxtRecordValue = "Hostsharing-domain-setup-verification-code=" + getCode.get(); final var verificationFoundInSuperDomain = superDomain.flatMap(superDomainName -> findTxtRecord( new Dns(superDomainName).fetchRecordsOfType("TXT"), @@ -77,17 +78,9 @@ class HsDomainSetupHostingAssetValidator extends HostingAssetEntityValidator { violations.add("[DNS] lookup failed for domain name '" + assetEntity.getIdentifier() + "': " + dnsResult.exception()); break; } - - violations.addAll(super.validateEntity(assetEntity)); return violations; } - private static Optional findTxtRecord(final Dns.Result result, final String expectedTxtRecordValue) { - return result.records().stream() - .filter(r -> r.contains(expectedTxtRecordValue)) - .findAny(); - } - @Override protected Pattern identifierPattern(final HsHostingAsset assetEntity) { if (assetEntity.getBookingItem() != null) { @@ -98,4 +91,16 @@ class HsDomainSetupHostingAssetValidator extends HostingAssetEntityValidator { final var parentDomainName = assetEntity.getParentAsset().getIdentifier(); return Pattern.compile(SUBDOMAIN_NAME_REGEX + "\\." + parentDomainName.replace(".", "\\."), Pattern.CASE_INSENSITIVE); } + + private static boolean isDnsVerificationRequiredForUnregisteredDomain(final HsHostingAsset assetEntity) { + return !Dns.isRegistrableDomain(assetEntity.getIdentifier()) + && assetEntity.getParentAsset() == null; + } + + + private static Optional findTxtRecord(final Dns.Result result, final String expectedTxtRecordValue) { + return result.records().stream() + .filter(r -> r.contains(expectedTxtRecordValue)) + .findAny(); + } } 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 index 352c71de..9fbdac45 100644 --- 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 @@ -12,10 +12,12 @@ import java.util.Map; import static java.util.Map.entry; import static net.hostsharing.hsadminng.hs.booking.item.HsBookingItemType.DOMAIN_SETUP; +import static org.apache.commons.lang3.StringUtils.right; import static org.assertj.core.api.Assertions.assertThat; class HsDomainSetupBookingItemValidatorUnitTest { + public static final String TOO_LONG_DOMAIN_NAME = "asdfghijklmnopqrstuvwxyz0123456789.".repeat(8) + "example.org"; final HsBookingDebitorEntity debitor = HsBookingDebitorEntity.builder() .debitorNumber(12345) .build(); @@ -44,6 +46,42 @@ class HsDomainSetupBookingItemValidatorUnitTest { assertThat(result).isEmpty(); } + @Test + void acceptsMaximumDomainNameLength() { + final var domainSetupBookingItemEntity = HsBookingItemRealEntity.builder() + .type(DOMAIN_SETUP) + .project(project) + .caption("Test-Domain") + .resources(Map.ofEntries( + entry("domainName", right(TOO_LONG_DOMAIN_NAME, 253)) + )) + .build(); + + // when + final var result = HsBookingItemEntityValidatorRegistry.doValidate(em, domainSetupBookingItemEntity); + + // then + assertThat(result).isEmpty(); + } + + @Test + void rejectsTooLongTotalName() { + final var domainSetupBookingItemEntity = HsBookingItemRealEntity.builder() + .type(DOMAIN_SETUP) + .project(project) + .caption("Test-Domain") + .resources(Map.ofEntries( + entry("domainName", right(TOO_LONG_DOMAIN_NAME, 254)) + )) + .build(); + + // when + final var result = HsBookingItemEntityValidatorRegistry.doValidate(em, domainSetupBookingItemEntity); + + // then + assertThat(result).contains("'D-12345:Test-Project:Test-Domain.resources.domainName' length is expected to be at max 253 but length of 'dfghijklmnopqrstuvwxyz0123456789.asdfghijklmnopqrstuvwxyz0123456789.asdfghijklmnopqrstuvwxyz0123456789.asdfghijklmnopqrstuvwxyz0123456789.asdfghijklmnopqrstuvwxyz0123456789.asdfghijklmnopqrstuvwxyz0123456789.asdfghijklmnopqrstuvwxyz0123456789.example.org' is 254"); + } + @ParameterizedTest @ValueSource(strings = { "de", "com", "net", "org", "actually-any-top-level-domain", @@ -81,7 +119,7 @@ class HsDomainSetupBookingItemValidatorUnitTest { @ParameterizedTest @ValueSource(strings = { - "hostsharing.net", "hostsharing.org", "hostsharing.com", "hostsharing.coop" + "hostsharing.net", "hostsharing.org", "hostsharing.com", "hostsharing.coop", "hostsharing.de" }) void rejectHostsharingDomain(final String secondLevelRegistrarDomain) { // given @@ -111,7 +149,7 @@ class HsDomainSetupBookingItemValidatorUnitTest { // then assertThat(validator.properties()).map(Map::toString).containsExactlyInAnyOrder( - "{type=string, propertyName=domainName, matchesRegEx=[^((?!-)[A-Za-z0-9-]{1,63}(? bib.resources(new HashMap<>(ofEntries( - entry("domainName", "example.org") - ))).build() - ).build(); - fakeValidDnsVerification(givenEntity); + final var givenEntity = validEntityBuilder(testCase.domainName) + .bookingItem(null) + .parentAsset(HsHostingAssetRealEntity.builder().type(DOMAIN_SETUP).identifier("example.org").build()) + .build(); + // fakeValidDnsVerification(givenEntity); final var validator = HostingAssetEntityValidatorRegistry.forType(givenEntity.getType()); // when @@ -90,26 +90,26 @@ class HsDomainSetupHostingAssetValidatorUnitTest { // then assertThat(result).contains( - "'identifier' expected to match 'example.org', but is '" + testCase.domainName + "'" + "'identifier' expected to match '(\\*|(?!-)[A-Za-z0-9-]{1,63}(?