From c9437d8c72c44fcd31e8583c64b21f887a452837 Mon Sep 17 00:00:00 2001 From: Ben McIlwain Date: Thu, 20 Jun 2024 11:22:38 -0400 Subject: [PATCH] Make registrant nullable on domains (#2477) This is the first step in migrating to the minimum registration data set. Note that our database model already permits null domain registrants, so this just makes the code accept it as well. Note that I haven't changed any requirements in EPP flows yet; a later step will be to check the migration schedule and then not require the registrant to be present if in a suitable state. This does potentially affect the output of WHOIS/RDAP, but that's a NOOP so long as EPP commands and other tools continue to enforce the requirement of a registrant. --- .../google/registry/beam/rde/RdePipeline.java | 2 +- .../flows/domain/DomainFlowUtils.java | 19 ++-- .../registry/flows/domain/DomainInfoFlow.java | 7 +- .../flows/domain/DomainUpdateFlow.java | 7 +- .../registry/model/domain/DomainBase.java | 16 ++-- .../registry/model/domain/DomainCommand.java | 13 +-- .../registry/model/domain/DomainInfoData.java | 1 + .../registry/rdap/RdapEntityAction.java | 2 +- .../registry/rdap/RdapEntitySearchAction.java | 2 +- .../registry/rdap/RdapJsonFormatter.java | 66 +++++++++---- .../registry/rde/DomainToXjcConverter.java | 12 +-- .../ui/server/console/ConsoleApiAction.java | 1 - .../console/ConsoleUpdateRegistrarAction.java | 4 +- .../registry/whois/DomainWhoisResponse.java | 4 +- .../beam/common/RegistryJpaReadTest.java | 3 +- .../registry/beam/rde/RdePipelineTest.java | 96 +++++++++++++++---- .../beam/spec11/Spec11PipelineTest.java | 3 +- .../flows/domain/DomainDeleteFlowTest.java | 6 +- .../flows/domain/DomainInfoFlowTest.java | 10 +- .../flows/domain/DomainUpdateFlowTest.java | 16 ++-- .../registry/model/domain/DomainSqlTest.java | 5 +- .../registry/model/domain/DomainTest.java | 22 +++-- .../reporting/Spec11ThreatMatchTest.java | 3 +- .../registry/rdap/RdapDomainActionTest.java | 14 +++ .../registry/rdap/RdapJsonFormatterTest.java | 26 +++-- .../rde/DomainToXjcConverterTest.java | 12 ++- .../java/google/registry/rde/RdeFixtures.java | 6 +- .../registry/testing/DatabaseHelper.java | 4 +- .../testing/FullFieldsTestEntityHelper.java | 3 +- .../whois/DomainWhoisResponseTest.java | 18 +++- .../domain_info_response_no_registrant.xml | 37 +++++++ .../whois/whois_domain_no_registrant.txt | 53 ++++++++++ 32 files changed, 377 insertions(+), 116 deletions(-) create mode 100644 core/src/test/resources/google/registry/flows/domain/domain_info_response_no_registrant.xml create mode 100644 core/src/test/resources/google/registry/whois/whois_domain_no_registrant.txt diff --git a/core/src/main/java/google/registry/beam/rde/RdePipeline.java b/core/src/main/java/google/registry/beam/rde/RdePipeline.java index 9a5983395..a44d030ea 100644 --- a/core/src/main/java/google/registry/beam/rde/RdePipeline.java +++ b/core/src/main/java/google/registry/beam/rde/RdePipeline.java @@ -468,7 +468,7 @@ public class RdePipeline implements Serializable { HashSet contacts = new HashSet<>(); contacts.add(domain.getAdminContact().getKey()); contacts.add(domain.getTechContact().getKey()); - contacts.add(domain.getRegistrant().getKey()); + domain.getRegistrant().ifPresent(r -> contacts.add(r.getKey())); // Billing contact is not mandatory. if (domain.getBillingContact() != null) { contacts.add(domain.getBillingContact().getKey()); diff --git a/core/src/main/java/google/registry/flows/domain/DomainFlowUtils.java b/core/src/main/java/google/registry/flows/domain/DomainFlowUtils.java index ba3d2373d..f7c9c135d 100644 --- a/core/src/main/java/google/registry/flows/domain/DomainFlowUtils.java +++ b/core/src/main/java/google/registry/flows/domain/DomainFlowUtils.java @@ -412,11 +412,13 @@ public class DomainFlowUtils { /** Verify that no linked resources have disallowed statuses. */ static void verifyNotInPendingDelete( - Set contacts, VKey registrant, Set> nameservers) + Set contacts, + Optional> registrant, + Set> nameservers) throws EppException { ImmutableList.Builder> keysToLoad = new ImmutableList.Builder<>(); contacts.stream().map(DesignatedContact::getContactKey).forEach(keysToLoad::add); - Optional.ofNullable(registrant).ifPresent(keysToLoad::add); + registrant.ifPresent(keysToLoad::add); keysToLoad.addAll(nameservers); verifyNotInPendingDelete(EppResource.loadCached(keysToLoad.build()).values()); } @@ -480,9 +482,10 @@ public class DomainFlowUtils { } static void validateRequiredContactsPresent( - @Nullable VKey registrant, Set contacts) + Optional> registrant, Set contacts) throws RequiredParameterMissingException { - if (registrant == null) { + // TODO: Check minimum reg data set migration schedule here and don't throw when any are empty. + if (registrant.isEmpty()) { throw new MissingRegistrantException(); } @@ -498,14 +501,14 @@ public class DomainFlowUtils { } } - static void validateRegistrantAllowedOnTld(String tld, String registrantContactId) + static void validateRegistrantAllowedOnTld(String tld, Optional registrantContactId) throws RegistrantNotAllowedException { ImmutableSet allowedRegistrants = Tld.get(tld).getAllowedRegistrantContactIds(); // Empty allow list or null registrantContactId are ignored. - if (registrantContactId != null + if (registrantContactId.isPresent() && !allowedRegistrants.isEmpty() - && !allowedRegistrants.contains(registrantContactId)) { - throw new RegistrantNotAllowedException(registrantContactId); + && !allowedRegistrants.contains(registrantContactId.get())) { + throw new RegistrantNotAllowedException(registrantContactId.get()); } } diff --git a/core/src/main/java/google/registry/flows/domain/DomainInfoFlow.java b/core/src/main/java/google/registry/flows/domain/DomainInfoFlow.java index 8b52185cf..e4640e6d4 100644 --- a/core/src/main/java/google/registry/flows/domain/DomainInfoFlow.java +++ b/core/src/main/java/google/registry/flows/domain/DomainInfoFlow.java @@ -119,8 +119,11 @@ public final class DomainInfoFlow implements TransactionalFlow { .setCreationTime(domain.getCreationTime()) .setLastEppUpdateTime(domain.getLastEppUpdateTime()) .setRegistrationExpirationTime(domain.getRegistrationExpirationTime()) - .setLastTransferTime(domain.getLastTransferTime()) - .setRegistrant(tm().loadByKey(domain.getRegistrant()).getContactId()); + .setLastTransferTime(domain.getLastTransferTime()); + domain + .getRegistrant() + .ifPresent(r -> infoBuilder.setRegistrant(tm().loadByKey(r).getContactId())); + // If authInfo is non-null, then the caller is authorized to see the full information since we // will have already verified the authInfo is valid. if (registrarId.equals(domain.getCurrentSponsorRegistrarId()) || authInfo.isPresent()) { diff --git a/core/src/main/java/google/registry/flows/domain/DomainUpdateFlow.java b/core/src/main/java/google/registry/flows/domain/DomainUpdateFlow.java index f12dda3e3..2d66e54a6 100644 --- a/core/src/main/java/google/registry/flows/domain/DomainUpdateFlow.java +++ b/core/src/main/java/google/registry/flows/domain/DomainUpdateFlow.java @@ -278,7 +278,7 @@ public final class DomainUpdateFlow implements MutatingFlow { .removeStatusValues(remove.getStatusValues()) .removeContacts(remove.getContacts()) .addContacts(add.getContacts()) - .setRegistrant(firstNonNull(change.getRegistrant(), domain.getRegistrant())) + .setRegistrant(change.getRegistrant().or(domain::getRegistrant)) .setAuthInfo(firstNonNull(change.getAuthInfo(), domain.getAuthInfo())); if (!add.getNameservers().isEmpty()) { @@ -301,7 +301,10 @@ public final class DomainUpdateFlow implements MutatingFlow { } private static void validateRegistrantIsntBeingRemoved(Change change) throws EppException { - if (change.getRegistrantContactId() != null && change.getRegistrantContactId().isEmpty()) { + // TODO(mcilwain): Make this check the minimum registration data set migration schedule + // and not require presence of a registrant in later stages. + if (change.getRegistrantContactId().isPresent() + && change.getRegistrantContactId().get().isEmpty()) { throw new MissingRegistrantException(); } } diff --git a/core/src/main/java/google/registry/model/domain/DomainBase.java b/core/src/main/java/google/registry/model/domain/DomainBase.java index 83ce948f8..721463b66 100644 --- a/core/src/main/java/google/registry/model/domain/DomainBase.java +++ b/core/src/main/java/google/registry/model/domain/DomainBase.java @@ -135,7 +135,7 @@ public class DomainBase extends EppResource @Expose VKey billingContact; @Expose VKey techContact; - @Expose VKey registrantContact; + @Expose @Nullable VKey registrantContact; /** Authorization info (aka transfer secret) of the domain. */ @Embedded @@ -585,8 +585,8 @@ public class DomainBase extends EppResource } /** A key to the registrant who registered this domain. */ - public VKey getRegistrant() { - return registrantContact; + public Optional> getRegistrant() { + return Optional.ofNullable(registrantContact); } public VKey getAdminContact() { @@ -606,6 +606,11 @@ public class DomainBase extends EppResource return getAllContacts(false); } + /** Gets all associated contacts for the domain, including the registrant. */ + public ImmutableSet getAllContacts() { + return getAllContacts(true); + } + public DomainAuthInfo getAuthInfo() { return authInfo; } @@ -717,7 +722,6 @@ public class DomainBase extends EppResource instance.autorenewEndTime = firstNonNull(getInstance().autorenewEndTime, END_OF_TIME); checkArgumentNotNull(emptyToNull(instance.domainName), "Missing domainName"); - checkArgumentNotNull(instance.getRegistrant(), "Missing registrant"); instance.tld = getTldFromDomainName(instance.domainName); T newDomain = super.build(); @@ -749,9 +753,9 @@ public class DomainBase extends EppResource return thisCastToDerived(); } - public B setRegistrant(VKey registrant) { + public B setRegistrant(Optional> registrant) { // Set the registrant field specifically. - getInstance().registrantContact = registrant; + getInstance().registrantContact = registrant.orElse(null); return thisCastToDerived(); } diff --git a/core/src/main/java/google/registry/model/domain/DomainCommand.java b/core/src/main/java/google/registry/model/domain/DomainCommand.java index 02be7109a..d9b02fbf2 100644 --- a/core/src/main/java/google/registry/model/domain/DomainCommand.java +++ b/core/src/main/java/google/registry/model/domain/DomainCommand.java @@ -40,6 +40,7 @@ import google.registry.model.eppinput.ResourceCommand.ResourceUpdate; import google.registry.model.eppinput.ResourceCommand.SingleResourceCommand; import google.registry.model.host.Host; import google.registry.persistence.VKey; +import java.util.Optional; import java.util.Set; import javax.annotation.Nullable; import javax.xml.bind.annotation.XmlAttribute; @@ -76,21 +77,21 @@ public class DomainCommand { /** The contactId of the registrant who registered this domain. */ @XmlElement(name = "registrant") + @Nullable String registrantContactId; /** A resolved key to the registrant who registered this domain. */ - @XmlTransient VKey registrant; + @Nullable @XmlTransient VKey registrant; /** Authorization info (aka transfer secret) of the domain. */ DomainAuthInfo authInfo; - public String getRegistrantContactId() { - return registrantContactId; + public Optional getRegistrantContactId() { + return Optional.ofNullable(registrantContactId); } - @Nullable - public VKey getRegistrant() { - return registrant; + public Optional> getRegistrant() { + return Optional.ofNullable(registrant); } public DomainAuthInfo getAuthInfo() { diff --git a/core/src/main/java/google/registry/model/domain/DomainInfoData.java b/core/src/main/java/google/registry/model/domain/DomainInfoData.java index 7ba90e76f..4b1417c9b 100644 --- a/core/src/main/java/google/registry/model/domain/DomainInfoData.java +++ b/core/src/main/java/google/registry/model/domain/DomainInfoData.java @@ -63,6 +63,7 @@ public abstract class DomainInfoData implements ResponseData { abstract ImmutableSet getStatusValues(); @XmlElement(name = "registrant") + @Nullable abstract String getRegistrant(); @XmlElement(name = "contact") diff --git a/core/src/main/java/google/registry/rdap/RdapEntityAction.java b/core/src/main/java/google/registry/rdap/RdapEntityAction.java index f4c24adb6..2a24ca90b 100644 --- a/core/src/main/java/google/registry/rdap/RdapEntityAction.java +++ b/core/src/main/java/google/registry/rdap/RdapEntityAction.java @@ -76,7 +76,7 @@ public class RdapEntityAction extends RdapActionBase { // they are global, and might have different roles for different domains. if (contact.isPresent() && isAuthorized(contact.get())) { return rdapJsonFormatter.createRdapContactEntity( - contact.get(), ImmutableSet.of(), OutputDataType.FULL); + contact, ImmutableSet.of(), OutputDataType.FULL); } } diff --git a/core/src/main/java/google/registry/rdap/RdapEntitySearchAction.java b/core/src/main/java/google/registry/rdap/RdapEntitySearchAction.java index 7314a37d1..e338df6ec 100644 --- a/core/src/main/java/google/registry/rdap/RdapEntitySearchAction.java +++ b/core/src/main/java/google/registry/rdap/RdapEntitySearchAction.java @@ -461,7 +461,7 @@ public class RdapEntitySearchAction extends RdapSearchActionBase { .entitySearchResultsBuilder() .add( rdapJsonFormatter.createRdapContactEntity( - contact, ImmutableSet.of(), outputDataType)); + Optional.of(contact), ImmutableSet.of(), outputDataType)); newCursor = Optional.of( CONTACT_CURSOR_PREFIX diff --git a/core/src/main/java/google/registry/rdap/RdapJsonFormatter.java b/core/src/main/java/google/registry/rdap/RdapJsonFormatter.java index 1aa027ec1..ef7fcbcf6 100644 --- a/core/src/main/java/google/registry/rdap/RdapJsonFormatter.java +++ b/core/src/main/java/google/registry/rdap/RdapJsonFormatter.java @@ -61,6 +61,7 @@ import google.registry.rdap.RdapDataStructures.RdapStatus; import google.registry.rdap.RdapObjectClasses.RdapContactEntity; import google.registry.rdap.RdapObjectClasses.RdapDomain; import google.registry.rdap.RdapObjectClasses.RdapEntity; +import google.registry.rdap.RdapObjectClasses.RdapEntity.Role; import google.registry.rdap.RdapObjectClasses.RdapNameserver; import google.registry.rdap.RdapObjectClasses.RdapRegistrarEntity; import google.registry.rdap.RdapObjectClasses.SecureDns; @@ -74,6 +75,7 @@ import java.net.InetAddress; import java.net.URI; import java.nio.file.Paths; import java.util.HashMap; +import java.util.LinkedHashSet; import java.util.Locale; import java.util.Optional; import java.util.Set; @@ -249,6 +251,16 @@ public class RdapJsonFormatter { private static final Ordering DESIGNATED_CONTACT_ORDERING = Ordering.natural().onResultOf(DesignatedContact::getType); + /** + * The list of RDAP contact roles that are required to be present on each domain. + * + *

Per RDAP Response Profile 2.7.3, A domain MUST have the REGISTRANT, ADMIN, TECH roles and + * MAY have others. We also have the BILLING role in our system but it isn't required and is only + * listed if actually present. + */ + private static final ImmutableSet REQUIRED_CONTACT_ROLES = + ImmutableSet.of(Role.REGISTRANT, Role.ADMIN, Role.TECH); + /** Creates the TOS notice that is added to every reply. */ Notice createTosNotice() { String linkValue = makeRdapServletRelativeUrl("help", RdapHelpAction.TOS_PATH); @@ -371,36 +383,48 @@ public class RdapJsonFormatter { // Load the registrant and other contacts and add them to the data. ImmutableMap, Contact> loadedContacts = replicaTm().transact(() -> replicaTm().loadByKeysIfPresent(domain.getReferencedContacts())); - // RDAP Response Profile 2.7.3, A domain MUST have the REGISTRANT, ADMIN, TECH roles and MAY - // have others. We also add the BILLING. - // + // RDAP Response Profile 2.7.1, 2.7.3 - we MUST have the contacts. 2.7.4 discusses redaction of // fields we don't want to show (as opposed to not having contacts at all) because of GDPR etc. // - // the GDPR redaction is handled in createRdapContactEntity + // The GDPR redaction is handled in createRdapContactEntity. + + // Load all contacts that are present and group them by type (it is common for a single contact + // entity to be used across multiple contact types on domain, e.g. registrant and admin). ImmutableSetMultimap, Type> contactsToRoles = - Streams.concat( - domain.getContacts().stream(), - Stream.of(DesignatedContact.create(Type.REGISTRANT, domain.getRegistrant()))) + domain.getAllContacts().stream() .sorted(DESIGNATED_CONTACT_ORDERING) .collect( toImmutableSetMultimap( DesignatedContact::getContactKey, DesignatedContact::getType)); + // Convert the contact entities to RDAP output contacts (this also converts the contact types + // to RDAP roles). + Set rdapContacts = new LinkedHashSet<>(); for (VKey contactKey : contactsToRoles.keySet()) { - Set roles = + Set roles = contactsToRoles.get(contactKey).stream() .map(RdapJsonFormatter::convertContactTypeToRdapRole) .collect(toImmutableSet()); if (roles.isEmpty()) { continue; } - builder - .entitiesBuilder() - .add( - createRdapContactEntity( - loadedContacts.get(contactKey), roles, OutputDataType.INTERNAL)); + rdapContacts.add( + createRdapContactEntity( + Optional.ofNullable(loadedContacts.get(contactKey)), roles, OutputDataType.INTERNAL)); } + + // Loop through all required contact roles and fill in placeholder REDACTED info for any + // required ones that are missing, i.e. because of minimum registration data set. + for (Role role : REQUIRED_CONTACT_ROLES) { + if (rdapContacts.stream().noneMatch(c -> c.roles().contains(role))) { + rdapContacts.add( + createRdapContactEntity( + Optional.empty(), ImmutableSet.of(role), OutputDataType.INTERNAL)); + } + } + builder.entitiesBuilder().addAll(rdapContacts); + // Add the nameservers to the data; the load was kicked off above for efficiency. // RDAP Response Profile 2.9: we MUST have the nameservers for (Host host : HOST_RESOURCE_ORDERING.immutableSortedCopy(loadedHosts)) { @@ -502,25 +526,35 @@ public class RdapJsonFormatter { /** * Creates a JSON object for a {@link Contact} and associated contact type. * + *

If the contact isn't present (i.e. because of minimum registration data set), then always + * show all of its fields as if they were redacted, and always deny RDAP authorization. + * * @param contact the contact resource object from which the JSON object should be created * @param roles the roles of this contact * @param outputDataType whether to generate full or summary data */ RdapContactEntity createRdapContactEntity( - Contact contact, Iterable roles, OutputDataType outputDataType) { + Optional contact, Iterable roles, OutputDataType outputDataType) { RdapContactEntity.Builder contactBuilder = RdapContactEntity.builder(); // RDAP Response Profile 2.7.1, 2.7.3 - we MUST have the contacts. 2.7.4 discusses censoring of // fields we don't want to show (as opposed to not having contacts at all) because of GDPR etc. // // 2.8 allows for unredacted output for authorized people. + // TODO(mcilwain): Once the RDAP profile is fully updated for minimum registration data set, + // we will want to not include non-existent contacts at all, rather than + // pretending they exist and just showing REDACTED info. This is especially + // important for authorized flows, where you wouldn't expect to see redaction. boolean isAuthorized = - rdapAuthorization.isAuthorizedForRegistrar(contact.getCurrentSponsorRegistrarId()); + contact.isPresent() + && rdapAuthorization.isAuthorizedForRegistrar( + contact.get().getCurrentSponsorRegistrarId()); VcardArray.Builder vcardBuilder = VcardArray.builder(); if (isAuthorized) { - fillRdapContactEntityWhenAuthorized(contactBuilder, vcardBuilder, contact, outputDataType); + fillRdapContactEntityWhenAuthorized( + contactBuilder, vcardBuilder, contact.get(), outputDataType); } else { // GTLD Registration Data Temp Spec 17may18, Appendix A, 2.3, 2.4 and RDAP Response Profile // 2.7.4.1, 2.7.4.2 - the following fields must be redacted: diff --git a/core/src/main/java/google/registry/rde/DomainToXjcConverter.java b/core/src/main/java/google/registry/rde/DomainToXjcConverter.java index ae5a91d58..fa7167b63 100644 --- a/core/src/main/java/google/registry/rde/DomainToXjcConverter.java +++ b/core/src/main/java/google/registry/rde/DomainToXjcConverter.java @@ -20,7 +20,6 @@ import static google.registry.persistence.transaction.TransactionManagerFactory. import com.google.common.base.Ascii; import com.google.common.base.Strings; import com.google.common.collect.ImmutableSet; -import com.google.common.flogger.FluentLogger; import google.registry.model.contact.Contact; import google.registry.model.domain.DesignatedContact; import google.registry.model.domain.Domain; @@ -45,12 +44,11 @@ import google.registry.xjc.rgp.XjcRgpStatusType; import google.registry.xjc.rgp.XjcRgpStatusValueType; import google.registry.xjc.secdns.XjcSecdnsDsDataType; import google.registry.xjc.secdns.XjcSecdnsDsOrKeyType; +import java.util.Optional; /** Utility class that turns {@link Domain} as {@link XjcRdeDomainElement}. */ final class DomainToXjcConverter { - private static final FluentLogger logger = FluentLogger.forEnclosingClass(); - /** Converts {@link Domain} to {@link XjcRdeDomainElement}. */ static XjcRdeDomainElement convert(Domain domain, RdeMode mode) { return new XjcRdeDomainElement(convertDomain(domain, mode)); @@ -168,11 +166,9 @@ final class DomainToXjcConverter { // o An OPTIONAL element that contain the identifier for // the human or organizational social information object associated // as the holder of the domain name object. - VKey registrant = model.getRegistrant(); - if (registrant == null) { - logger.atWarning().log("Domain %s has no registrant contact.", domainName); - } else { - Contact registrantContact = tm().transact(() -> tm().loadByKey(registrant)); + Optional> registrant = model.getRegistrant(); + if (registrant.isPresent()) { + Contact registrantContact = tm().transact(() -> tm().loadByKey(registrant.get())); checkState( registrantContact != null, "Registrant contact %s on domain %s does not exist", diff --git a/core/src/main/java/google/registry/ui/server/console/ConsoleApiAction.java b/core/src/main/java/google/registry/ui/server/console/ConsoleApiAction.java index 63fba82b3..d4d3a3bc6 100644 --- a/core/src/main/java/google/registry/ui/server/console/ConsoleApiAction.java +++ b/core/src/main/java/google/registry/ui/server/console/ConsoleApiAction.java @@ -254,5 +254,4 @@ public abstract class ConsoleApiAction implements Runnable { super(message); } } - } diff --git a/core/src/main/java/google/registry/ui/server/console/ConsoleUpdateRegistrarAction.java b/core/src/main/java/google/registry/ui/server/console/ConsoleUpdateRegistrarAction.java index b94b6b374..56bca601f 100644 --- a/core/src/main/java/google/registry/ui/server/console/ConsoleUpdateRegistrarAction.java +++ b/core/src/main/java/google/registry/ui/server/console/ConsoleUpdateRegistrarAction.java @@ -47,8 +47,7 @@ public class ConsoleUpdateRegistrarAction extends ConsoleApiAction { @Inject ConsoleUpdateRegistrarAction( - ConsoleApiParams consoleApiParams, - @Parameter("registrar") Optional registrar) { + ConsoleApiParams consoleApiParams, @Parameter("registrar") Optional registrar) { super(consoleApiParams); this.registrar = registrar; } @@ -108,5 +107,4 @@ public class ConsoleUpdateRegistrarAction extends ConsoleApiAction { consoleApiParams.response().setStatus(SC_OK); } - } diff --git a/core/src/main/java/google/registry/whois/DomainWhoisResponse.java b/core/src/main/java/google/registry/whois/DomainWhoisResponse.java index b275ed50c..6fc033e67 100644 --- a/core/src/main/java/google/registry/whois/DomainWhoisResponse.java +++ b/core/src/main/java/google/registry/whois/DomainWhoisResponse.java @@ -105,7 +105,9 @@ final class DomainWhoisResponse extends WhoisResponseImpl { "Registrar Abuse Contact Phone", abuseContact.map(RegistrarPoc::getPhoneNumber).orElse("")) .emitStatusValues(domain.getStatusValues(), domain.getGracePeriods()) - .emitContact("Registrant", Optional.of(domain.getRegistrant()), preferUnicode) + // TODO(mcilwain): Investigate if the WHOIS spec requires us to always output REDACTED + // text in WHOIS even if the contact is not present, and if so, do so. + .emitContact("Registrant", domain.getRegistrant(), preferUnicode) .emitContact("Admin", getContactReference(Type.ADMIN), preferUnicode) .emitContact("Tech", getContactReference(Type.TECH), preferUnicode) .emitContact("Billing", getContactReference(Type.BILLING), preferUnicode) diff --git a/core/src/test/java/google/registry/beam/common/RegistryJpaReadTest.java b/core/src/test/java/google/registry/beam/common/RegistryJpaReadTest.java index c5d67d080..e47700104 100644 --- a/core/src/test/java/google/registry/beam/common/RegistryJpaReadTest.java +++ b/core/src/test/java/google/registry/beam/common/RegistryJpaReadTest.java @@ -43,6 +43,7 @@ import google.registry.persistence.transaction.CriteriaQueryBuilder; import google.registry.persistence.transaction.JpaTestExtensions; import google.registry.persistence.transaction.JpaTestExtensions.JpaIntegrationTestExtension; import google.registry.testing.FakeClock; +import java.util.Optional; import org.apache.beam.sdk.coders.StringUtf8Coder; import org.apache.beam.sdk.testing.PAssert; import org.apache.beam.sdk.values.PCollection; @@ -191,7 +192,7 @@ public class RegistryJpaReadTest { StatusValue.SERVER_UPDATE_PROHIBITED, StatusValue.SERVER_RENEW_PROHIBITED, StatusValue.SERVER_HOLD)) - .setRegistrant(contact.createVKey()) + .setRegistrant(Optional.of(contact.createVKey())) .setContacts(ImmutableSet.of()) .setSubordinateHosts(ImmutableSet.of("ns1.example.com")) .setPersistedCurrentSponsorRegistrarId(registrar.getRegistrarId()) diff --git a/core/src/test/java/google/registry/beam/rde/RdePipelineTest.java b/core/src/test/java/google/registry/beam/rde/RdePipelineTest.java index 99ded6601..1744a2005 100644 --- a/core/src/test/java/google/registry/beam/rde/RdePipelineTest.java +++ b/core/src/test/java/google/registry/beam/rde/RdePipelineTest.java @@ -31,6 +31,7 @@ import static google.registry.rde.RdeResourceType.HOST; import static google.registry.rde.RdeResourceType.REGISTRAR; import static google.registry.testing.DatabaseHelper.createTld; import static google.registry.testing.DatabaseHelper.insertSimpleResources; +import static google.registry.testing.DatabaseHelper.newDomain; import static google.registry.testing.DatabaseHelper.persistActiveContact; import static google.registry.testing.DatabaseHelper.persistActiveDomain; import static google.registry.testing.DatabaseHelper.persistActiveHost; @@ -83,10 +84,10 @@ import google.registry.rde.PendingDeposit; import google.registry.rde.RdeResourceType; import google.registry.testing.CloudTasksHelper; import google.registry.testing.CloudTasksHelper.TaskMatcher; -import google.registry.testing.DatabaseHelper; import google.registry.testing.FakeClock; import google.registry.testing.FakeKeyringModule; import java.io.IOException; +import java.util.Optional; import java.util.function.Function; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -266,23 +267,23 @@ public class RdePipelineTest { persistHostHistory(host1); Domain helloDomain = persistEppResource( - DatabaseHelper.newDomain("hello.soy", contact1) - .asBuilder() - .addNameserver(host1.createVKey()) - .build()); + newDomain("hello.soy", contact1).asBuilder().addNameserver(host1.createVKey()).build()); persistDomainHistory(helloDomain); persistHostHistory(persistActiveHost("not-used-subordinate.hello.soy")); Host host2 = persistActiveHost("ns1.hello.soy"); persistHostHistory(host2); + + // This domain has no registrant. Domain kittyDomain = persistEppResource( - DatabaseHelper.newDomain("kitty.fun", contact2) + newDomain("kitty.fun", contact2) .asBuilder() .addNameservers(ImmutableSet.of(host1.createVKey(), host2.createVKey())) + .setRegistrant(Optional.empty()) .build()); persistDomainHistory(kittyDomain); // Should not appear because the TLD is not included in a pending deposit. - persistDomainHistory(persistEppResource(DatabaseHelper.newDomain("lol.cat", contact1))); + persistDomainHistory(persistEppResource(newDomain("lol.cat", contact1))); // To be deleted. Domain deletedDomain = persistActiveDomain("deleted.soy"); persistDomainHistory(deletedDomain); @@ -325,7 +326,7 @@ public class RdePipelineTest { persistHostHistory(futureHost); persistDomainHistory( persistEppResource( - DatabaseHelper.newDomain("future.soy", futureContact) + newDomain("future.soy", futureContact) .asBuilder() .setNameservers(futureHost.createVKey()) .build())); @@ -379,18 +380,30 @@ public class RdePipelineTest { // The same registrars are attached to all the pending deposits. .containsExactly("New Registrar", "The Registrar", "external_monitoring"); // Domain fragments. - if ("soy".equals(kv.getKey().tld())) { - assertThat( - getFragmentForType(kv, DOMAIN) - .map(getXmlElement(DOMAIN_NAME_PATTERN)) - .collect(toImmutableSet())) - .containsExactly("hello.soy"); - } else { - assertThat( - getFragmentForType(kv, DOMAIN) - .map(getXmlElement(DOMAIN_NAME_PATTERN)) - .collect(toImmutableSet())) - .containsExactly("cat.fun"); + ImmutableSet domainFrags = + getFragmentForType(kv, DOMAIN).collect(toImmutableSet()); + assertThat(domainFrags).hasSize(1); + if ("fun".equals(kv.getKey().tld())) { + // Note that this fragment contains no registrant (which is valid). + assertThat(domainFrags.stream().findFirst().get().xml().strip()) + .isEqualTo( + """ + + cat.fun + 15-FUN + cat.fun + + contact456 + contact456 + + ns1.external.tld + ns1.hello.soy + + TheRegistrar + TheRegistrar + 1970-01-01T00:00:00Z + 294247-01-10T04:00:54Z + """); } if (kv.getKey().mode().equals(FULL)) { // Contact fragments for hello.soy. @@ -400,12 +413,35 @@ public class RdePipelineTest { .map(getXmlElement(CONTACT_ID_PATTERN)) .collect(toImmutableSet())) .containsExactly("contact1234", "contact789"); + // Host fragments for hello.soy. assertThat( getFragmentForType(kv, HOST) .map(getXmlElement(HOST_NAME_PATTERN)) .collect(toImmutableSet())) .containsExactly("ns1.external.tld", "ns1.lol.cat"); + + // Domain fragments for hello.soy: Note that this contains a registrant. + assertThat(domainFrags.stream().findFirst().get().xml().strip()) + .isEqualTo( + """ + + hello.soy + E-SOY + hello.soy + + contact1234 + contact789 + contact1234 + + ns1.external.tld + ns1.lol.cat + + TheRegistrar + TheRegistrar + 1970-01-01T00:00:00Z + 294247-01-10T04:00:54Z + """); } else { // Contact fragments for cat.fun. assertThat( @@ -413,6 +449,7 @@ public class RdePipelineTest { .map(getXmlElement(CONTACT_ID_PATTERN)) .collect(toImmutableSet())) .containsExactly("contactABC"); + // Host fragments for cat.soy. assertThat( getFragmentForType(kv, HOST) @@ -429,6 +466,25 @@ public class RdePipelineTest { fragment.type().equals(CONTACT) || fragment.type().equals(HOST))) .isFalse(); + + // Domain fragments for hello.soy: Note that this contains no contact info. + assertThat(domainFrags.stream().findFirst().get().xml().strip()) + .isEqualTo( + """ + + hello.soy + E-SOY + hello.soy + + + ns1.external.tld + ns1.lol.cat + + TheRegistrar + TheRegistrar + 1970-01-01T00:00:00Z + 294247-01-10T04:00:54Z + """); } }); return null; diff --git a/core/src/test/java/google/registry/beam/spec11/Spec11PipelineTest.java b/core/src/test/java/google/registry/beam/spec11/Spec11PipelineTest.java index ee34b7807..b73a93919 100644 --- a/core/src/test/java/google/registry/beam/spec11/Spec11PipelineTest.java +++ b/core/src/test/java/google/registry/beam/spec11/Spec11PipelineTest.java @@ -54,6 +54,7 @@ import google.registry.util.Retrier; import java.io.File; import java.nio.file.Files; import java.nio.file.Path; +import java.util.Optional; import org.apache.beam.sdk.coders.KvCoder; import org.apache.beam.sdk.coders.SerializableCoder; import org.apache.beam.sdk.options.PipelineOptionsFactory; @@ -297,7 +298,7 @@ class Spec11PipelineTest { .setLastEppUpdateTime(fakeClock.nowUtc()) .setLastEppUpdateRegistrarId(registrar.getRegistrarId()) .setLastTransferTime(fakeClock.nowUtc()) - .setRegistrant(contact.createVKey()) + .setRegistrant(Optional.of(contact.createVKey())) .setPersistedCurrentSponsorRegistrarId(registrar.getRegistrarId()) .setRegistrationExpirationTime(fakeClock.nowUtc().plusYears(1)) .setAuthInfo(DomainAuthInfo.create(PasswordAuth.create("password"))) diff --git a/core/src/test/java/google/registry/flows/domain/DomainDeleteFlowTest.java b/core/src/test/java/google/registry/flows/domain/DomainDeleteFlowTest.java index 1c39f2d36..fcd167f22 100644 --- a/core/src/test/java/google/registry/flows/domain/DomainDeleteFlowTest.java +++ b/core/src/test/java/google/registry/flows/domain/DomainDeleteFlowTest.java @@ -103,6 +103,7 @@ import google.registry.model.transfer.TransferStatus; import google.registry.testing.CloudTasksHelper.TaskMatcher; import google.registry.testing.DatabaseHelper; import java.util.Map; +import java.util.Optional; import org.joda.money.Money; import org.joda.time.DateTime; import org.joda.time.Duration; @@ -162,7 +163,7 @@ class DomainDeleteFlowTest extends ResourceFlowTestCase { .setLastEppUpdateTime(DateTime.parse("1999-12-03T09:00:00.0Z")) .setLastTransferTime(DateTime.parse("2000-04-08T09:00:00.0Z")) .setRegistrationExpirationTime(DateTime.parse("2005-04-03T22:00:00.0Z")) - .setRegistrant(registrant.createVKey()) + .setRegistrant(Optional.of(registrant.createVKey())) .setContacts( ImmutableSet.of( DesignatedContact.create(Type.ADMIN, contact.createVKey()), @@ -227,6 +228,13 @@ class DomainInfoFlowTest extends ResourceFlowTestCase { doSuccessfulTest("domain_info_response.xml"); } + @Test + void testSuccess_noRegistrant() throws Exception { + persistTestEntities(false); + domain = persistResource(domain.asBuilder().setRegistrant(Optional.empty()).build()); + doSuccessfulTest("domain_info_response_no_registrant.xml", false); + } + @Test void testSuccess_clTridNotSpecified() throws Exception { setEppInput("domain_info_no_cltrid.xml"); diff --git a/core/src/test/java/google/registry/flows/domain/DomainUpdateFlowTest.java b/core/src/test/java/google/registry/flows/domain/DomainUpdateFlowTest.java index 8583682fd..bfb61e6d5 100644 --- a/core/src/test/java/google/registry/flows/domain/DomainUpdateFlowTest.java +++ b/core/src/test/java/google/registry/flows/domain/DomainUpdateFlowTest.java @@ -162,7 +162,7 @@ class DomainUpdateFlowTest extends ResourceFlowTestCase assertThat(loadByKey(contact.getContactKey()).getContactId()).isEqualTo("mak21")); - assertThat(loadByKey(reloadResourceByForeignKey().getRegistrant()).getContactId()) + assertThat(loadByKey(reloadResourceByForeignKey().getRegistrant().get()).getContactId()) .isEqualTo("mak21"); runFlow(); @@ -1615,7 +1615,7 @@ class DomainUpdateFlowTest extends ResourceFlowTestCase assertThat(loadByKey(contact.getContactKey()).getContactId()).isEqualTo("sh8013")); - assertThat(loadByKey(reloadResourceByForeignKey().getRegistrant()).getContactId()) + assertThat(loadByKey(reloadResourceByForeignKey().getRegistrant().get()).getContactId()) .isEqualTo("sh8013"); } diff --git a/core/src/test/java/google/registry/model/domain/DomainSqlTest.java b/core/src/test/java/google/registry/model/domain/DomainSqlTest.java index 66396a037..fef1474ab 100644 --- a/core/src/test/java/google/registry/model/domain/DomainSqlTest.java +++ b/core/src/test/java/google/registry/model/domain/DomainSqlTest.java @@ -54,6 +54,7 @@ import google.registry.persistence.transaction.JpaTestExtensions.JpaIntegrationW import google.registry.testing.FakeClock; import google.registry.util.SerializeUtils; import java.util.Arrays; +import java.util.Optional; import org.joda.time.DateTime; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -69,7 +70,7 @@ public class DomainSqlTest { new JpaTestExtensions.Builder().withClock(fakeClock).buildIntegrationWithCoverageExtension(); private Domain domain; - private VKey contactKey; + private Optional> contactKey; private VKey contact2Key; private VKey host1VKey; private Host host; @@ -82,7 +83,7 @@ public class DomainSqlTest { saveRegistrar("registrar1"); saveRegistrar("registrar2"); saveRegistrar("registrar3"); - contactKey = createKey(Contact.class, "contact_id1"); + contactKey = Optional.of(createKey(Contact.class, "contact_id1")); contact2Key = createKey(Contact.class, "contact_id2"); host1VKey = createKey(Host.class, "host1"); diff --git a/core/src/test/java/google/registry/model/domain/DomainTest.java b/core/src/test/java/google/registry/model/domain/DomainTest.java index 81842b542..1307bffb1 100644 --- a/core/src/test/java/google/registry/model/domain/DomainTest.java +++ b/core/src/test/java/google/registry/model/domain/DomainTest.java @@ -185,7 +185,7 @@ public class DomainTest { StatusValue.SERVER_UPDATE_PROHIBITED, StatusValue.SERVER_RENEW_PROHIBITED, StatusValue.SERVER_HOLD)) - .setRegistrant(contact1Key) + .setRegistrant(Optional.of(contact1Key)) .setNameservers(ImmutableSet.of(hostKey)) .setSubordinateHosts(ImmutableSet.of("ns1.example.com")) .setPersistedCurrentSponsorRegistrarId("NewRegistrar") @@ -242,6 +242,16 @@ public class DomainTest { .hasValue(domain); } + @Test + void testRegistrantNotRequired() { + persistResource(domain.asBuilder().setRegistrant(Optional.empty()).build()); + assertThat( + loadByForeignKey(Domain.class, domain.getForeignKey(), fakeClock.nowUtc()) + .get() + .getRegistrant()) + .isEmpty(); + } + @Test void testEmptyStringsBecomeNull() { assertThat( @@ -1012,14 +1022,14 @@ public class DomainTest { DesignatedContact.create(Type.BILLING, contact3Key), DesignatedContact.create(Type.TECH, contact4Key)), true); - assertThat(domain.getRegistrant()).isEqualTo(contact1Key); + assertThat(domain.getRegistrant()).hasValue(contact1Key); assertThat(domain.getAdminContact()).isEqualTo(contact2Key); assertThat(domain.getBillingContact()).isEqualTo(contact3Key); assertThat(domain.getTechContact()).isEqualTo(contact4Key); // Make sure everything gets nulled out. domain.setContactFields(ImmutableSet.of(), true); - assertThat(domain.getRegistrant()).isNull(); + assertThat(domain.getRegistrant()).isEmpty(); assertThat(domain.getAdminContact()).isNull(); assertThat(domain.getBillingContact()).isNull(); assertThat(domain.getTechContact()).isNull(); @@ -1032,13 +1042,13 @@ public class DomainTest { DesignatedContact.create(Type.BILLING, contact3Key), DesignatedContact.create(Type.TECH, contact4Key)), false); - assertThat(domain.getRegistrant()).isNull(); + assertThat(domain.getRegistrant()).isEmpty(); assertThat(domain.getAdminContact()).isEqualTo(contact2Key); assertThat(domain.getBillingContact()).isEqualTo(contact3Key); assertThat(domain.getTechContact()).isEqualTo(contact4Key); - domain = domain.asBuilder().setRegistrant(contact1Key).build(); + domain = domain.asBuilder().setRegistrant(Optional.of(contact1Key)).build(); domain.setContactFields(ImmutableSet.of(), false); - assertThat(domain.getRegistrant()).isEqualTo(contact1Key); + assertThat(domain.getRegistrant()).hasValue(contact1Key); assertThat(domain.getAdminContact()).isNull(); assertThat(domain.getBillingContact()).isNull(); assertThat(domain.getTechContact()).isNull(); diff --git a/core/src/test/java/google/registry/model/reporting/Spec11ThreatMatchTest.java b/core/src/test/java/google/registry/model/reporting/Spec11ThreatMatchTest.java index f4d90268c..fdc16488b 100644 --- a/core/src/test/java/google/registry/model/reporting/Spec11ThreatMatchTest.java +++ b/core/src/test/java/google/registry/model/reporting/Spec11ThreatMatchTest.java @@ -31,6 +31,7 @@ import google.registry.model.domain.Domain; import google.registry.model.host.Host; import google.registry.model.transfer.ContactTransferData; import google.registry.persistence.VKey; +import java.util.Optional; import org.joda.time.LocalDate; import org.joda.time.format.ISODateTimeFormat; import org.junit.jupiter.api.BeforeEach; @@ -68,7 +69,7 @@ public final class Spec11ThreatMatchTest extends EntityTestCase { .setDomainName("foo.tld") .setRepoId(domainRepoId) .setNameservers(hostVKey) - .setRegistrant(registrantContactVKey) + .setRegistrant(Optional.of(registrantContactVKey)) .setContacts(ImmutableSet.of()) .build(); diff --git a/core/src/test/java/google/registry/rdap/RdapDomainActionTest.java b/core/src/test/java/google/registry/rdap/RdapDomainActionTest.java index 1972d73b3..b834fb0a4 100644 --- a/core/src/test/java/google/registry/rdap/RdapDomainActionTest.java +++ b/core/src/test/java/google/registry/rdap/RdapDomainActionTest.java @@ -15,6 +15,7 @@ package google.registry.rdap; import static com.google.common.truth.Truth.assertThat; +import static google.registry.model.EppResourceUtils.loadByForeignKey; import static google.registry.testing.DatabaseHelper.createTld; import static google.registry.testing.DatabaseHelper.persistResource; import static google.registry.testing.DatabaseHelper.persistSimpleResources; @@ -278,6 +279,19 @@ class RdapDomainActionTest extends RdapActionBaseTestCase { assertProperResponseForCatLol("cat.lol", "rdap_domain_no_contacts_with_remark.json"); } + @Test + void testValidDomain_notLoggedIn_contactsShowRedacted_evenWhenRegistrantDoesntExist() { + // Even though the registrant is empty on this domain, it still shows a full set of REDACTED + // fields through RDAP. + persistResource( + loadByForeignKey(Domain.class, "cat.lol", clock.nowUtc()) + .get() + .asBuilder() + .setRegistrant(Optional.empty()) + .build()); + assertProperResponseForCatLol("cat.lol", "rdap_domain_no_contacts_with_remark.json"); + } + @Test void testValidDomain_loggedInAsOtherRegistrar_noContacts() { login("idnregistrar"); diff --git a/core/src/test/java/google/registry/rdap/RdapJsonFormatterTest.java b/core/src/test/java/google/registry/rdap/RdapJsonFormatterTest.java index acca40c8c..54431185d 100644 --- a/core/src/test/java/google/registry/rdap/RdapJsonFormatterTest.java +++ b/core/src/test/java/google/registry/rdap/RdapJsonFormatterTest.java @@ -52,6 +52,8 @@ import google.registry.rdap.RdapObjectClasses.ReplyPayloadBase; import google.registry.rdap.RdapObjectClasses.TopLevelReplyObject; import google.registry.testing.FakeClock; import google.registry.testing.FullFieldsTestEntityHelper; +import java.util.Optional; +import javax.annotation.Nullable; import org.joda.time.DateTime; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -77,7 +79,7 @@ class RdapJsonFormatterTest { private Host hostNoAddresses; private Host hostNotLinked; private Host hostSuperordinatePendingTransfer; - private Contact contactRegistrant; + @Nullable private Contact contactRegistrant; private Contact contactAdmin; private Contact contactTech; private Contact contactNotLinked; @@ -371,7 +373,7 @@ class RdapJsonFormatterTest { .that( rdapJsonFormatter .createRdapContactEntity( - contactRegistrant, + Optional.of(contactRegistrant), ImmutableSet.of(RdapEntity.Role.REGISTRANT), OutputDataType.FULL) .toJson()) @@ -384,7 +386,7 @@ class RdapJsonFormatterTest { .that( rdapJsonFormatter .createRdapContactEntity( - contactRegistrant, + Optional.of(contactRegistrant), ImmutableSet.of(RdapEntity.Role.REGISTRANT), OutputDataType.SUMMARY) .toJson()) @@ -398,7 +400,7 @@ class RdapJsonFormatterTest { .that( rdapJsonFormatter .createRdapContactEntity( - contactRegistrant, + Optional.of(contactRegistrant), ImmutableSet.of(RdapEntity.Role.REGISTRANT), OutputDataType.FULL) .toJson()) @@ -418,7 +420,7 @@ class RdapJsonFormatterTest { .that( rdapJsonFormatter .createRdapContactEntity( - contactRegistrant, + Optional.of(contactRegistrant), ImmutableSet.of(RdapEntity.Role.REGISTRANT), OutputDataType.FULL) .toJson()) @@ -431,7 +433,9 @@ class RdapJsonFormatterTest { .that( rdapJsonFormatter .createRdapContactEntity( - contactAdmin, ImmutableSet.of(RdapEntity.Role.ADMIN), OutputDataType.FULL) + Optional.of(contactAdmin), + ImmutableSet.of(RdapEntity.Role.ADMIN), + OutputDataType.FULL) .toJson()) .isEqualTo(loadJson("rdapjson_admincontact.json")); } @@ -442,7 +446,9 @@ class RdapJsonFormatterTest { .that( rdapJsonFormatter .createRdapContactEntity( - contactTech, ImmutableSet.of(RdapEntity.Role.TECH), OutputDataType.FULL) + Optional.of(contactTech), + ImmutableSet.of(RdapEntity.Role.TECH), + OutputDataType.FULL) .toJson()) .isEqualTo(loadJson("rdapjson_techcontact.json")); } @@ -452,7 +458,8 @@ class RdapJsonFormatterTest { assertAboutJson() .that( rdapJsonFormatter - .createRdapContactEntity(contactTech, ImmutableSet.of(), OutputDataType.FULL) + .createRdapContactEntity( + Optional.of(contactTech), ImmutableSet.of(), OutputDataType.FULL) .toJson()) .isEqualTo(loadJson("rdapjson_rolelesscontact.json")); } @@ -462,7 +469,8 @@ class RdapJsonFormatterTest { assertAboutJson() .that( rdapJsonFormatter - .createRdapContactEntity(contactNotLinked, ImmutableSet.of(), OutputDataType.FULL) + .createRdapContactEntity( + Optional.of(contactNotLinked), ImmutableSet.of(), OutputDataType.FULL) .toJson()) .isEqualTo(loadJson("rdapjson_unlinkedcontact.json")); } diff --git a/core/src/test/java/google/registry/rde/DomainToXjcConverterTest.java b/core/src/test/java/google/registry/rde/DomainToXjcConverterTest.java index e8e6eb53f..620453331 100644 --- a/core/src/test/java/google/registry/rde/DomainToXjcConverterTest.java +++ b/core/src/test/java/google/registry/rde/DomainToXjcConverterTest.java @@ -70,6 +70,7 @@ import google.registry.xjc.rdedomain.XjcRdeDomainElement; import google.registry.xjc.rgp.XjcRgpStatusType; import google.registry.xjc.secdns.XjcSecdnsDsDataType; import java.io.ByteArrayOutputStream; +import java.util.Optional; import org.joda.money.Money; import org.joda.time.DateTime; import org.junit.jupiter.api.BeforeEach; @@ -280,9 +281,14 @@ public class DomainToXjcConverterTest { makeHost(clock, "4-Q9JYB4C", "ns2.cat.みんな", "bad:f00d:cafe::15:beef") .createVKey())) .setRegistrant( - makeContact( - clock, "12-Q9JYB4C", "5372808-ERL", "(◕‿◕) nevermore", "prophet@evil.みんな") - .createVKey()) + Optional.of( + makeContact( + clock, + "12-Q9JYB4C", + "5372808-ERL", + "(◕‿◕) nevermore", + "prophet@evil.みんな") + .createVKey())) .setRegistrationExpirationTime(DateTime.parse("1930-01-01T00:00:00Z")) .setGracePeriods( ImmutableSet.of( diff --git a/core/src/test/java/google/registry/rde/RdeFixtures.java b/core/src/test/java/google/registry/rde/RdeFixtures.java index 5b2c62ff6..27d0469f7 100644 --- a/core/src/test/java/google/registry/rde/RdeFixtures.java +++ b/core/src/test/java/google/registry/rde/RdeFixtures.java @@ -51,6 +51,7 @@ import google.registry.model.transfer.DomainTransferData; import google.registry.model.transfer.TransferStatus; import google.registry.testing.FakeClock; import google.registry.util.Idn; +import java.util.Optional; import org.joda.money.Money; import org.joda.time.DateTime; @@ -63,8 +64,9 @@ final class RdeFixtures { .setDomainName("example." + tld) .setRepoId(generateNewDomainRoid(tld)) .setRegistrant( - makeContact(clock, "5372808-ERL", "(◕‿◕) nevermore", "prophet@evil.みんな") - .createVKey()) + Optional.of( + makeContact(clock, "5372808-ERL", "(◕‿◕) nevermore", "prophet@evil.みんな") + .createVKey())) .build(); DomainHistory historyEntry = persistResource( diff --git a/core/src/test/java/google/registry/testing/DatabaseHelper.java b/core/src/test/java/google/registry/testing/DatabaseHelper.java index c5666a552..3d11ed54c 100644 --- a/core/src/test/java/google/registry/testing/DatabaseHelper.java +++ b/core/src/test/java/google/registry/testing/DatabaseHelper.java @@ -190,7 +190,7 @@ public final class DatabaseHelper { .setPersistedCurrentSponsorRegistrarId("TheRegistrar") .setCreationTimeForTest(START_OF_TIME) .setAuthInfo(DomainAuthInfo.create(PasswordAuth.create("2fooBAR"))) - .setRegistrant(contactKey) + .setRegistrant(Optional.of(contactKey)) .setContacts( ImmutableSet.of( DesignatedContact.create(Type.ADMIN, contactKey), @@ -603,7 +603,7 @@ public final class DatabaseHelper { .setCreationRegistrarId("TheRegistrar") .setCreationTimeForTest(creationTime) .setRegistrationExpirationTime(expirationTime) - .setRegistrant(contact.createVKey()) + .setRegistrant(Optional.of(contact.createVKey())) .setContacts( ImmutableSet.of( DesignatedContact.create(Type.ADMIN, contact.createVKey()), diff --git a/core/src/test/java/google/registry/testing/FullFieldsTestEntityHelper.java b/core/src/test/java/google/registry/testing/FullFieldsTestEntityHelper.java index 096ec693c..973eb424d 100644 --- a/core/src/test/java/google/registry/testing/FullFieldsTestEntityHelper.java +++ b/core/src/test/java/google/registry/testing/FullFieldsTestEntityHelper.java @@ -46,6 +46,7 @@ import google.registry.persistence.VKey; import google.registry.util.Idn; import java.net.InetAddress; import java.util.List; +import java.util.Optional; import javax.annotation.Nullable; import org.joda.time.DateTime; @@ -351,7 +352,7 @@ public final class FullFieldsTestEntityHelper { StatusValue.SERVER_UPDATE_PROHIBITED)) .setDsData(ImmutableSet.of(DomainDsData.create(1, 2, 3, "deadface"))); if (registrant != null) { - builder.setRegistrant(registrant.createVKey()); + builder.setRegistrant(Optional.of(registrant.createVKey())); } if ((admin != null) || (tech != null)) { ImmutableSet.Builder contactsBuilder = new ImmutableSet.Builder<>(); diff --git a/core/src/test/java/google/registry/whois/DomainWhoisResponseTest.java b/core/src/test/java/google/registry/whois/DomainWhoisResponseTest.java index b8654a76d..54f334fe1 100644 --- a/core/src/test/java/google/registry/whois/DomainWhoisResponseTest.java +++ b/core/src/test/java/google/registry/whois/DomainWhoisResponseTest.java @@ -41,6 +41,7 @@ import google.registry.persistence.transaction.JpaTestExtensions; import google.registry.persistence.transaction.JpaTestExtensions.JpaIntegrationTestExtension; import google.registry.testing.FakeClock; import google.registry.whois.WhoisResponse.WhoisResponseResults; +import java.util.Optional; import org.joda.time.DateTime; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -264,7 +265,7 @@ class DomainWhoisResponseTest { StatusValue.CLIENT_RENEW_PROHIBITED, StatusValue.CLIENT_TRANSFER_PROHIBITED, StatusValue.SERVER_UPDATE_PROHIBITED)) - .setRegistrant(registrantResourceKey) + .setRegistrant(Optional.of(registrantResourceKey)) .setContacts( ImmutableSet.of( DesignatedContact.create(DesignatedContact.Type.ADMIN, adminResourceKey), @@ -291,6 +292,21 @@ class DomainWhoisResponseTest { .isEqualTo(WhoisResponseResults.create(loadFile("whois_domain.txt"), 1)); } + @Test + void getPlainTextOutputTest_noRegistrant() { + DomainWhoisResponse domainWhoisResponse = + new DomainWhoisResponse( + domain.asBuilder().setRegistrant(Optional.empty()).build(), + false, + "Please contact registrar", + clock.nowUtc()); + assertThat( + domainWhoisResponse.getResponse( + false, + "Doodle Disclaimer\nI exist so that carriage return\nin disclaimer can be tested.")) + .isEqualTo(WhoisResponseResults.create(loadFile("whois_domain_no_registrant.txt"), 1)); + } + @Test void getPlainTextOutputTest_registrarAbuseInfoMissing() { persistResource(abuseContact.asBuilder().setVisibleInDomainWhoisAsAbuse(false).build()); diff --git a/core/src/test/resources/google/registry/flows/domain/domain_info_response_no_registrant.xml b/core/src/test/resources/google/registry/flows/domain/domain_info_response_no_registrant.xml new file mode 100644 index 000000000..a596e58f7 --- /dev/null +++ b/core/src/test/resources/google/registry/flows/domain/domain_info_response_no_registrant.xml @@ -0,0 +1,37 @@ + + + + Command completed successfully + + + + example.tld + %ROID% + + sh8013 + sh8013 + + ns1.example.tld + ns1.example.net + + ns1.example.tld + ns2.example.tld + NewRegistrar + TheRegistrar + 1999-04-03T22:00:00.0Z + NewRegistrar + 1999-12-03T09:00:00.0Z + 2005-04-03T22:00:00.0Z + 2000-04-08T09:00:00.0Z + + 2fooBAR + + + + + ABC-12345 + server-trid + + + diff --git a/core/src/test/resources/google/registry/whois/whois_domain_no_registrant.txt b/core/src/test/resources/google/registry/whois/whois_domain_no_registrant.txt new file mode 100644 index 000000000..9b2568642 --- /dev/null +++ b/core/src/test/resources/google/registry/whois/whois_domain_no_registrant.txt @@ -0,0 +1,53 @@ +Domain Name: example.tld +Registry Domain ID: 3-TLD +Registrar WHOIS Server: whois.nic.fakewhois.example +Registrar URL: http://my.fake.url +Updated Date: 2009-05-29T20:13:00Z +Creation Date: 2000-10-08T00:45:00Z +Registry Expiry Date: 2010-10-08T00:44:59Z +Registrar: New Registrar +Registrar IANA ID: 5555555 +Registrar Abuse Contact Email: jakedoe@theregistrar.com +Registrar Abuse Contact Phone: +1.2125551216 +Domain Status: addPeriod https://icann.org/epp#addPeriod +Domain Status: clientDeleteProhibited https://icann.org/epp#clientDeleteProhibited +Domain Status: clientRenewProhibited https://icann.org/epp#clientRenewProhibited +Domain Status: clientTransferProhibited https://icann.org/epp#clientTransferProhibited +Domain Status: serverUpdateProhibited https://icann.org/epp#serverUpdateProhibited +Domain Status: transferPeriod https://icann.org/epp#transferPeriod +Registry Admin ID: REDACTED FOR PRIVACY +Admin Name: REDACTED FOR PRIVACY +Admin Organization: REDACTED FOR PRIVACY +Admin Street: REDACTED FOR PRIVACY +Admin City: REDACTED FOR PRIVACY +Admin State/Province: REDACTED FOR PRIVACY +Admin Postal Code: REDACTED FOR PRIVACY +Admin Country: REDACTED FOR PRIVACY +Admin Phone: REDACTED FOR PRIVACY +Admin Phone Ext: REDACTED FOR PRIVACY +Admin Fax: REDACTED FOR PRIVACY +Admin Email: Please contact registrar +Registry Tech ID: REDACTED FOR PRIVACY +Tech Name: REDACTED FOR PRIVACY +Tech Organization: REDACTED FOR PRIVACY +Tech Street: REDACTED FOR PRIVACY +Tech City: REDACTED FOR PRIVACY +Tech State/Province: REDACTED FOR PRIVACY +Tech Postal Code: REDACTED FOR PRIVACY +Tech Country: REDACTED FOR PRIVACY +Tech Phone: REDACTED FOR PRIVACY +Tech Phone Ext: REDACTED FOR PRIVACY +Tech Fax: REDACTED FOR PRIVACY +Tech Fax Ext: REDACTED FOR PRIVACY +Tech Email: Please contact registrar +Name Server: ns01.exampleregistrar.tld +Name Server: ns02.exampleregistrar.tld +DNSSEC: signedDelegation +URL of the ICANN Whois Inaccuracy Complaint Form: https://www.icann.org/wicf/ +>>> Last update of WHOIS database: 2009-05-29T20:15:00Z <<< + +For more information on Whois status codes, please visit https://icann.org/epp + +Doodle Disclaimer +I exist so that carriage return +in disclaimer can be tested.