From 514d24ed678b9f9b0b04cb7603e3d9a1dda1039a Mon Sep 17 00:00:00 2001 From: Ben McIlwain Date: Mon, 14 Jul 2025 08:29:14 -0700 Subject: [PATCH] Implement the contacts prohibited feature flag for minimum data set (#2781) This prohibits all contact data on create and update EPP flows for both domain and contact flows. It also refactors how default values on FeatureFlags work, as it's safer to specify a single default on the flag itself rather than have to specify it independently at a number of callsites (and potentially end up having an inconsistent value). Domain updates on existing domains that still have contact data will fail unless all contact data is removed, as a forcing function to require registrars to rectify the situation prior to being able to do any other kind of domain changes. Contact-related flows that are still allowed after this point: Updating a domain to remove all contacts from it, and deleting a contact object. --- .../export/ExportDomainListsAction.java | 9 +- .../flows/contact/ContactCreateFlow.java | 7 ++ .../flows/contact/ContactUpdateFlow.java | 7 ++ .../flows/domain/DomainCreateFlow.java | 6 +- .../flows/domain/DomainFlowUtils.java | 62 ++++++++----- .../flows/domain/DomainUpdateFlow.java | 15 ++-- .../ContactsProhibitedException.java | 24 ++++++ .../registry/model/common/FeatureFlag.java | 52 +++++++---- .../registry/tools/CreateDomainCommand.java | 6 +- .../flows/contact/ContactCreateFlowTest.java | 19 ++++ .../flows/contact/ContactUpdateFlowTest.java | 19 ++++ .../flows/domain/DomainCreateFlowTest.java | 81 ++++++++++++++--- .../flows/domain/DomainUpdateFlowTest.java | 86 ++++++++++++++++--- .../model/common/FeatureFlagTest.java | 16 +--- .../tools/CreateDomainCommandTest.java | 16 +++- .../domain/domain_create_no_contacts.xml | 19 ++++ .../domain/domain_create_response_eap_fee.xml | 2 +- .../domain_create_response_premium_eap.xml | 2 +- .../domain_update_remove_all_contacts.xml | 20 +++++ 19 files changed, 374 insertions(+), 94 deletions(-) create mode 100644 core/src/main/java/google/registry/flows/exceptions/ContactsProhibitedException.java create mode 100644 core/src/test/resources/google/registry/flows/domain/domain_create_no_contacts.xml create mode 100644 core/src/test/resources/google/registry/flows/domain/domain_update_remove_all_contacts.xml diff --git a/core/src/main/java/google/registry/export/ExportDomainListsAction.java b/core/src/main/java/google/registry/export/ExportDomainListsAction.java index 88b4712c9..02223c5ed 100644 --- a/core/src/main/java/google/registry/export/ExportDomainListsAction.java +++ b/core/src/main/java/google/registry/export/ExportDomainListsAction.java @@ -15,6 +15,7 @@ package google.registry.export; import static com.google.common.base.Verify.verifyNotNull; +import static google.registry.model.common.FeatureFlag.FeatureName.INCLUDE_PENDING_DELETE_DATE_FOR_DOMAINS; import static google.registry.model.tld.Tlds.getTldsOfType; import static google.registry.persistence.PersistenceModule.TransactionIsolationLevel.TRANSACTION_REPEATABLE_READ; import static google.registry.persistence.transaction.TransactionManagerFactory.replicaTm; @@ -75,7 +76,8 @@ public class ExportDomainListsAction implements Runnable { ON d.repo_id = gp.domain_repo_id WHERE d.tld = :tld AND d.deletion_time > CAST(:now AS timestamptz) - ORDER BY d.domain_name"""; + ORDER BY d.domain_name + """; // This may be a CSV, but it is uses a .txt file extension for back-compatibility static final String REGISTERED_DOMAINS_FILENAME_FORMAT = "registered_domains_%s.txt"; @@ -93,10 +95,7 @@ public class ExportDomainListsAction implements Runnable { logger.atInfo().log("Exporting domain lists for TLDs %s.", realTlds); boolean includeDeletionTimes = - tm().transact( - () -> - FeatureFlag.isActiveNowOrElse( - FeatureFlag.FeatureName.INCLUDE_PENDING_DELETE_DATE_FOR_DOMAINS, false)); + tm().transact(() -> FeatureFlag.isActiveNow(INCLUDE_PENDING_DELETE_DATE_FOR_DOMAINS)); realTlds.forEach( tld -> { List domainsList = diff --git a/core/src/main/java/google/registry/flows/contact/ContactCreateFlow.java b/core/src/main/java/google/registry/flows/contact/ContactCreateFlow.java index a12eca9cc..364ef2f74 100644 --- a/core/src/main/java/google/registry/flows/contact/ContactCreateFlow.java +++ b/core/src/main/java/google/registry/flows/contact/ContactCreateFlow.java @@ -19,6 +19,7 @@ import static google.registry.flows.ResourceFlowUtils.verifyResourceDoesNotExist import static google.registry.flows.contact.ContactFlowUtils.validateAsciiPostalInfo; import static google.registry.flows.contact.ContactFlowUtils.validateContactAgainstPolicy; import static google.registry.model.EppResourceUtils.createRepoId; +import static google.registry.model.common.FeatureFlag.FeatureName.MINIMUM_DATASET_CONTACTS_PROHIBITED; import static google.registry.persistence.transaction.TransactionManagerFactory.tm; import com.google.common.collect.ImmutableSet; @@ -29,8 +30,10 @@ import google.registry.flows.FlowModule.RegistrarId; import google.registry.flows.FlowModule.TargetId; import google.registry.flows.MutatingFlow; import google.registry.flows.annotations.ReportingSpec; +import google.registry.flows.exceptions.ContactsProhibitedException; import google.registry.flows.exceptions.ResourceAlreadyExistsForThisClientException; import google.registry.flows.exceptions.ResourceCreateContentionException; +import google.registry.model.common.FeatureFlag; import google.registry.model.contact.Contact; import google.registry.model.contact.ContactCommand.Create; import google.registry.model.contact.ContactHistory; @@ -47,6 +50,7 @@ import org.joda.time.DateTime; * An EPP flow that creates a new contact. * * @error {@link google.registry.flows.FlowUtils.NotLoggedInException} + * @error {@link ContactsProhibitedException} * @error {@link ResourceAlreadyExistsForThisClientException} * @error {@link ResourceCreateContentionException} * @error {@link ContactFlowUtils.BadInternationalizedPostalInfoException} @@ -69,6 +73,9 @@ public final class ContactCreateFlow implements MutatingFlow { extensionManager.register(MetadataExtension.class); validateRegistrarIsLoggedIn(registrarId); extensionManager.validate(); + if (FeatureFlag.isActiveNow(MINIMUM_DATASET_CONTACTS_PROHIBITED)) { + throw new ContactsProhibitedException(); + } Create command = (Create) resourceCommand; DateTime now = tm().getTransactionTime(); verifyResourceDoesNotExist(Contact.class, targetId, now, registrarId); diff --git a/core/src/main/java/google/registry/flows/contact/ContactUpdateFlow.java b/core/src/main/java/google/registry/flows/contact/ContactUpdateFlow.java index 20050d462..afc3c2070 100644 --- a/core/src/main/java/google/registry/flows/contact/ContactUpdateFlow.java +++ b/core/src/main/java/google/registry/flows/contact/ContactUpdateFlow.java @@ -24,6 +24,7 @@ import static google.registry.flows.ResourceFlowUtils.verifyOptionalAuthInfo; import static google.registry.flows.ResourceFlowUtils.verifyResourceOwnership; import static google.registry.flows.contact.ContactFlowUtils.validateAsciiPostalInfo; import static google.registry.flows.contact.ContactFlowUtils.validateContactAgainstPolicy; +import static google.registry.model.common.FeatureFlag.FeatureName.MINIMUM_DATASET_CONTACTS_PROHIBITED; import static google.registry.model.reporting.HistoryEntry.Type.CONTACT_UPDATE; import static google.registry.persistence.transaction.TransactionManagerFactory.tm; @@ -35,7 +36,9 @@ import google.registry.flows.FlowModule.Superuser; import google.registry.flows.FlowModule.TargetId; import google.registry.flows.MutatingFlow; import google.registry.flows.annotations.ReportingSpec; +import google.registry.flows.exceptions.ContactsProhibitedException; import google.registry.flows.exceptions.ResourceHasClientUpdateProhibitedException; +import google.registry.model.common.FeatureFlag; import google.registry.model.contact.Contact; import google.registry.model.contact.ContactCommand.Update; import google.registry.model.contact.ContactCommand.Update.Change; @@ -55,6 +58,7 @@ import org.joda.time.DateTime; /** * An EPP flow that updates a contact. * + * @error {@link ContactsProhibitedException} * @error {@link google.registry.flows.FlowUtils.NotLoggedInException} * @error {@link google.registry.flows.ResourceFlowUtils.AddRemoveSameValueException} * @error {@link google.registry.flows.ResourceFlowUtils.ResourceDoesNotExistException} @@ -92,6 +96,9 @@ public final class ContactUpdateFlow implements MutatingFlow { extensionManager.register(MetadataExtension.class); validateRegistrarIsLoggedIn(registrarId); extensionManager.validate(); + if (FeatureFlag.isActiveNow(MINIMUM_DATASET_CONTACTS_PROHIBITED)) { + throw new ContactsProhibitedException(); + } Update command = (Update) resourceCommand; DateTime now = tm().getTransactionTime(); Contact existingContact = loadAndVerifyExistence(Contact.class, targetId, now); diff --git a/core/src/main/java/google/registry/flows/domain/DomainCreateFlow.java b/core/src/main/java/google/registry/flows/domain/DomainCreateFlow.java index 088501028..4901ea961 100644 --- a/core/src/main/java/google/registry/flows/domain/DomainCreateFlow.java +++ b/core/src/main/java/google/registry/flows/domain/DomainCreateFlow.java @@ -72,7 +72,9 @@ import google.registry.flows.custom.DomainCreateFlowCustomLogic; import google.registry.flows.custom.DomainCreateFlowCustomLogic.BeforeResponseParameters; import google.registry.flows.custom.DomainCreateFlowCustomLogic.BeforeResponseReturnData; import google.registry.flows.custom.EntityChanges; +import google.registry.flows.domain.DomainFlowUtils.RegistrantProhibitedException; import google.registry.flows.domain.token.AllocationTokenFlowUtils; +import google.registry.flows.exceptions.ContactsProhibitedException; import google.registry.flows.exceptions.ResourceAlreadyExistsForThisClientException; import google.registry.flows.exceptions.ResourceCreateContentionException; import google.registry.model.ImmutableObject; @@ -147,6 +149,7 @@ import org.joda.time.Duration; * @error {@link DomainCreateFlow.NoGeneralRegistrationsInCurrentPhaseException} * @error {@link DomainCreateFlow.NoTrademarkedRegistrationsBeforeSunriseException} * @error {@link BulkDomainRegisteredForTooManyYearsException} + * @error {@link ContactsProhibitedException} * @error {@link DomainCreateFlow.SignedMarksOnlyDuringSunriseException} * @error {@link DomainFlowTmchUtils.NoMarksFoundMatchingDomainException} * @error {@link DomainFlowTmchUtils.FoundMarkNotYetValidException} @@ -194,6 +197,7 @@ import org.joda.time.Duration; * @error {@link DomainFlowUtils.NameserversNotSpecifiedForTldWithNameserverAllowListException} * @error {@link DomainFlowUtils.PremiumNameBlockedException} * @error {@link DomainFlowUtils.RegistrantNotAllowedException} + * @error {@link RegistrantProhibitedException} * @error {@link DomainFlowUtils.RegistrarMustBeActiveForThisOperationException} * @error {@link DomainFlowUtils.TldDoesNotExistException} * @error {@link DomainFlowUtils.TooManyDsRecordsException} @@ -244,7 +248,7 @@ public final class DomainCreateFlow implements MutatingFlow { verifyResourceDoesNotExist(Domain.class, targetId, now, registrarId); // Validate that this is actually a legal domain name on a TLD that the registrar has access to. InternetDomainName domainName = validateDomainName(command.getDomainName()); - String domainLabel = domainName.parts().get(0); + String domainLabel = domainName.parts().getFirst(); Tld tld = Tld.get(domainName.parent().toString()); validateCreateCommandContactsAndNameservers(command, tld, domainName); TldState tldState = tld.getTldState(now); 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 9011dd823..0740dda5e 100644 --- a/core/src/main/java/google/registry/flows/domain/DomainFlowUtils.java +++ b/core/src/main/java/google/registry/flows/domain/DomainFlowUtils.java @@ -25,7 +25,7 @@ import static com.google.common.collect.Sets.intersection; import static com.google.common.collect.Sets.union; import static google.registry.bsa.persistence.BsaLabelUtils.isLabelBlocked; import static google.registry.model.common.FeatureFlag.FeatureName.MINIMUM_DATASET_CONTACTS_OPTIONAL; -import static google.registry.model.common.FeatureFlag.isActiveNow; +import static google.registry.model.common.FeatureFlag.FeatureName.MINIMUM_DATASET_CONTACTS_PROHIBITED; import static google.registry.model.domain.Domain.MAX_REGISTRATION_YEARS; import static google.registry.model.domain.token.AllocationToken.TokenType.REGISTER_BSA; import static google.registry.model.tld.Tld.TldState.GENERAL_AVAILABILITY; @@ -75,11 +75,13 @@ import google.registry.flows.EppException.ParameterValueSyntaxErrorException; import google.registry.flows.EppException.RequiredParameterMissingException; import google.registry.flows.EppException.StatusProhibitsOperationException; import google.registry.flows.EppException.UnimplementedOptionException; +import google.registry.flows.exceptions.ContactsProhibitedException; import google.registry.flows.exceptions.ResourceHasClientUpdateProhibitedException; import google.registry.model.EppResource; import google.registry.model.billing.BillingBase.Flag; import google.registry.model.billing.BillingBase.Reason; import google.registry.model.billing.BillingRecurrence; +import google.registry.model.common.FeatureFlag; import google.registry.model.contact.Contact; import google.registry.model.domain.DesignatedContact; import google.registry.model.domain.DesignatedContact.Type; @@ -478,27 +480,37 @@ public class DomainFlowUtils { } } - static void validateRequiredContactsPresentIfRequiredForDataset( + /** + * Enforces the presence/absence of contact data depending on the minimum data set migration + * schedule. + */ + static void validateContactDataPresence( Optional> registrant, Set contacts) - throws RequiredParameterMissingException { - // TODO(b/353347632): Change this flag check to a registry config check. - if (isActiveNow(MINIMUM_DATASET_CONTACTS_OPTIONAL)) { - // Contacts are not required once we have begun the migration to the minimum dataset - return; - } - if (registrant.isEmpty()) { - throw new MissingRegistrantException(); - } + throws RequiredParameterMissingException, ParameterValuePolicyErrorException { + // TODO(b/353347632): Change these flag checks to a registry config check once minimum data set + // migration is completed. + if (FeatureFlag.isActiveNow(MINIMUM_DATASET_CONTACTS_PROHIBITED)) { + if (registrant.isPresent()) { + throw new RegistrantProhibitedException(); + } + if (!contacts.isEmpty()) { + throw new ContactsProhibitedException(); + } + } else if (!FeatureFlag.isActiveNow(MINIMUM_DATASET_CONTACTS_OPTIONAL)) { + if (registrant.isEmpty()) { + throw new MissingRegistrantException(); + } - Set roles = new HashSet<>(); - for (DesignatedContact contact : contacts) { - roles.add(contact.getType()); - } - if (!roles.contains(Type.ADMIN)) { - throw new MissingAdminContactException(); - } - if (!roles.contains(Type.TECH)) { - throw new MissingTechnicalContactException(); + Set roles = new HashSet<>(); + for (DesignatedContact contact : contacts) { + roles.add(contact.getType()); + } + if (!roles.contains(Type.ADMIN)) { + throw new MissingAdminContactException(); + } + if (!roles.contains(Type.TECH)) { + throw new MissingTechnicalContactException(); + } } } @@ -1042,8 +1054,7 @@ public class DomainFlowUtils { String tldStr = tld.getTldStr(); validateRegistrantAllowedOnTld(tldStr, command.getRegistrantContactId()); validateNoDuplicateContacts(command.getContacts()); - validateRequiredContactsPresentIfRequiredForDataset( - command.getRegistrant(), command.getContacts()); + validateContactDataPresence(command.getRegistrant(), command.getContacts()); ImmutableSet hostNames = command.getNameserverHostNames(); validateNameserversCountForTld(tldStr, domainName, hostNames.size()); validateNameserversAllowedOnTld(tldStr, hostNames); @@ -1367,6 +1378,13 @@ public class DomainFlowUtils { } } + /** Having a registrant is prohibited by registry policy. */ + static class RegistrantProhibitedException extends ParameterValuePolicyErrorException { + public RegistrantProhibitedException() { + super("Having a registrant is prohibited by registry policy"); + } + } + /** Admin contact is required. */ static class MissingAdminContactException extends RequiredParameterMissingException { public MissingAdminContactException() { 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 a3363ef86..c7b3600a4 100644 --- a/core/src/main/java/google/registry/flows/domain/DomainUpdateFlow.java +++ b/core/src/main/java/google/registry/flows/domain/DomainUpdateFlow.java @@ -30,6 +30,7 @@ import static google.registry.flows.ResourceFlowUtils.verifyResourceOwnership; import static google.registry.flows.domain.DomainFlowUtils.checkAllowedAccessToTld; import static google.registry.flows.domain.DomainFlowUtils.cloneAndLinkReferences; import static google.registry.flows.domain.DomainFlowUtils.updateDsData; +import static google.registry.flows.domain.DomainFlowUtils.validateContactDataPresence; import static google.registry.flows.domain.DomainFlowUtils.validateContactsHaveTypes; import static google.registry.flows.domain.DomainFlowUtils.validateDsData; import static google.registry.flows.domain.DomainFlowUtils.validateFeesAckedIfPresent; @@ -37,11 +38,10 @@ import static google.registry.flows.domain.DomainFlowUtils.validateNameserversAl import static google.registry.flows.domain.DomainFlowUtils.validateNameserversCountForTld; import static google.registry.flows.domain.DomainFlowUtils.validateNoDuplicateContacts; import static google.registry.flows.domain.DomainFlowUtils.validateRegistrantAllowedOnTld; -import static google.registry.flows.domain.DomainFlowUtils.validateRequiredContactsPresentIfRequiredForDataset; import static google.registry.flows.domain.DomainFlowUtils.verifyClientUpdateNotProhibited; import static google.registry.flows.domain.DomainFlowUtils.verifyNotInPendingDelete; import static google.registry.model.common.FeatureFlag.FeatureName.MINIMUM_DATASET_CONTACTS_OPTIONAL; -import static google.registry.model.common.FeatureFlag.isActiveNow; +import static google.registry.model.common.FeatureFlag.FeatureName.MINIMUM_DATASET_CONTACTS_PROHIBITED; import static google.registry.model.reporting.HistoryEntry.Type.DOMAIN_UPDATE; import static google.registry.persistence.transaction.TransactionManagerFactory.tm; @@ -64,9 +64,11 @@ import google.registry.flows.custom.DomainUpdateFlowCustomLogic.BeforeSaveParame import google.registry.flows.custom.EntityChanges; import google.registry.flows.domain.DomainFlowUtils.MissingRegistrantException; import google.registry.flows.domain.DomainFlowUtils.NameserversNotSpecifiedForTldWithNameserverAllowListException; +import google.registry.flows.domain.DomainFlowUtils.RegistrantProhibitedException; import google.registry.model.ImmutableObject; import google.registry.model.billing.BillingBase.Reason; import google.registry.model.billing.BillingEvent; +import google.registry.model.common.FeatureFlag; import google.registry.model.contact.Contact; import google.registry.model.domain.DesignatedContact; import google.registry.model.domain.Domain; @@ -130,6 +132,7 @@ import org.joda.time.DateTime; * @error {@link NameserversNotSpecifiedForTldWithNameserverAllowListException} * @error {@link DomainFlowUtils.NotAuthorizedForTldException} * @error {@link DomainFlowUtils.RegistrantNotAllowedException} + * @error {@link RegistrantProhibitedException} * @error {@link DomainFlowUtils.SecDnsAllUsageException} * @error {@link DomainFlowUtils.TooManyDsRecordsException} * @error {@link DomainFlowUtils.TooManyNameserversException} @@ -304,11 +307,12 @@ public final class DomainUpdateFlow implements MutatingFlow { private Optional> determineUpdatedRegistrant(Change change, Domain domain) throws EppException { - // During phase 1 of minimum dataset transition, allow registrant to be removed + // During or after the minimum dataset transition, allow registrant to be removed. if (change.getRegistrantContactId().isPresent() && change.getRegistrantContactId().get().isEmpty()) { // TODO(b/353347632): Change this flag check to a registry config check. - if (isActiveNow(MINIMUM_DATASET_CONTACTS_OPTIONAL)) { + if (FeatureFlag.isActiveNow(MINIMUM_DATASET_CONTACTS_OPTIONAL) + || FeatureFlag.isActiveNow(MINIMUM_DATASET_CONTACTS_PROHIBITED)) { return Optional.empty(); } else { throw new MissingRegistrantException(); @@ -325,8 +329,7 @@ public final class DomainUpdateFlow implements MutatingFlow { * cause Domain update failure. */ private static void validateNewState(Domain newDomain) throws EppException { - validateRequiredContactsPresentIfRequiredForDataset( - newDomain.getRegistrant(), newDomain.getContacts()); + validateContactDataPresence(newDomain.getRegistrant(), newDomain.getContacts()); validateDsData(newDomain.getDsData()); validateNameserversCountForTld( newDomain.getTld(), diff --git a/core/src/main/java/google/registry/flows/exceptions/ContactsProhibitedException.java b/core/src/main/java/google/registry/flows/exceptions/ContactsProhibitedException.java new file mode 100644 index 000000000..2132e05f1 --- /dev/null +++ b/core/src/main/java/google/registry/flows/exceptions/ContactsProhibitedException.java @@ -0,0 +1,24 @@ +// Copyright 2025 The Nomulus Authors. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package google.registry.flows.exceptions; + +import google.registry.flows.EppException.ParameterValuePolicyErrorException; + +/** Having contacts is prohibited by registry policy */ +public class ContactsProhibitedException extends ParameterValuePolicyErrorException { + public ContactsProhibitedException() { + super("Having contacts is prohibited by registry policy"); + } +} diff --git a/core/src/main/java/google/registry/model/common/FeatureFlag.java b/core/src/main/java/google/registry/model/common/FeatureFlag.java index ad901645d..3c96e820b 100644 --- a/core/src/main/java/google/registry/model/common/FeatureFlag.java +++ b/core/src/main/java/google/registry/model/common/FeatureFlag.java @@ -62,11 +62,31 @@ public class FeatureFlag extends ImmutableObject implements Buildable { INACTIVE } + /** The names of the feature flags that can be individually set. */ public enum FeatureName { - TEST_FEATURE, - MINIMUM_DATASET_CONTACTS_OPTIONAL, - MINIMUM_DATASET_CONTACTS_PROHIBITED, - INCLUDE_PENDING_DELETE_DATE_FOR_DOMAINS + /** Feature flag name used for testing only. */ + TEST_FEATURE(FeatureStatus.INACTIVE), + + /** If we're not requiring the presence of contact data on domain EPP commands. */ + MINIMUM_DATASET_CONTACTS_OPTIONAL(FeatureStatus.INACTIVE), + + /** If we're not permitting the presence of contact data on any EPP commands. */ + MINIMUM_DATASET_CONTACTS_PROHIBITED(FeatureStatus.INACTIVE), + + /** + * If we're including the upcoming domain drop date in the exported list of registered domains. + */ + INCLUDE_PENDING_DELETE_DATE_FOR_DOMAINS(FeatureStatus.INACTIVE); + + private final FeatureStatus defaultStatus; + + FeatureName(FeatureStatus defaultStatus) { + this.defaultStatus = defaultStatus; + } + + FeatureStatus getDefaultStatus() { + return this.defaultStatus; + } } /** The name of the flag/feature. */ @@ -155,24 +175,24 @@ public class FeatureFlag extends ImmutableObject implements Buildable { return status.getValueAtTime(time); } - /** Returns if the flag is active, or the default value if the flag does not exist. */ - public static boolean isActiveNowOrElse(FeatureName featureName, boolean defaultValue) { - tm().assertInTransaction(); - return CACHE - .get(featureName) - .map(flag -> flag.getStatus(tm().getTransactionTime()).equals(ACTIVE)) - .orElse(defaultValue); - } - - /** Returns if the FeatureFlag with the given FeatureName is active now. */ + /** + * Returns whether the flag is active now, or else the flag's default value if it doesn't exist. + */ public static boolean isActiveNow(FeatureName featureName) { tm().assertInTransaction(); return isActiveAt(featureName, tm().getTransactionTime()); } - /** Returns if the FeatureFlag with the given FeatureName is active at a given time. */ + /** + * Returns whether the flag is active at the given time, or else the flag's default value if it + * doesn't exist. + */ public static boolean isActiveAt(FeatureName featureName, DateTime dateTime) { - return FeatureFlag.get(featureName).getStatus(dateTime).equals(ACTIVE); + tm().assertInTransaction(); + return CACHE + .get(featureName) + .map(flag -> flag.getStatus(dateTime).equals(ACTIVE)) + .orElse(featureName.getDefaultStatus().equals(ACTIVE)); } @Override diff --git a/core/src/main/java/google/registry/tools/CreateDomainCommand.java b/core/src/main/java/google/registry/tools/CreateDomainCommand.java index dc408f0cf..c3085f034 100644 --- a/core/src/main/java/google/registry/tools/CreateDomainCommand.java +++ b/core/src/main/java/google/registry/tools/CreateDomainCommand.java @@ -16,6 +16,8 @@ package google.registry.tools; import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Strings.isNullOrEmpty; +import static google.registry.model.common.FeatureFlag.FeatureName.MINIMUM_DATASET_CONTACTS_OPTIONAL; +import static google.registry.model.common.FeatureFlag.FeatureName.MINIMUM_DATASET_CONTACTS_PROHIBITED; import static google.registry.persistence.transaction.TransactionManagerFactory.tm; import static google.registry.pricing.PricingEngineProxy.getPricesForDomainName; import static google.registry.util.PreconditionsUtils.checkArgumentNotNull; @@ -62,8 +64,8 @@ final class CreateDomainCommand extends CreateOrUpdateDomainCommand { protected void initMutatingEppToolCommand() { tm().transact( () -> { - if (!FeatureFlag.isActiveNowOrElse( - FeatureFlag.FeatureName.MINIMUM_DATASET_CONTACTS_OPTIONAL, false)) { + if (!FeatureFlag.isActiveNow(MINIMUM_DATASET_CONTACTS_OPTIONAL) + && !FeatureFlag.isActiveNow(MINIMUM_DATASET_CONTACTS_PROHIBITED)) { checkArgumentNotNull(registrant, "Registrant must be specified"); checkArgument(!admins.isEmpty(), "At least one admin must be specified"); checkArgument(!techs.isEmpty(), "At least one tech must be specified"); diff --git a/core/src/test/java/google/registry/flows/contact/ContactCreateFlowTest.java b/core/src/test/java/google/registry/flows/contact/ContactCreateFlowTest.java index bc758d63f..4b2232b4c 100644 --- a/core/src/test/java/google/registry/flows/contact/ContactCreateFlowTest.java +++ b/core/src/test/java/google/registry/flows/contact/ContactCreateFlowTest.java @@ -15,6 +15,9 @@ package google.registry.flows.contact; import static com.google.common.truth.Truth.assertThat; +import static google.registry.model.common.FeatureFlag.FeatureName.MINIMUM_DATASET_CONTACTS_PROHIBITED; +import static google.registry.model.common.FeatureFlag.FeatureStatus.ACTIVE; +import static google.registry.model.common.FeatureFlag.FeatureStatus.INACTIVE; import static google.registry.testing.ContactSubject.assertAboutContacts; import static google.registry.testing.DatabaseHelper.assertNoBillingEvents; import static google.registry.testing.DatabaseHelper.newContact; @@ -22,15 +25,19 @@ import static google.registry.testing.DatabaseHelper.persistActiveContact; import static google.registry.testing.DatabaseHelper.persistDeletedContact; import static google.registry.testing.DatabaseHelper.persistResource; import static google.registry.testing.EppExceptionSubject.assertAboutEppExceptions; +import static google.registry.util.DateTimeUtils.START_OF_TIME; import static org.junit.jupiter.api.Assertions.assertThrows; +import com.google.common.collect.ImmutableSortedMap; import google.registry.flows.EppException; import google.registry.flows.FlowUtils.NotLoggedInException; import google.registry.flows.ResourceFlowTestCase; import google.registry.flows.contact.ContactFlowUtils.BadInternationalizedPostalInfoException; import google.registry.flows.contact.ContactFlowUtils.DeclineContactDisclosureFieldDisallowedPolicyException; +import google.registry.flows.exceptions.ContactsProhibitedException; import google.registry.flows.exceptions.ResourceAlreadyExistsForThisClientException; import google.registry.flows.exceptions.ResourceCreateContentionException; +import google.registry.model.common.FeatureFlag; import google.registry.model.contact.Contact; import org.joda.time.DateTime; import org.junit.jupiter.api.Test; @@ -89,6 +96,18 @@ class ContactCreateFlowTest extends ResourceFlowTestCase> nameservers = new ImmutableSet.Builder<>(); for (int i = 1; i < 15; i++) { @@ -1540,8 +1555,8 @@ class DomainUpdateFlowTest extends ResourceFlowTestCase { - assertThat(FeatureFlag.isActiveNowOrElse(TEST_FEATURE, false)).isFalse(); - assertThat(FeatureFlag.isActiveNowOrElse(TEST_FEATURE, true)).isFalse(); + assertThat(FeatureFlag.isActiveNow(TEST_FEATURE)).isFalse(); }); fakeClock.advanceBy(Duration.standardDays(365)); tm().transact( () -> { - assertThat(FeatureFlag.isActiveNowOrElse(TEST_FEATURE, false)).isTrue(); - assertThat(FeatureFlag.isActiveNowOrElse(TEST_FEATURE, true)).isTrue(); - }); - } - - @Test - void testSuccess_default_doesNotExist() { - tm().transact( - () -> { - assertThat(FeatureFlag.isActiveNowOrElse(TEST_FEATURE, false)).isFalse(); - assertThat(FeatureFlag.isActiveNowOrElse(TEST_FEATURE, true)).isTrue(); + assertThat(FeatureFlag.isActiveNow(TEST_FEATURE)).isTrue(); }); } } diff --git a/core/src/test/java/google/registry/tools/CreateDomainCommandTest.java b/core/src/test/java/google/registry/tools/CreateDomainCommandTest.java index 000486cae..9da1db398 100644 --- a/core/src/test/java/google/registry/tools/CreateDomainCommandTest.java +++ b/core/src/test/java/google/registry/tools/CreateDomainCommandTest.java @@ -16,6 +16,7 @@ package google.registry.tools; import static com.google.common.truth.Truth.assertThat; import static google.registry.model.common.FeatureFlag.FeatureName.MINIMUM_DATASET_CONTACTS_OPTIONAL; +import static google.registry.model.common.FeatureFlag.FeatureName.MINIMUM_DATASET_CONTACTS_PROHIBITED; import static google.registry.model.common.FeatureFlag.FeatureStatus.ACTIVE; import static google.registry.persistence.transaction.TransactionManagerFactory.tm; import static google.registry.testing.DatabaseHelper.createTld; @@ -115,7 +116,7 @@ class CreateDomainCommandTest extends EppToolCommandTestCase + + + + example.tld + 2 + + ns1.example.net + ns2.example.net + + + 2fooBAR + + + + ABC-12345 + + diff --git a/core/src/test/resources/google/registry/flows/domain/domain_create_response_eap_fee.xml b/core/src/test/resources/google/registry/flows/domain/domain_create_response_eap_fee.xml index 5b9397f67..f573fb789 100644 --- a/core/src/test/resources/google/registry/flows/domain/domain_create_response_eap_fee.xml +++ b/core/src/test/resources/google/registry/flows/domain/domain_create_response_eap_fee.xml @@ -15,7 +15,7 @@ USD 24.00 - 100.00 + 100.00 diff --git a/core/src/test/resources/google/registry/flows/domain/domain_create_response_premium_eap.xml b/core/src/test/resources/google/registry/flows/domain/domain_create_response_premium_eap.xml index 2854b478c..b5bdbc0bf 100644 --- a/core/src/test/resources/google/registry/flows/domain/domain_create_response_premium_eap.xml +++ b/core/src/test/resources/google/registry/flows/domain/domain_create_response_premium_eap.xml @@ -15,7 +15,7 @@ USD 200.00 - 100.00 + 100.00 diff --git a/core/src/test/resources/google/registry/flows/domain/domain_update_remove_all_contacts.xml b/core/src/test/resources/google/registry/flows/domain/domain_update_remove_all_contacts.xml new file mode 100644 index 000000000..93c25ccf1 --- /dev/null +++ b/core/src/test/resources/google/registry/flows/domain/domain_update_remove_all_contacts.xml @@ -0,0 +1,20 @@ + + + + + example.tld + + + sh8013 + sh8013 + sh8013 + + + + + + + ABC-12345 + +