1
0
mirror of https://github.com/google/nomulus synced 2026-06-09 16:33:02 +00:00

Compare commits

...

13 Commits

Author SHA1 Message Date
gbrodman 9555dca8c6 Don't allow loopback IP addresses for hosts (#2920)
I don't know where in the spec these are explicitly disallowed, but it
seems like good practice and we'll fail the RST tests if we don't
disallow them.
2026-01-05 21:29:15 +00:00
Ben McIlwain 49484c06d3 Filter out registrars of type OT&E from RDE escrow deposits (#2921)
The RDE XML schema (which is verified by ICANN's RST) requires the presence of a
numeric IANA identifier, which is always null for OT&E registrars. This change
synchronizes the three types of registrars that must have a null IANA identifier
(see
https://cs.opensource.google/nomulus/nomulus/+/master:core/src/main/java/google/registry/model/registrar/Registrar.java;l=109-142;drc=b1266c95e8d9f8206415d2821929d4161869b699
) with the registrars that are excluded from the RDE deposit. Note that there
are no registrars of type OT&E in prod and I can't think of a reason they would
need to be included in escrow deposits on sandbox.
2026-01-05 21:20:11 +00:00
Nilay Shah 81d222e7d6 Add GetServiceState action for MoSAPI service monitoring (#2906)
* Add GetServiceState action for MoSAPI service monitoring

Implements the `/api/mosapi/getServiceState` endpoint to retrieve service health summaries for TLDs from the MoSAPI system.

- Introduces `GetServiceStateAction` to fetch TLD service status.
- Implements `MosApiStateService` to transform raw MoSAPI responses into a curated `ServiceStateSummary`.
- Uses concurrent processing with a fixed thread pool to fetch states for all configured TLDs efficiently while respecting MoSAPI rate limits.

junit test added

* Refactor MoSAPI models to records and address review nits

- Convert model classes to Java records for conciseness and immutability.
- Update unit tests to use Java text blocks for improved JSON readability.
- Simplify service and action layers by removing redundant logic and logging.
- Fix configuration nits regarding primitive types and comment formatting.

* Consolidate MoSAPI models and enhance null-safety

- Moves model records into a single MosApiModels.java file.
- Switches to ImmutableList/ImmutableMap with non-null defaults in constructors.
- Removes redundant pass-through methods in MosApiStateService.
- Updates tests to use Java Text Blocks and non-null collection assertions.

* Improve MoSAPI client error handling and clean up data models

Refactors the MoSAPI monitoring client to be more robust against
infrastructure failures

* Refactor: use nullToEmptyImmutableCopy() for MoSAPI models

Standardize null-handling in model classes by using the Nomulus
`nullToEmptyImmutableCopy()` utility. This ensures consistent API
responses with empty lists instead of omitted fields.
2026-01-05 15:44:01 +00:00
Weimin Yu 7e9d4c27d1 Use downloaded Gradle distribution on Cloud Build (#2918)
This way we get around the http url and no longer needs public access on
the GCS bucket.
2025-12-30 21:08:04 +00:00
Weimin Yu f9c22ff1c5 Add RST support in Sandbox (#2917)
* Add RST support in Sandbox

Added RST test label files as resources.

Added a RstTmchUtils class that loads appropriate labels according to
TLD pattern.

Temporarily changed label fetching in production to include the TLD
string, so that the new class may know which set of labels to use.

* Addressing comments

* Addressing comments
2025-12-30 20:59:28 +00:00
gbrodman 2562d582f3 Add more strict hostname validation on host:check flows (#2915)
We do most of these on host create already so we should also do them on
host checks. The only added change is the character validation (our
existing hostnames all match these).
2025-12-30 16:41:56 +00:00
Ben McIlwain 6f0bc1ded9 Add Augmented Latin IDN table to IDN enums (#2914)
This was added in https://github.com/google/nomulus/pull/2884 , but now as of
this PR it can actually be configured and used on a TLD.
2025-12-27 00:57:24 +00:00
gbrodman db9fc3271d Change EPP errors 2306->2005 for some structural issues (#2911)
2306 signifies something that is syntactically valid but semantically
invalid (like if someone tried to register a .com domain). These errors
are for domain syntax that could never be valid, thus we should throw a
syntax exception instead of a policy exception.
2025-12-26 16:08:04 +00:00
Ben McIlwain 84491fde70 Don't allow underscores in TLD ROID suffixes (#2913)
Per ICANN it's a disallowed character.
2025-12-26 16:01:28 +00:00
Juan Celhay 0519e2ffcf Change gradle memory/workers to avoid OOM in CB (#2910) 2025-12-23 15:49:25 +00:00
gbrodman 85f75494ab Remove implementation of contact flows (#2896)
Now that we have transitioned to the minimum dataset, we no longer
support any actions on contacts (and by the time this is merged /
deployed, all contacts will be deleted). We should just throw an
appropriate exception on all contact-related flows. We don't delete the
flows themselves, so that we can have an appropriate error message.

We also keep all the flows and XML templates around individually for now because we may be
required to continue to differentiate the requests in ICANN activity
reporting (e.g. srs-cont-create vs srs-cont-delete)
2025-12-23 15:38:24 +00:00
Ben McIlwain cbba91558a Allow double hyphens in 3rd&4th position in all domain operations (#2909)
This is a follow-up to PR #2908, which relaxed this restriction on bare TLDs
only, but now we also allow it systemwide on domains and hostnames as well.  The
rules against hyphens in these positions are still enforced on all parts of the
domain name except the last one. Correct handling of multi-part TLDs in this
regard is out of scope in this PR; a multi-part TLD that looked something like
".zz--foobar.foobar" would still fail validation. (But of course you cannot a
priori know just from looking at a 3-part string whether it might be a hostname
on a normal TLD, or a domain name on a 2-part TLD.)

This also has some annoying interactions with a trailing dot (indicating the
root), which need to be preserved, but otherwise don't affect how TLD validation
is handled.

BUG= http://b/471013082
2025-12-23 00:57:57 +00:00
Ben McIlwain c24f09febc Don't call canonicalizeHostname() on nomulus command TLD args (#2908)
The canonicalizeHostname() helper method is only suitable for use with domain
names or host names. It does not work on bare TLDs, because a bare TLD can
have hyphens in the third and fourth position without necessarily being an IDN.
Note that the configure TLD command already correctly allows TLDs with such
names to be created.

Note that we are still enforcing that the TLDs to be added exist, so they have
to pass all TLD naming requirements that are enforced on creating TLDs, and we
are still lowercasing the TLD names passed as arguments here (though we're no
longer punycoding them, although arguably that's not super useful on
command-line params anyway).

BUG= http://b/471013082
2025-12-22 21:34:55 +00:00
111 changed files with 2078 additions and 3722 deletions
+1 -1
View File
@@ -56,7 +56,7 @@ PROPERTIES_HEADER = """\
# nom_build), run ./nom_build --help.
#
# DO NOT EDIT THIS FILE BY HAND
org.gradle.jvmargs=-Xmx2048m
org.gradle.jvmargs=-Xmx4096m
org.gradle.caching=true
org.gradle.parallel=true
"""
@@ -184,10 +184,10 @@ public class RdePipeline implements Serializable {
private final CloudTasksUtils cloudTasksUtils;
private final RdeMarshaller marshaller;
// Registrars to be excluded from data escrow. Not including the sandbox-only OTE type so that
// if sneaks into production we would get an extra signal.
// Registrars to be excluded from data escrow (i.e. all registrar types that have a null IANA
// identifier and thus would not be valid according to the RDE schema).
private static final ImmutableSet<Type> IGNORED_REGISTRAR_TYPES =
Sets.immutableEnumSet(Registrar.Type.MONITORING, Registrar.Type.TEST);
Sets.immutableEnumSet(Registrar.Type.MONITORING, Registrar.Type.OTE, Registrar.Type.TEST);
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
@@ -1462,6 +1462,12 @@ public final class RegistryConfig {
return ImmutableSet.copyOf(config.mosapi.services);
}
@Provides
@Config("mosapiTldThreadCnt")
public static int provideMosapiTldThreads(RegistryConfigSettings config) {
return config.mosapi.tldThreadCnt;
}
private static String formatComments(String text) {
return Splitter.on('\n').omitEmptyStrings().trimResults().splitToList(text).stream()
.map(s -> "# " + s)
@@ -272,5 +272,6 @@ public class RegistryConfigSettings {
public String entityType;
public List<String> tlds;
public List<String> services;
public int tldThreadCnt;
}
}
@@ -642,4 +642,8 @@ mosapi:
- "epp"
- "dnssec"
# Provides a fixed thread pool for parallel TLD processing.
# @see <a href="https://www.icann.org/mosapi-specification.pdf">
# ICANN MoSAPI Specification, Section 12.3</a>
tldThreadCnt: 4
@@ -14,60 +14,19 @@
package google.registry.flows.contact;
import static google.registry.flows.FlowUtils.validateRegistrarIsLoggedIn;
import static google.registry.flows.ResourceFlowUtils.verifyTargetIdCount;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import google.registry.config.RegistryConfig.Config;
import google.registry.flows.EppException;
import google.registry.flows.ExtensionManager;
import google.registry.flows.FlowModule.RegistrarId;
import google.registry.flows.TransactionalFlow;
import google.registry.flows.annotations.ReportingSpec;
import google.registry.model.ForeignKeyUtils;
import google.registry.model.contact.Contact;
import google.registry.model.contact.ContactCommand.Check;
import google.registry.model.eppinput.ResourceCommand;
import google.registry.model.eppoutput.CheckData.ContactCheck;
import google.registry.model.eppoutput.CheckData.ContactCheckData;
import google.registry.model.eppoutput.EppResponse;
import google.registry.flows.exceptions.ContactsProhibitedException;
import google.registry.model.reporting.IcannReportingTypes.ActivityReportField;
import google.registry.util.Clock;
import jakarta.inject.Inject;
/**
* An EPP flow that checks whether a contact can be provisioned.
* An EPP flow that is meant to check whether a contact can be provisioned.
*
* <p>This flows can check the existence of multiple contacts simultaneously.
*
* @error {@link google.registry.flows.exceptions.TooManyResourceChecksException}
* @error {@link google.registry.flows.FlowUtils.NotLoggedInException}
* @error {@link ContactsProhibitedException}
*/
@Deprecated
@ReportingSpec(ActivityReportField.CONTACT_CHECK)
public final class ContactCheckFlow implements TransactionalFlow {
@Inject ResourceCommand resourceCommand;
@Inject @RegistrarId String registrarId;
@Inject ExtensionManager extensionManager;
@Inject Clock clock;
@Inject @Config("maxChecks") int maxChecks;
@Inject EppResponse.Builder responseBuilder;
public final class ContactCheckFlow extends ContactsProhibitedFlow {
@Inject ContactCheckFlow() {}
@Override
public EppResponse run() throws EppException {
validateRegistrarIsLoggedIn(registrarId);
extensionManager.validate(); // There are no legal extensions for this flow.
ImmutableList<String> targetIds = ((Check) resourceCommand).getTargetIds();
verifyTargetIdCount(targetIds, maxChecks);
ImmutableSet<String> existingIds =
ForeignKeyUtils.loadKeys(Contact.class, targetIds, clock.nowUtc()).keySet();
ImmutableList.Builder<ContactCheck> checks = new ImmutableList.Builder<>();
for (String id : targetIds) {
boolean unused = !existingIds.contains(id);
checks.add(ContactCheck.create(unused, id, unused ? null : "In use"));
}
return responseBuilder.setResData(ContactCheckData.create(checks.build())).build();
}
}
@@ -14,94 +14,19 @@
package google.registry.flows.contact;
import static google.registry.flows.FlowUtils.validateRegistrarIsLoggedIn;
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;
import google.registry.config.RegistryConfig.Config;
import google.registry.flows.EppException;
import google.registry.flows.ExtensionManager;
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;
import google.registry.model.domain.metadata.MetadataExtension;
import google.registry.model.eppinput.ResourceCommand;
import google.registry.model.eppoutput.CreateData.ContactCreateData;
import google.registry.model.eppoutput.EppResponse;
import google.registry.model.reporting.HistoryEntry;
import google.registry.model.reporting.IcannReportingTypes.ActivityReportField;
import jakarta.inject.Inject;
import org.joda.time.DateTime;
/**
* An EPP flow that creates a new contact.
* An EPP flow meant to create a new contact.
*
* @error {@link google.registry.flows.FlowUtils.NotLoggedInException}
* @error {@link ContactsProhibitedException}
* @error {@link ResourceAlreadyExistsForThisClientException}
* @error {@link ResourceCreateContentionException}
* @error {@link ContactFlowUtils.BadInternationalizedPostalInfoException}
* @error {@link ContactFlowUtils.DeclineContactDisclosureFieldDisallowedPolicyException}
*/
@Deprecated
@ReportingSpec(ActivityReportField.CONTACT_CREATE)
public final class ContactCreateFlow implements MutatingFlow {
@Inject ResourceCommand resourceCommand;
@Inject ExtensionManager extensionManager;
@Inject @RegistrarId String registrarId;
@Inject @TargetId String targetId;
@Inject ContactHistory.Builder historyBuilder;
@Inject EppResponse.Builder responseBuilder;
@Inject @Config("contactAndHostRoidSuffix") String roidSuffix;
public final class ContactCreateFlow extends ContactsProhibitedFlow {
@Inject ContactCreateFlow() {}
@Override
public EppResponse run() throws EppException {
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);
Contact newContact =
new Contact.Builder()
.setContactId(targetId)
.setAuthInfo(command.getAuthInfo())
.setCreationRegistrarId(registrarId)
.setPersistedCurrentSponsorRegistrarId(registrarId)
.setRepoId(createRepoId(tm().allocateId(), roidSuffix))
.setFaxNumber(command.getFax())
.setVoiceNumber(command.getVoice())
.setDisclose(command.getDisclose())
.setEmailAddress(command.getEmail())
.setInternationalizedPostalInfo(command.getInternationalizedPostalInfo())
.setLocalizedPostalInfo(command.getLocalizedPostalInfo())
.build();
validateAsciiPostalInfo(newContact.getInternationalizedPostalInfo());
validateContactAgainstPolicy(newContact);
historyBuilder
.setType(HistoryEntry.Type.CONTACT_CREATE)
.setXmlBytes(null) // We don't want to store contact details in the history entry.
.setContact(newContact);
tm().insertAll(ImmutableSet.of(newContact, historyBuilder.build()));
return responseBuilder
.setResData(ContactCreateData.create(newContact.getContactId(), now))
.build();
}
}
@@ -14,97 +14,20 @@
package google.registry.flows.contact;
import static google.registry.flows.FlowUtils.DELETE_PROHIBITED_STATUSES;
import static google.registry.flows.FlowUtils.validateRegistrarIsLoggedIn;
import static google.registry.flows.ResourceFlowUtils.checkLinkedDomains;
import static google.registry.flows.ResourceFlowUtils.loadAndVerifyExistence;
import static google.registry.flows.ResourceFlowUtils.verifyNoDisallowedStatuses;
import static google.registry.flows.ResourceFlowUtils.verifyOptionalAuthInfo;
import static google.registry.flows.ResourceFlowUtils.verifyResourceOwnership;
import static google.registry.model.ResourceTransferUtils.denyPendingTransfer;
import static google.registry.model.ResourceTransferUtils.handlePendingTransferOnDelete;
import static google.registry.model.eppoutput.Result.Code.SUCCESS;
import static google.registry.model.transfer.TransferStatus.SERVER_CANCELLED;
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
import com.google.common.collect.ImmutableSet;
import google.registry.flows.EppException;
import google.registry.flows.ExtensionManager;
import google.registry.flows.FlowModule.RegistrarId;
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.model.contact.Contact;
import google.registry.model.contact.ContactHistory;
import google.registry.model.domain.metadata.MetadataExtension;
import google.registry.model.eppcommon.AuthInfo;
import google.registry.model.eppcommon.StatusValue;
import google.registry.model.eppcommon.Trid;
import google.registry.model.eppoutput.EppResponse;
import google.registry.model.reporting.HistoryEntry.Type;
import google.registry.flows.exceptions.ContactsProhibitedException;
import google.registry.model.reporting.IcannReportingTypes.ActivityReportField;
import jakarta.inject.Inject;
import java.util.Optional;
import org.joda.time.DateTime;
/**
* An EPP flow that deletes a contact.
* An EPP flow that is meant to delete a contact.
*
* <p>Contacts that are in use by any domain cannot be deleted. The flow may return immediately if a
* quick smoke check determines that deletion is impossible due to an existing reference. However, a
* successful delete will always be asynchronous, as all existing domains must be checked for
* references to the host before the deletion is allowed to proceed. A poll message will be written
* with the success or failure message when the process is complete.
*
* @error {@link google.registry.flows.FlowUtils.NotLoggedInException}
* @error {@link google.registry.flows.ResourceFlowUtils.ResourceDoesNotExistException}
* @error {@link google.registry.flows.ResourceFlowUtils.ResourceNotOwnedException}
* @error {@link google.registry.flows.exceptions.ResourceStatusProhibitsOperationException}
* @error {@link google.registry.flows.exceptions.ResourceToDeleteIsReferencedException}
* @error {@link ContactsProhibitedException}
*/
@Deprecated
@ReportingSpec(ActivityReportField.CONTACT_DELETE)
public final class ContactDeleteFlow implements MutatingFlow {
@Inject ExtensionManager extensionManager;
@Inject @RegistrarId String registrarId;
@Inject @TargetId String targetId;
@Inject Trid trid;
@Inject @Superuser boolean isSuperuser;
@Inject Optional<AuthInfo> authInfo;
@Inject ContactHistory.Builder historyBuilder;
@Inject EppResponse.Builder responseBuilder;
public final class ContactDeleteFlow extends ContactsProhibitedFlow {
@Inject
ContactDeleteFlow() {}
@Override
public EppResponse run() throws EppException {
extensionManager.register(MetadataExtension.class);
validateRegistrarIsLoggedIn(registrarId);
extensionManager.validate();
DateTime now = tm().getTransactionTime();
checkLinkedDomains(targetId, now, Contact.class);
Contact existingContact = loadAndVerifyExistence(Contact.class, targetId, now);
verifyOptionalAuthInfo(authInfo, existingContact);
verifyNoDisallowedStatuses(existingContact, ImmutableSet.of(StatusValue.PENDING_DELETE));
if (!isSuperuser) {
verifyNoDisallowedStatuses(existingContact, DELETE_PROHIBITED_STATUSES);
verifyResourceOwnership(registrarId, existingContact);
}
// Handle pending transfers on contact deletion.
Contact newContact =
existingContact.getStatusValues().contains(StatusValue.PENDING_TRANSFER)
? denyPendingTransfer(existingContact, SERVER_CANCELLED, now, registrarId)
: existingContact;
// Wipe out PII on contact deletion.
newContact =
newContact.asBuilder().wipeOut().setStatusValues(null).setDeletionTime(now).build();
ContactHistory contactHistory =
historyBuilder.setType(Type.CONTACT_DELETE).setContact(newContact).build();
handlePendingTransferOnDelete(existingContact, newContact, now, contactHistory);
tm().insert(contactHistory);
tm().update(newContact);
return responseBuilder.setResultFromCode(SUCCESS).build();
}
}
@@ -1,126 +0,0 @@
// Copyright 2017 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.contact;
import static google.registry.model.contact.PostalInfo.Type.INTERNATIONALIZED;
import com.google.common.base.CharMatcher;
import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Sets;
import google.registry.flows.EppException;
import google.registry.flows.EppException.ParameterValuePolicyErrorException;
import google.registry.flows.EppException.ParameterValueSyntaxErrorException;
import google.registry.model.contact.Contact;
import google.registry.model.contact.ContactAddress;
import google.registry.model.contact.PostalInfo;
import google.registry.model.poll.PendingActionNotificationResponse.ContactPendingActionNotificationResponse;
import google.registry.model.poll.PollMessage;
import google.registry.model.reporting.HistoryEntry.HistoryEntryId;
import google.registry.model.transfer.TransferData;
import google.registry.model.transfer.TransferResponse.ContactTransferResponse;
import java.util.Set;
import javax.annotation.Nullable;
import org.joda.time.DateTime;
/** Static utility functions for contact flows. */
public class ContactFlowUtils {
/** Check that an internationalized postal info has only ascii characters. */
static void validateAsciiPostalInfo(@Nullable PostalInfo internationalized) throws EppException {
if (internationalized != null) {
Preconditions.checkState(INTERNATIONALIZED.equals(internationalized.getType()));
ContactAddress address = internationalized.getAddress();
Set<String> fields = Sets.newHashSet(
internationalized.getName(),
internationalized.getOrg(),
address.getCity(),
address.getCountryCode(),
address.getState(),
address.getZip());
fields.addAll(address.getStreet());
for (String field : fields) {
if (field != null && !CharMatcher.ascii().matchesAllOf(field)) {
throw new BadInternationalizedPostalInfoException();
}
}
}
}
/** Check contact's state against server policy. */
static void validateContactAgainstPolicy(Contact contact) throws EppException {
if (contact.getDisclose() != null && !contact.getDisclose().getFlag()) {
throw new DeclineContactDisclosureFieldDisallowedPolicyException();
}
}
/** Create a poll message for the gaining client in a transfer. */
static PollMessage createGainingTransferPollMessage(
String targetId, TransferData transferData, DateTime now, HistoryEntryId contactHistoryId) {
return new PollMessage.OneTime.Builder()
.setRegistrarId(transferData.getGainingRegistrarId())
.setEventTime(transferData.getPendingTransferExpirationTime())
.setMsg(transferData.getTransferStatus().getMessage())
.setResponseData(
ImmutableList.of(
createTransferResponse(targetId, transferData),
ContactPendingActionNotificationResponse.create(
targetId,
transferData.getTransferStatus().isApproved(),
transferData.getTransferRequestTrid(),
now)))
.setContactHistoryId(contactHistoryId)
.build();
}
/** Create a poll message for the losing client in a transfer. */
static PollMessage createLosingTransferPollMessage(
String targetId, TransferData transferData, HistoryEntryId contactHistoryId) {
return new PollMessage.OneTime.Builder()
.setRegistrarId(transferData.getLosingRegistrarId())
.setEventTime(transferData.getPendingTransferExpirationTime())
.setMsg(transferData.getTransferStatus().getMessage())
.setResponseData(ImmutableList.of(createTransferResponse(targetId, transferData)))
.setContactHistoryId(contactHistoryId)
.build();
}
/** Create a {@link ContactTransferResponse} off of the info in a {@link TransferData}. */
static ContactTransferResponse createTransferResponse(
String targetId, TransferData transferData) {
return new ContactTransferResponse.Builder()
.setContactId(targetId)
.setGainingRegistrarId(transferData.getGainingRegistrarId())
.setLosingRegistrarId(transferData.getLosingRegistrarId())
.setPendingTransferExpirationTime(transferData.getPendingTransferExpirationTime())
.setTransferRequestTime(transferData.getTransferRequestTime())
.setTransferStatus(transferData.getTransferStatus())
.build();
}
/** Declining contact disclosure is disallowed by server policy. */
static class DeclineContactDisclosureFieldDisallowedPolicyException
extends ParameterValuePolicyErrorException {
public DeclineContactDisclosureFieldDisallowedPolicyException() {
super("Declining contact disclosure is disallowed by server policy.");
}
}
/** Internationalized postal infos can only contain ASCII characters. */
static class BadInternationalizedPostalInfoException extends ParameterValueSyntaxErrorException {
public BadInternationalizedPostalInfoException() {
super("Internationalized postal infos can only contain ASCII characters");
}
}
}
@@ -14,91 +14,20 @@
package google.registry.flows.contact;
import static google.registry.flows.FlowUtils.validateRegistrarIsLoggedIn;
import static google.registry.flows.ResourceFlowUtils.loadAndVerifyExistence;
import static google.registry.flows.ResourceFlowUtils.verifyResourceOwnership;
import static google.registry.model.EppResourceUtils.isLinked;
import com.google.common.collect.ImmutableSet;
import google.registry.flows.EppException;
import google.registry.flows.ExtensionManager;
import google.registry.flows.FlowModule.RegistrarId;
import google.registry.flows.FlowModule.Superuser;
import google.registry.flows.FlowModule.TargetId;
import google.registry.flows.TransactionalFlow;
import google.registry.flows.annotations.ReportingSpec;
import google.registry.model.contact.Contact;
import google.registry.model.contact.ContactInfoData;
import google.registry.model.eppcommon.AuthInfo;
import google.registry.model.eppcommon.StatusValue;
import google.registry.model.eppoutput.EppResponse;
import google.registry.flows.exceptions.ContactsProhibitedException;
import google.registry.model.reporting.IcannReportingTypes.ActivityReportField;
import google.registry.util.Clock;
import jakarta.inject.Inject;
import java.util.Optional;
import org.joda.time.DateTime;
/**
* An EPP flow that returns information about a contact.
* An EPP flow that is meant to return information about a contact.
*
* <p>The response includes the contact's postal info, phone numbers, emails, the authInfo which can
* be used to request a transfer and the details of the contact's most recent transfer if it has
* ever been transferred. Any registrar can see any contact's information, but the authInfo is only
* visible to the registrar that owns the contact or to a registrar that already supplied it.
*
* @error {@link google.registry.flows.FlowUtils.NotLoggedInException}
* @error {@link google.registry.flows.ResourceFlowUtils.ResourceDoesNotExistException}
* @error {@link google.registry.flows.ResourceFlowUtils.ResourceNotOwnedException}
* @error {@link ContactsProhibitedException}
*/
@Deprecated
@ReportingSpec(ActivityReportField.CONTACT_INFO)
public final class ContactInfoFlow implements TransactionalFlow {
@Inject ExtensionManager extensionManager;
@Inject Clock clock;
@Inject @RegistrarId String registrarId;
@Inject @TargetId String targetId;
@Inject Optional<AuthInfo> authInfo;
@Inject @Superuser boolean isSuperuser;
@Inject EppResponse.Builder responseBuilder;
public final class ContactInfoFlow extends ContactsProhibitedFlow {
@Inject
ContactInfoFlow() {}
@Override
public EppResponse run() throws EppException {
DateTime now = clock.nowUtc();
validateRegistrarIsLoggedIn(registrarId);
extensionManager.validate(); // There are no legal extensions for this flow.
Contact contact = loadAndVerifyExistence(Contact.class, targetId, now);
if (!isSuperuser) {
verifyResourceOwnership(registrarId, contact);
}
boolean includeAuthInfo =
registrarId.equals(contact.getCurrentSponsorRegistrarId()) || authInfo.isPresent();
ImmutableSet.Builder<StatusValue> statusValues = new ImmutableSet.Builder<>();
statusValues.addAll(contact.getStatusValues());
if (isLinked(contact.createVKey(), now)) {
statusValues.add(StatusValue.LINKED);
}
return responseBuilder
.setResData(
ContactInfoData.newBuilder()
.setContactId(contact.getContactId())
.setRepoId(contact.getRepoId())
.setStatusValues(statusValues.build())
.setPostalInfos(contact.getPostalInfosAsList())
.setVoiceNumber(contact.getVoiceNumber())
.setFaxNumber(contact.getFaxNumber())
.setEmailAddress(contact.getEmailAddress())
.setCurrentSponsorRegistrarId(contact.getCurrentSponsorRegistrarId())
.setCreationRegistrarId(contact.getCreationRegistrarId())
.setCreationTime(contact.getCreationTime())
.setLastEppUpdateRegistrarId(contact.getLastEppUpdateRegistrarId())
.setLastEppUpdateTime(contact.getLastEppUpdateTime())
.setLastTransferTime(contact.getLastTransferTime())
.setAuthInfo(includeAuthInfo ? contact.getAuthInfo() : null)
.setDisclose(contact.getDisclose())
.build())
.build();
}
}
@@ -14,92 +14,19 @@
package google.registry.flows.contact;
import static google.registry.flows.FlowUtils.validateRegistrarIsLoggedIn;
import static google.registry.flows.ResourceFlowUtils.loadAndVerifyExistence;
import static google.registry.flows.ResourceFlowUtils.verifyHasPendingTransfer;
import static google.registry.flows.ResourceFlowUtils.verifyOptionalAuthInfo;
import static google.registry.flows.ResourceFlowUtils.verifyResourceOwnership;
import static google.registry.flows.contact.ContactFlowUtils.createGainingTransferPollMessage;
import static google.registry.flows.contact.ContactFlowUtils.createTransferResponse;
import static google.registry.model.ResourceTransferUtils.approvePendingTransfer;
import static google.registry.model.reporting.HistoryEntry.Type.CONTACT_TRANSFER_APPROVE;
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
import com.google.common.collect.ImmutableSet;
import google.registry.flows.EppException;
import google.registry.flows.ExtensionManager;
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.model.contact.Contact;
import google.registry.model.contact.ContactHistory;
import google.registry.model.domain.metadata.MetadataExtension;
import google.registry.model.eppcommon.AuthInfo;
import google.registry.model.eppinput.ResourceCommand;
import google.registry.model.eppoutput.EppResponse;
import google.registry.model.poll.PollMessage;
import google.registry.flows.exceptions.ContactsProhibitedException;
import google.registry.model.reporting.IcannReportingTypes.ActivityReportField;
import google.registry.model.transfer.TransferStatus;
import jakarta.inject.Inject;
import java.util.Optional;
import org.joda.time.DateTime;
/**
* An EPP flow that approves a pending transfer on a contact.
* An EPP flow that is meant to approve a pending transfer on a contact.
*
* <p>The "gaining" registrar requests a transfer from the "losing" (aka current) registrar. The
* losing registrar has a "transfer" time period to respond (by default five days) after which the
* transfer is automatically approved. Within that window, this flow allows the losing client to
* explicitly approve the transfer request, which then becomes effective immediately.
*
* @error {@link google.registry.flows.FlowUtils.NotLoggedInException}
* @error {@link google.registry.flows.ResourceFlowUtils.BadAuthInfoForResourceException}
* @error {@link google.registry.flows.ResourceFlowUtils.ResourceNotOwnedException}
* @error {@link google.registry.flows.ResourceFlowUtils.ResourceDoesNotExistException}
* @error {@link google.registry.flows.exceptions.NotPendingTransferException}
* @error {@link ContactsProhibitedException}
*/
@Deprecated
@ReportingSpec(ActivityReportField.CONTACT_TRANSFER_APPROVE)
public final class ContactTransferApproveFlow implements MutatingFlow {
@Inject ResourceCommand resourceCommand;
@Inject ExtensionManager extensionManager;
@Inject @RegistrarId String registrarId;
@Inject @TargetId String targetId;
@Inject Optional<AuthInfo> authInfo;
@Inject ContactHistory.Builder historyBuilder;
@Inject EppResponse.Builder responseBuilder;
public final class ContactTransferApproveFlow extends ContactsProhibitedFlow {
@Inject ContactTransferApproveFlow() {}
/**
* The logic in this flow, which handles client approvals, very closely parallels the logic in
* {@link Contact#cloneProjectedAtTime} which handles implicit server approvals.
*/
@Override
public EppResponse run() throws EppException {
extensionManager.register(MetadataExtension.class);
validateRegistrarIsLoggedIn(registrarId);
extensionManager.validate();
DateTime now = tm().getTransactionTime();
Contact existingContact = loadAndVerifyExistence(Contact.class, targetId, now);
verifyOptionalAuthInfo(authInfo, existingContact);
verifyHasPendingTransfer(existingContact);
verifyResourceOwnership(registrarId, existingContact);
Contact newContact =
approvePendingTransfer(existingContact, TransferStatus.CLIENT_APPROVED, now);
ContactHistory contactHistory =
historyBuilder.setType(CONTACT_TRANSFER_APPROVE).setContact(newContact).build();
// Create a poll message for the gaining client.
PollMessage gainingPollMessage =
createGainingTransferPollMessage(
targetId, newContact.getTransferData(), now, contactHistory.getHistoryEntryId());
tm().insertAll(ImmutableSet.of(contactHistory, gainingPollMessage));
tm().update(newContact);
// Delete the billing event and poll messages that were written in case the transfer would have
// been implicitly server approved.
tm().delete(existingContact.getTransferData().getServerApproveEntities());
return responseBuilder
.setResData(createTransferResponse(targetId, newContact.getTransferData()))
.build();
}
}
@@ -14,88 +14,19 @@
package google.registry.flows.contact;
import static google.registry.flows.FlowUtils.validateRegistrarIsLoggedIn;
import static google.registry.flows.ResourceFlowUtils.loadAndVerifyExistence;
import static google.registry.flows.ResourceFlowUtils.verifyHasPendingTransfer;
import static google.registry.flows.ResourceFlowUtils.verifyOptionalAuthInfo;
import static google.registry.flows.ResourceFlowUtils.verifyTransferInitiator;
import static google.registry.flows.contact.ContactFlowUtils.createLosingTransferPollMessage;
import static google.registry.flows.contact.ContactFlowUtils.createTransferResponse;
import static google.registry.model.ResourceTransferUtils.denyPendingTransfer;
import static google.registry.model.reporting.HistoryEntry.Type.CONTACT_TRANSFER_CANCEL;
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
import com.google.common.collect.ImmutableSet;
import google.registry.flows.EppException;
import google.registry.flows.ExtensionManager;
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.model.contact.Contact;
import google.registry.model.contact.ContactHistory;
import google.registry.model.domain.metadata.MetadataExtension;
import google.registry.model.eppcommon.AuthInfo;
import google.registry.model.eppinput.ResourceCommand;
import google.registry.model.eppoutput.EppResponse;
import google.registry.model.poll.PollMessage;
import google.registry.flows.exceptions.ContactsProhibitedException;
import google.registry.model.reporting.IcannReportingTypes.ActivityReportField;
import google.registry.model.transfer.TransferStatus;
import jakarta.inject.Inject;
import java.util.Optional;
import org.joda.time.DateTime;
/**
* An EPP flow that cancels a pending transfer on a contact.
* An EPP flow that is meant to cancel a pending transfer on a contact.
*
* <p>The "gaining" registrar requests a transfer from the "losing" (aka current) registrar. The
* losing registrar has a "transfer" time period to respond (by default five days) after which the
* transfer is automatically approved. Within that window, this flow allows the gaining client to
* withdraw the transfer request.
*
* @error {@link google.registry.flows.FlowUtils.NotLoggedInException}
* @error {@link google.registry.flows.ResourceFlowUtils.BadAuthInfoForResourceException}
* @error {@link google.registry.flows.ResourceFlowUtils.ResourceDoesNotExistException}
* @error {@link google.registry.flows.exceptions.NotPendingTransferException}
* @error {@link google.registry.flows.exceptions.NotTransferInitiatorException}
* @error {@link ContactsProhibitedException}
*/
@Deprecated
@ReportingSpec(ActivityReportField.CONTACT_TRANSFER_CANCEL)
public final class ContactTransferCancelFlow implements MutatingFlow {
@Inject ResourceCommand resourceCommand;
@Inject ExtensionManager extensionManager;
@Inject Optional<AuthInfo> authInfo;
@Inject @RegistrarId String registrarId;
@Inject @TargetId String targetId;
@Inject ContactHistory.Builder historyBuilder;
@Inject EppResponse.Builder responseBuilder;
public final class ContactTransferCancelFlow extends ContactsProhibitedFlow {
@Inject ContactTransferCancelFlow() {}
@Override
public EppResponse run() throws EppException {
extensionManager.register(MetadataExtension.class);
validateRegistrarIsLoggedIn(registrarId);
extensionManager.validate();
DateTime now = tm().getTransactionTime();
Contact existingContact = loadAndVerifyExistence(Contact.class, targetId, now);
verifyOptionalAuthInfo(authInfo, existingContact);
verifyHasPendingTransfer(existingContact);
verifyTransferInitiator(registrarId, existingContact);
Contact newContact =
denyPendingTransfer(existingContact, TransferStatus.CLIENT_CANCELLED, now, registrarId);
ContactHistory contactHistory =
historyBuilder.setType(CONTACT_TRANSFER_CANCEL).setContact(newContact).build();
// Create a poll message for the losing client.
PollMessage losingPollMessage =
createLosingTransferPollMessage(
targetId, newContact.getTransferData(), contactHistory.getHistoryEntryId());
tm().insertAll(ImmutableSet.of(contactHistory, losingPollMessage));
tm().update(newContact);
// Delete the billing event and poll messages that were written in case the transfer would have
// been implicitly server approved.
tm().delete(existingContact.getTransferData().getServerApproveEntities());
return responseBuilder
.setResData(createTransferResponse(targetId, newContact.getTransferData()))
.build();
}
}
@@ -14,74 +14,19 @@
package google.registry.flows.contact;
import static google.registry.flows.FlowUtils.validateRegistrarIsLoggedIn;
import static google.registry.flows.ResourceFlowUtils.loadAndVerifyExistence;
import static google.registry.flows.ResourceFlowUtils.verifyOptionalAuthInfo;
import static google.registry.flows.contact.ContactFlowUtils.createTransferResponse;
import google.registry.flows.EppException;
import google.registry.flows.ExtensionManager;
import google.registry.flows.FlowModule.RegistrarId;
import google.registry.flows.FlowModule.TargetId;
import google.registry.flows.TransactionalFlow;
import google.registry.flows.annotations.ReportingSpec;
import google.registry.flows.exceptions.NoTransferHistoryToQueryException;
import google.registry.flows.exceptions.NotAuthorizedToViewTransferException;
import google.registry.model.contact.Contact;
import google.registry.model.eppcommon.AuthInfo;
import google.registry.model.eppoutput.EppResponse;
import google.registry.flows.exceptions.ContactsProhibitedException;
import google.registry.model.reporting.IcannReportingTypes.ActivityReportField;
import google.registry.util.Clock;
import jakarta.inject.Inject;
import java.util.Optional;
/**
* An EPP flow that queries a pending transfer on a contact.
* An EPP flow that is meant to query a pending transfer on a contact.
*
* <p>The "gaining" registrar requests a transfer from the "losing" (aka current) registrar. The
* losing registrar has a "transfer" time period to respond (by default five days) after which the
* transfer is automatically approved. This flow can be used by the gaining or losing registrars (or
* anyone with the correct authId) to see the status of a transfer, which may still be pending or
* may have been approved, rejected, cancelled or implicitly approved by virtue of the transfer
* period expiring.
*
* @error {@link google.registry.flows.FlowUtils.NotLoggedInException}
* @error {@link google.registry.flows.ResourceFlowUtils.BadAuthInfoForResourceException}
* @error {@link google.registry.flows.ResourceFlowUtils.ResourceDoesNotExistException}
* @error {@link google.registry.flows.exceptions.NoTransferHistoryToQueryException}
* @error {@link google.registry.flows.exceptions.NotAuthorizedToViewTransferException}
* @error {@link ContactsProhibitedException}
*/
@Deprecated
@ReportingSpec(ActivityReportField.CONTACT_TRANSFER_QUERY)
public final class ContactTransferQueryFlow implements TransactionalFlow {
@Inject ExtensionManager extensionManager;
@Inject Optional<AuthInfo> authInfo;
@Inject @RegistrarId String registrarId;
@Inject @TargetId String targetId;
@Inject Clock clock;
@Inject EppResponse.Builder responseBuilder;
public final class ContactTransferQueryFlow extends ContactsProhibitedFlow {
@Inject ContactTransferQueryFlow() {}
@Override
public EppResponse run() throws EppException {
validateRegistrarIsLoggedIn(registrarId);
extensionManager.validate(); // There are no legal extensions for this flow.
Contact contact = loadAndVerifyExistence(Contact.class, targetId, clock.nowUtc());
verifyOptionalAuthInfo(authInfo, contact);
// Most of the fields on the transfer response are required, so there's no way to return valid
// XML if the object has never been transferred (and hence the fields aren't populated).
if (contact.getTransferData().getTransferStatus() == null) {
throw new NoTransferHistoryToQueryException();
}
// Note that the authorization info on the command (if present) has already been verified. If
// it's present, then the other checks are unnecessary.
if (authInfo.isEmpty()
&& !registrarId.equals(contact.getTransferData().getGainingRegistrarId())
&& !registrarId.equals(contact.getTransferData().getLosingRegistrarId())) {
throw new NotAuthorizedToViewTransferException();
}
return responseBuilder
.setResData(createTransferResponse(targetId, contact.getTransferData()))
.build();
}
}
@@ -14,85 +14,19 @@
package google.registry.flows.contact;
import static google.registry.flows.FlowUtils.validateRegistrarIsLoggedIn;
import static google.registry.flows.ResourceFlowUtils.loadAndVerifyExistence;
import static google.registry.flows.ResourceFlowUtils.verifyHasPendingTransfer;
import static google.registry.flows.ResourceFlowUtils.verifyOptionalAuthInfo;
import static google.registry.flows.ResourceFlowUtils.verifyResourceOwnership;
import static google.registry.flows.contact.ContactFlowUtils.createGainingTransferPollMessage;
import static google.registry.flows.contact.ContactFlowUtils.createTransferResponse;
import static google.registry.model.ResourceTransferUtils.denyPendingTransfer;
import static google.registry.model.reporting.HistoryEntry.Type.CONTACT_TRANSFER_REJECT;
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
import com.google.common.collect.ImmutableSet;
import google.registry.flows.EppException;
import google.registry.flows.ExtensionManager;
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.model.contact.Contact;
import google.registry.model.contact.ContactHistory;
import google.registry.model.domain.metadata.MetadataExtension;
import google.registry.model.eppcommon.AuthInfo;
import google.registry.model.eppoutput.EppResponse;
import google.registry.model.poll.PollMessage;
import google.registry.flows.exceptions.ContactsProhibitedException;
import google.registry.model.reporting.IcannReportingTypes.ActivityReportField;
import google.registry.model.transfer.TransferStatus;
import jakarta.inject.Inject;
import java.util.Optional;
import org.joda.time.DateTime;
/**
* An EPP flow that rejects a pending transfer on a contact.
* An EPP flow that is meant to reject a pending transfer on a contact.
*
* <p>The "gaining" registrar requests a transfer from the "losing" (aka current) registrar. The
* losing registrar has a "transfer" time period to respond (by default five days) after which the
* transfer is automatically approved. Within that window, this flow allows the losing client to
* reject the transfer request.
*
* @error {@link google.registry.flows.FlowUtils.NotLoggedInException}
* @error {@link google.registry.flows.ResourceFlowUtils.BadAuthInfoForResourceException}
* @error {@link google.registry.flows.ResourceFlowUtils.ResourceDoesNotExistException}
* @error {@link google.registry.flows.ResourceFlowUtils.ResourceNotOwnedException}
* @error {@link google.registry.flows.exceptions.NotPendingTransferException}
* @error {@link ContactsProhibitedException}
*/
@Deprecated
@ReportingSpec(ActivityReportField.CONTACT_TRANSFER_REJECT)
public final class ContactTransferRejectFlow implements MutatingFlow {
@Inject ExtensionManager extensionManager;
@Inject Optional<AuthInfo> authInfo;
@Inject @RegistrarId String registrarId;
@Inject @TargetId String targetId;
@Inject ContactHistory.Builder historyBuilder;
@Inject EppResponse.Builder responseBuilder;
public final class ContactTransferRejectFlow extends ContactsProhibitedFlow {
@Inject ContactTransferRejectFlow() {}
@Override
public EppResponse run() throws EppException {
extensionManager.register(MetadataExtension.class);
validateRegistrarIsLoggedIn(registrarId);
extensionManager.validate();
DateTime now = tm().getTransactionTime();
Contact existingContact = loadAndVerifyExistence(Contact.class, targetId, now);
verifyOptionalAuthInfo(authInfo, existingContact);
verifyHasPendingTransfer(existingContact);
verifyResourceOwnership(registrarId, existingContact);
Contact newContact =
denyPendingTransfer(existingContact, TransferStatus.CLIENT_REJECTED, now, registrarId);
ContactHistory contactHistory =
historyBuilder.setType(CONTACT_TRANSFER_REJECT).setContact(newContact).build();
PollMessage gainingPollMessage =
createGainingTransferPollMessage(
targetId, newContact.getTransferData(), now, contactHistory.getHistoryEntryId());
tm().insertAll(ImmutableSet.of(contactHistory, gainingPollMessage));
tm().update(newContact);
// Delete the billing event and poll messages that were written in case the transfer would have
// been implicitly server approved.
tm().delete(existingContact.getTransferData().getServerApproveEntities());
return responseBuilder
.setResData(createTransferResponse(targetId, newContact.getTransferData()))
.build();
}
}
@@ -14,162 +14,20 @@
package google.registry.flows.contact;
import static google.registry.flows.FlowUtils.createHistoryEntryId;
import static google.registry.flows.FlowUtils.validateRegistrarIsLoggedIn;
import static google.registry.flows.ResourceFlowUtils.loadAndVerifyExistence;
import static google.registry.flows.ResourceFlowUtils.verifyAuthInfo;
import static google.registry.flows.ResourceFlowUtils.verifyAuthInfoPresentForResourceTransfer;
import static google.registry.flows.ResourceFlowUtils.verifyNoDisallowedStatuses;
import static google.registry.flows.contact.ContactFlowUtils.createGainingTransferPollMessage;
import static google.registry.flows.contact.ContactFlowUtils.createLosingTransferPollMessage;
import static google.registry.flows.contact.ContactFlowUtils.createTransferResponse;
import static google.registry.model.eppoutput.Result.Code.SUCCESS_WITH_ACTION_PENDING;
import static google.registry.model.reporting.HistoryEntry.Type.CONTACT_TRANSFER_REQUEST;
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
import com.google.common.collect.ImmutableSet;
import google.registry.config.RegistryConfig.Config;
import google.registry.flows.EppException;
import google.registry.flows.ExtensionManager;
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.AlreadyPendingTransferException;
import google.registry.flows.exceptions.ObjectAlreadySponsoredException;
import google.registry.model.contact.Contact;
import google.registry.model.contact.ContactHistory;
import google.registry.model.domain.metadata.MetadataExtension;
import google.registry.model.eppcommon.AuthInfo;
import google.registry.model.eppcommon.StatusValue;
import google.registry.model.eppcommon.Trid;
import google.registry.model.eppoutput.EppResponse;
import google.registry.model.poll.PollMessage;
import google.registry.model.reporting.HistoryEntry.HistoryEntryId;
import google.registry.flows.exceptions.ContactsProhibitedException;
import google.registry.model.reporting.IcannReportingTypes.ActivityReportField;
import google.registry.model.transfer.ContactTransferData;
import google.registry.model.transfer.TransferStatus;
import jakarta.inject.Inject;
import java.util.Optional;
import org.joda.time.DateTime;
import org.joda.time.Duration;
/**
* An EPP flow that requests a transfer on a contact.
* An EPP flow that is meant to request a transfer on a contact.
*
* <p>The "gaining" registrar requests a transfer from the "losing" (aka current) registrar. The
* losing registrar has a "transfer" time period to respond (by default five days) after which the
* transfer is automatically approved. Within that window, the transfer might be approved explicitly
* by the losing registrar or rejected, and the gaining registrar can also cancel the transfer
* request.
*
* @error {@link google.registry.flows.FlowUtils.NotLoggedInException}
* @error {@link google.registry.flows.ResourceFlowUtils.BadAuthInfoForResourceException}
* @error {@link google.registry.flows.ResourceFlowUtils.ResourceDoesNotExistException}
* @error {@link google.registry.flows.exceptions.AlreadyPendingTransferException}
* @error {@link google.registry.flows.exceptions.MissingTransferRequestAuthInfoException}
* @error {@link google.registry.flows.exceptions.ObjectAlreadySponsoredException}
* @error {@link google.registry.flows.exceptions.ResourceStatusProhibitsOperationException}
* @error {@link ContactsProhibitedException}
*/
@Deprecated
@ReportingSpec(ActivityReportField.CONTACT_TRANSFER_REQUEST)
public final class ContactTransferRequestFlow implements MutatingFlow {
private static final ImmutableSet<StatusValue> DISALLOWED_STATUSES =
ImmutableSet.of(
StatusValue.CLIENT_TRANSFER_PROHIBITED,
StatusValue.PENDING_DELETE,
StatusValue.SERVER_TRANSFER_PROHIBITED);
@Inject ExtensionManager extensionManager;
@Inject Optional<AuthInfo> authInfo;
@Inject @RegistrarId String gainingClientId;
@Inject @TargetId String targetId;
@Inject
@Config("contactAutomaticTransferLength")
Duration automaticTransferLength;
@Inject ContactHistory.Builder historyBuilder;
@Inject Trid trid;
@Inject EppResponse.Builder responseBuilder;
public final class ContactTransferRequestFlow extends ContactsProhibitedFlow {
@Inject
ContactTransferRequestFlow() {}
@Override
public EppResponse run() throws EppException {
extensionManager.register(MetadataExtension.class);
validateRegistrarIsLoggedIn(gainingClientId);
extensionManager.validate();
DateTime now = tm().getTransactionTime();
Contact existingContact = loadAndVerifyExistence(Contact.class, targetId, now);
verifyAuthInfoPresentForResourceTransfer(authInfo);
verifyAuthInfo(authInfo.get(), existingContact);
// Verify that the resource does not already have a pending transfer.
if (TransferStatus.PENDING.equals(existingContact.getTransferData().getTransferStatus())) {
throw new AlreadyPendingTransferException(targetId);
}
String losingClientId = existingContact.getCurrentSponsorRegistrarId();
// Verify that this client doesn't already sponsor this resource.
if (gainingClientId.equals(losingClientId)) {
throw new ObjectAlreadySponsoredException();
}
verifyNoDisallowedStatuses(existingContact, DISALLOWED_STATUSES);
DateTime transferExpirationTime = now.plus(automaticTransferLength);
ContactTransferData serverApproveTransferData =
new ContactTransferData.Builder()
.setTransferRequestTime(now)
.setTransferRequestTrid(trid)
.setGainingRegistrarId(gainingClientId)
.setLosingRegistrarId(losingClientId)
.setPendingTransferExpirationTime(transferExpirationTime)
.setTransferStatus(TransferStatus.SERVER_APPROVED)
.build();
HistoryEntryId contactHistoryId = createHistoryEntryId(existingContact);
historyBuilder
.setRevisionId(contactHistoryId.getRevisionId())
.setType(CONTACT_TRANSFER_REQUEST);
// If the transfer is server approved, this message will be sent to the losing registrar. */
PollMessage serverApproveLosingPollMessage =
createLosingTransferPollMessage(targetId, serverApproveTransferData, contactHistoryId);
// If the transfer is server approved, this message will be sent to the gaining registrar. */
PollMessage serverApproveGainingPollMessage =
createGainingTransferPollMessage(
targetId, serverApproveTransferData, now, contactHistoryId);
ContactTransferData pendingTransferData =
serverApproveTransferData
.asBuilder()
.setTransferStatus(TransferStatus.PENDING)
.setServerApproveEntities(
serverApproveGainingPollMessage.getContactRepoId(),
contactHistoryId.getRevisionId(),
ImmutableSet.of(
serverApproveGainingPollMessage.createVKey(),
serverApproveLosingPollMessage.createVKey()))
.build();
// When a transfer is requested, a poll message is created to notify the losing registrar.
PollMessage requestPollMessage =
createLosingTransferPollMessage(targetId, pendingTransferData, contactHistoryId)
.asBuilder()
.setEventTime(now) // Unlike the serverApprove messages, this applies immediately.
.build();
Contact newContact =
existingContact
.asBuilder()
.setTransferData(pendingTransferData)
.addStatusValue(StatusValue.PENDING_TRANSFER)
.build();
tm().update(newContact);
tm().insertAll(
ImmutableSet.of(
historyBuilder.setContact(newContact).build(),
requestPollMessage,
serverApproveGainingPollMessage,
serverApproveLosingPollMessage));
return responseBuilder
.setResultFromCode(SUCCESS_WITH_ACTION_PENDING)
.setResData(createTransferResponse(targetId, newContact.getTransferData()))
.build();
}
}
@@ -14,158 +14,19 @@
package google.registry.flows.contact;
import static com.google.common.collect.Sets.union;
import static google.registry.flows.FlowUtils.validateRegistrarIsLoggedIn;
import static google.registry.flows.ResourceFlowUtils.checkSameValuesNotAddedAndRemoved;
import static google.registry.flows.ResourceFlowUtils.loadAndVerifyExistence;
import static google.registry.flows.ResourceFlowUtils.verifyAllStatusesAreClientSettable;
import static google.registry.flows.ResourceFlowUtils.verifyNoDisallowedStatuses;
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;
import com.google.common.collect.ImmutableSet;
import google.registry.flows.EppException;
import google.registry.flows.ExtensionManager;
import google.registry.flows.FlowModule.RegistrarId;
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;
import google.registry.model.contact.ContactHistory;
import google.registry.model.contact.PostalInfo;
import google.registry.model.domain.metadata.MetadataExtension;
import google.registry.model.eppcommon.AuthInfo;
import google.registry.model.eppcommon.StatusValue;
import google.registry.model.eppinput.ResourceCommand;
import google.registry.model.eppoutput.EppResponse;
import google.registry.model.reporting.IcannReportingTypes.ActivityReportField;
import jakarta.inject.Inject;
import java.util.Optional;
import javax.annotation.Nullable;
import org.joda.time.DateTime;
/**
* An EPP flow that updates a contact.
* An EPP flow meant to update 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}
* @error {@link google.registry.flows.ResourceFlowUtils.ResourceNotOwnedException}
* @error {@link google.registry.flows.ResourceFlowUtils.StatusNotClientSettableException}
* @error {@link google.registry.flows.exceptions.ResourceHasClientUpdateProhibitedException}
* @error {@link google.registry.flows.exceptions.ResourceStatusProhibitsOperationException}
* @error {@link ContactFlowUtils.BadInternationalizedPostalInfoException}
* @error {@link ContactFlowUtils.DeclineContactDisclosureFieldDisallowedPolicyException}
*/
@Deprecated
@ReportingSpec(ActivityReportField.CONTACT_UPDATE)
public final class ContactUpdateFlow implements MutatingFlow {
/**
* Note that CLIENT_UPDATE_PROHIBITED is intentionally not in this list. This is because it
* requires special checking, since you must be able to clear the status off the object with an
* update.
*/
private static final ImmutableSet<StatusValue> DISALLOWED_STATUSES = ImmutableSet.of(
StatusValue.PENDING_DELETE,
StatusValue.SERVER_UPDATE_PROHIBITED);
@Inject ResourceCommand resourceCommand;
@Inject ExtensionManager extensionManager;
@Inject Optional<AuthInfo> authInfo;
@Inject @RegistrarId String registrarId;
@Inject @TargetId String targetId;
@Inject @Superuser boolean isSuperuser;
@Inject ContactHistory.Builder historyBuilder;
@Inject EppResponse.Builder responseBuilder;
public final class ContactUpdateFlow extends ContactsProhibitedFlow {
@Inject ContactUpdateFlow() {}
@Override
public EppResponse run() throws EppException {
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);
verifyOptionalAuthInfo(authInfo, existingContact);
ImmutableSet<StatusValue> statusToRemove = command.getInnerRemove().getStatusValues();
ImmutableSet<StatusValue> statusesToAdd = command.getInnerAdd().getStatusValues();
if (!isSuperuser) { // The superuser can update any contact and set any status.
verifyResourceOwnership(registrarId, existingContact);
verifyAllStatusesAreClientSettable(union(statusesToAdd, statusToRemove));
}
verifyNoDisallowedStatuses(existingContact, DISALLOWED_STATUSES);
checkSameValuesNotAddedAndRemoved(statusesToAdd, statusToRemove);
Contact.Builder builder = existingContact.asBuilder();
Change change = command.getInnerChange();
// The spec requires the following behaviors:
// * If you update part of a postal info, the fields that you didn't update are unchanged.
// * If you update one postal info but not the other, the other is deleted.
// Therefore, if you want to preserve one postal info and update another you need to send the
// update and also something that technically updates the preserved one, even if it only
// "updates" it by setting just one field to the same value.
PostalInfo internationalized = change.getInternationalizedPostalInfo();
PostalInfo localized = change.getLocalizedPostalInfo();
if (internationalized != null) {
builder.overlayInternationalizedPostalInfo(internationalized);
if (localized == null) {
builder.setLocalizedPostalInfo(null);
}
}
if (localized != null) {
builder.overlayLocalizedPostalInfo(localized);
if (internationalized == null) {
builder.setInternationalizedPostalInfo(null);
}
}
Contact newContact =
builder
.setLastEppUpdateTime(now)
.setLastEppUpdateRegistrarId(registrarId)
.setAuthInfo(preferFirst(change.getAuthInfo(), existingContact.getAuthInfo()))
.setDisclose(preferFirst(change.getDisclose(), existingContact.getDisclose()))
.setEmailAddress(preferFirst(change.getEmail(), existingContact.getEmailAddress()))
.setFaxNumber(preferFirst(change.getFax(), existingContact.getFaxNumber()))
.setVoiceNumber(preferFirst(change.getVoice(), existingContact.getVoiceNumber()))
.addStatusValues(statusesToAdd)
.removeStatusValues(statusToRemove)
.build();
// If the resource is marked with clientUpdateProhibited, and this update did not clear that
// status, then the update must be disallowed (unless a superuser is requesting the change).
if (!isSuperuser
&& existingContact.getStatusValues().contains(StatusValue.CLIENT_UPDATE_PROHIBITED)
&& newContact.getStatusValues().contains(StatusValue.CLIENT_UPDATE_PROHIBITED)) {
throw new ResourceHasClientUpdateProhibitedException();
}
validateAsciiPostalInfo(newContact.getInternationalizedPostalInfo());
validateContactAgainstPolicy(newContact);
historyBuilder
.setType(CONTACT_UPDATE)
.setXmlBytes(null) // We don't want to store contact details in the history entry.
.setContact(newContact);
tm().insert(historyBuilder.build());
tm().update(newContact);
return responseBuilder.build();
}
/** Return the first non-null param, or null if both are null. */
@Nullable
private static <T> T preferFirst(@Nullable T a, @Nullable T b) {
return a != null ? a : b;
}
}
@@ -0,0 +1,28 @@
// 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.contact;
import google.registry.flows.EppException;
import google.registry.flows.Flow;
import google.registry.flows.exceptions.ContactsProhibitedException;
import google.registry.model.eppoutput.EppResponse;
/** Nomulus follows the Minimum Dataset Requirements, meaning it stores no contact information. */
public abstract class ContactsProhibitedFlow implements Flow {
@Override
public EppResponse run() throws EppException {
throw new ContactsProhibitedException();
}
}
@@ -108,7 +108,8 @@ public final class DomainClaimsCheckFlow implements TransactionalFlow {
verifyClaimsPeriodNotEnded(tld, now);
}
}
Optional<String> claimKey = ClaimsListDao.get().getClaimKey(parsedDomain.parts().get(0));
Optional<String> claimKey =
ClaimsListDao.get(tldStr).getClaimKey(parsedDomain.parts().get(0));
launchChecksBuilder.add(
LaunchCheck.create(
LaunchCheckName.create(claimKey.isPresent(), domainName), claimKey.orElse(null)));
@@ -280,7 +280,7 @@ public final class DomainCreateFlow implements MutatingFlow {
checkAllowedAccessToTld(registrarId, tld.getTldStr());
checkHasBillingAccount(registrarId, tld.getTldStr());
boolean isValidReservedCreate = isValidReservedCreate(domainName, allocationToken);
ClaimsList claimsList = ClaimsListDao.get();
ClaimsList claimsList = ClaimsListDao.get(tld.getTldStr());
verifyIsGaOrSpecialCase(
tld,
claimsList,
@@ -312,7 +312,8 @@ public final class DomainCreateFlow implements MutatingFlow {
// at this point so that we can verify it before the "after validation" extension point.
signedMarkId =
tmchUtils
.verifySignedMarks(launchCreate.get().getSignedMarks(), domainLabel, now)
.verifySignedMarks(
tld.getTldStr(), launchCreate.get().getSignedMarks(), domainLabel, now)
.getId();
}
verifyNotBlockedByBsa(domainName, tld, now, allocationToken);
@@ -55,7 +55,7 @@ public final class DomainFlowTmchUtils {
}
public SignedMark verifySignedMarks(
ImmutableList<AbstractSignedMark> signedMarks, String domainLabel, DateTime now)
String tld, ImmutableList<AbstractSignedMark> signedMarks, String domainLabel, DateTime now)
throws EppException {
if (signedMarks.size() > 1) {
throw new TooManySignedMarksException();
@@ -64,7 +64,7 @@ public final class DomainFlowTmchUtils {
throw new SignedMarksMustBeEncodedException();
}
SignedMark signedMark =
verifyEncodedSignedMark((EncodedSignedMark) signedMarks.get(0), now);
verifyEncodedSignedMark(tld, (EncodedSignedMark) signedMarks.get(0), now);
return verifySignedMarkValidForDomainLabel(signedMark, domainLabel);
}
@@ -76,8 +76,9 @@ public final class DomainFlowTmchUtils {
return signedMark;
}
public SignedMark verifyEncodedSignedMark(EncodedSignedMark encodedSignedMark, DateTime now)
throws EppException {
// TODO(b/412715713): remove the tld parameter when RST completes.
public SignedMark verifyEncodedSignedMark(
String tld, EncodedSignedMark encodedSignedMark, DateTime now) throws EppException {
if (!encodedSignedMark.getEncoding().equals("base64")) {
throw new Base64RequiredForEncodedSignedMarksException();
}
@@ -95,7 +96,7 @@ public final class DomainFlowTmchUtils {
throw new SignedMarkParsingErrorException();
}
if (SignedMarkRevocationList.get().isSmdRevoked(signedMark.getId(), now)) {
if (SignedMarkRevocationList.get(tld).isSmdRevoked(signedMark.getId(), now)) {
throw new SignedMarkRevokedErrorException();
}
@@ -218,7 +218,7 @@ public class DomainFlowUtils {
return domainName;
}
private static void validateFirstLabel(String firstLabel) throws EppException {
public static void validateFirstLabel(String firstLabel) throws EppException {
if (firstLabel.length() > MAX_LABEL_SIZE) {
throw new DomainLabelTooLongException();
}
@@ -1240,49 +1240,49 @@ public class DomainFlowUtils {
}
/** Domain names can only contain a-z, 0-9, '.' and '-'. */
static class BadDomainNameCharacterException extends ParameterValuePolicyErrorException {
static class BadDomainNameCharacterException extends ParameterValueSyntaxErrorException {
public BadDomainNameCharacterException() {
super("Domain names can only contain a-z, 0-9, '.' and '-'");
}
}
/** Non-IDN domain names cannot contain hyphens in the third or fourth position. */
static class DashesInThirdAndFourthException extends ParameterValuePolicyErrorException {
static class DashesInThirdAndFourthException extends ParameterValueSyntaxErrorException {
public DashesInThirdAndFourthException() {
super("Non-IDN domain names cannot contain dashes in the third or fourth position");
}
}
/** Domain labels cannot begin with a dash. */
static class LeadingDashException extends ParameterValuePolicyErrorException {
static class LeadingDashException extends ParameterValueSyntaxErrorException {
public LeadingDashException() {
super("Domain labels cannot begin with a dash");
}
}
/** Domain labels cannot end with a dash. */
static class TrailingDashException extends ParameterValuePolicyErrorException {
static class TrailingDashException extends ParameterValueSyntaxErrorException {
public TrailingDashException() {
super("Domain labels cannot end with a dash");
}
}
/** Domain labels cannot be longer than 63 characters. */
static class DomainLabelTooLongException extends ParameterValuePolicyErrorException {
static class DomainLabelTooLongException extends ParameterValueSyntaxErrorException {
public DomainLabelTooLongException() {
super("Domain labels cannot be longer than 63 characters");
}
}
/** No part of a domain name can be empty. */
static class EmptyDomainNamePartException extends ParameterValuePolicyErrorException {
static class EmptyDomainNamePartException extends ParameterValueSyntaxErrorException {
public EmptyDomainNamePartException() {
super("No part of a domain name can be empty");
}
}
/** Domain name starts with xn-- but is not a valid IDN. */
static class InvalidPunycodeException extends ParameterValuePolicyErrorException {
static class InvalidPunycodeException extends ParameterValueSyntaxErrorException {
public InvalidPunycodeException() {
super("Domain name starts with xn-- but is not a valid IDN");
}
@@ -65,6 +65,7 @@ public final class HostCheckFlow implements TransactionalFlow {
ForeignKeyUtils.loadKeys(Host.class, hostnames, clock.nowUtc()).keySet();
ImmutableList.Builder<HostCheck> checks = new ImmutableList.Builder<>();
for (String hostname : hostnames) {
HostFlowUtils.validateHostName(hostname);
boolean unused = !existingIds.contains(hostname);
checks.add(HostCheck.create(unused, hostname, unused ? null : "In use"));
}
@@ -116,6 +116,7 @@ public final class HostCreateFlow implements MutatingFlow {
? new SubordinateHostMustHaveIpException()
: new UnexpectedExternalHostIpException();
}
HostFlowUtils.validateInetAddresses(command.getInetAddresses());
Host newHost =
new Host.Builder()
.setCreationRegistrarId(registrarId)
@@ -14,12 +14,15 @@
package google.registry.flows.host;
import static google.registry.flows.domain.DomainFlowUtils.validateFirstLabel;
import static google.registry.model.EppResourceUtils.isActive;
import static google.registry.model.tld.Tlds.findTldForName;
import static google.registry.util.PreconditionsUtils.checkArgumentNotNull;
import static java.util.stream.Collectors.joining;
import com.google.common.base.Ascii;
import com.google.common.base.CharMatcher;
import com.google.common.collect.ImmutableSet;
import com.google.common.net.InternetDomainName;
import google.registry.flows.EppException;
import google.registry.flows.EppException.AuthorizationErrorException;
@@ -32,12 +35,17 @@ import google.registry.model.ForeignKeyUtils;
import google.registry.model.domain.Domain;
import google.registry.model.eppcommon.StatusValue;
import google.registry.util.Idn;
import java.net.InetAddress;
import java.util.Optional;
import org.joda.time.DateTime;
/** Static utility functions for host flows. */
public class HostFlowUtils {
/** Validator for ASCII lowercase letters, digits, and "-_", allowing "." as a separator */
private static final CharMatcher HOST_NAME_ALLOWED_CHARS =
CharMatcher.inRange('a', 'z').or(CharMatcher.inRange('0', '9').or(CharMatcher.anyOf("-._")));
/** Checks that a host name is valid. */
public static InternetDomainName validateHostName(String name) throws EppException {
checkArgumentNotNull(name, "Must specify host name to validate");
@@ -53,6 +61,9 @@ public class HostFlowUtils {
if (!name.equals(hostNamePunyCoded)) {
throw new HostNameNotPunyCodedException(hostNamePunyCoded);
}
if (!HOST_NAME_ALLOWED_CHARS.matchesAllOf(name)) {
throw new BadHostNameCharacterException();
}
InternetDomainName hostName = InternetDomainName.from(name);
if (!name.equals(hostName.toString())) {
throw new HostNameNotNormalizedException(hostName.toString());
@@ -71,6 +82,7 @@ public class HostFlowUtils {
if (hostName.parts().size() < effectiveTld.parts().size() + 2) {
throw new HostNameTooShallowException();
}
validateFirstLabel(hostName.parts().getFirst());
return hostName;
} catch (IllegalArgumentException e) {
throw new InvalidHostNameException();
@@ -98,6 +110,24 @@ public class HostFlowUtils {
return superordinateDomain;
}
/** Makes sure that no provided IP addresses are local / loopback addresses. */
public static void validateInetAddresses(ImmutableSet<InetAddress> inetAddresses)
throws EppException {
if (inetAddresses == null) {
return;
}
if (inetAddresses.stream().anyMatch(InetAddress::isLoopbackAddress)) {
throw new LoopbackIpNotValidForHostException();
}
}
/** Loopback IPs are not valid for hosts. */
static class LoopbackIpNotValidForHostException extends ParameterValuePolicyErrorException {
public LoopbackIpNotValidForHostException() {
super("Loopback IPs are not valid for hosts");
}
}
/** Superordinate domain for this hostname does not exist. */
static class SuperordinateDomainDoesNotExistException extends ObjectDoesNotExistException {
public SuperordinateDomainDoesNotExistException(String domainName) {
@@ -180,4 +210,11 @@ public class HostFlowUtils {
String.format("Host names must be in normalized format; expected %s", expectedHostName));
}
}
/** Host names can only contain a-z, 0-9, '.', '_', and '-'. */
static class BadHostNameCharacterException extends ParameterValueSyntaxErrorException {
public BadHostNameCharacterException() {
super("Host names can only contain a-z, 0-9, '.', '_', and '-'");
}
}
}
@@ -161,6 +161,7 @@ public final class HostUpdateFlow implements MutatingFlow {
AddRemove remove = command.getInnerRemove();
checkSameValuesNotAddedAndRemoved(add.getStatusValues(), remove.getStatusValues());
checkSameValuesNotAddedAndRemoved(add.getInetAddresses(), remove.getInetAddresses());
HostFlowUtils.validateInetAddresses(add.getInetAddresses());
VKey<Domain> newSuperordinateDomainKey =
newSuperordinateDomain.map(Domain::createVKey).orElse(null);
// If the superordinateDomain field is changing, set the lastSuperordinateChange to now.
@@ -135,7 +135,6 @@ public class FlowPicker {
return switch (((Poll) innerCommand).getPollOp()) {
case ACK -> PollAckFlow.class;
case REQUEST -> PollRequestFlow.class;
default -> UnimplementedFlow.class;
};
}
};
@@ -21,6 +21,7 @@ import static google.registry.util.DateTimeUtils.isBeforeOrAt;
import com.google.common.base.Supplier;
import com.google.common.collect.ImmutableMap;
import google.registry.model.ImmutableObject;
import google.registry.tmch.RstTmchUtils;
import jakarta.persistence.CollectionTable;
import jakarta.persistence.Column;
import jakarta.persistence.ElementCollection;
@@ -71,6 +72,11 @@ public class SignedMarkRevocationList extends ImmutableObject {
return CACHE.get();
}
// TODO(b/412715713): remove the tld parameter when RST completes.
public static SignedMarkRevocationList get(String tld) {
return RstTmchUtils.getSmdrList(tld).orElseGet(SignedMarkRevocationList::get);
}
/** Create a new {@link SignedMarkRevocationList} without saving it. */
public static SignedMarkRevocationList create(
DateTime creationTime, ImmutableMap<String, DateTime> revokes) {
@@ -1034,12 +1034,13 @@ public class Tld extends ImmutableObject implements Buildable, UnsafeSerializabl
return this;
}
public static final Pattern ROID_SUFFIX_PATTERN = Pattern.compile("^[A-Z\\d_]{1,8}$");
public static final Pattern ROID_SUFFIX_PATTERN = Pattern.compile("^[A-Z\\d]{1,8}$");
public Builder setRoidSuffix(String roidSuffix) {
checkArgument(
ROID_SUFFIX_PATTERN.matcher(roidSuffix).matches(),
"ROID suffix must be in format %s",
"ROID suffix %s must be in format %s",
roidSuffix,
ROID_SUFFIX_PATTERN.pattern());
getInstance().roidSuffix = roidSuffix;
return this;
@@ -22,6 +22,7 @@ import com.github.benmanes.caffeine.cache.LoadingCache;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.ImmutableMap;
import google.registry.model.CacheUtils;
import google.registry.tmch.RstTmchUtils;
import java.time.Duration;
import java.util.Optional;
@@ -72,6 +73,11 @@ public class ClaimsListDao {
return CACHE.get(ClaimsListDao.class);
}
// TODO(b/412715713): remove the tld parameter when RST completes.
public static ClaimsList get(String tld) {
return RstTmchUtils.getClaimsList(tld).orElseGet(ClaimsListDao::get);
}
/**
* Returns the most recent revision of the {@link ClaimsList} in SQL or an empty list if it
* doesn't exist.
@@ -62,6 +62,8 @@ import google.registry.module.ReadinessProbeAction.ReadinessProbeActionFrontend;
import google.registry.module.ReadinessProbeAction.ReadinessProbeActionPubApi;
import google.registry.module.ReadinessProbeAction.ReadinessProbeConsoleAction;
import google.registry.monitoring.whitebox.WhiteboxModule;
import google.registry.mosapi.GetServiceStateAction;
import google.registry.mosapi.module.MosApiRequestModule;
import google.registry.rdap.RdapAutnumAction;
import google.registry.rdap.RdapDomainAction;
import google.registry.rdap.RdapDomainSearchAction;
@@ -151,6 +153,7 @@ import google.registry.ui.server.console.settings.SecurityAction;
EppToolModule.class,
IcannReportingModule.class,
LoadTestModule.class,
MosApiRequestModule.class,
RdapModule.class,
RdeModule.class,
ReportingModule.class,
@@ -232,6 +235,8 @@ interface RequestComponent {
GenerateZoneFilesAction generateZoneFilesAction();
GetServiceStateAction getServiceStateAction();
IcannReportingStagingAction icannReportingStagingAction();
IcannReportingUploadAction icannReportingUploadAction();
@@ -0,0 +1,68 @@
// 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.mosapi;
import com.google.common.net.MediaType;
import com.google.gson.Gson;
import google.registry.request.Action;
import google.registry.request.HttpException.ServiceUnavailableException;
import google.registry.request.Parameter;
import google.registry.request.Response;
import google.registry.request.auth.Auth;
import jakarta.inject.Inject;
import java.util.Optional;
/** An action that returns the current MoSAPI service state for a given TLD or all TLDs. */
@Action(
service = Action.Service.BACKEND,
path = GetServiceStateAction.PATH,
method = Action.Method.GET,
auth = Auth.AUTH_ADMIN)
public class GetServiceStateAction implements Runnable {
public static final String PATH = "/_dr/mosapi/getServiceState";
public static final String TLD_PARAM = "tld";
private final MosApiStateService stateService;
private final Response response;
private final Gson gson;
private final Optional<String> tld;
@Inject
public GetServiceStateAction(
MosApiStateService stateService,
Response response,
Gson gson,
@Parameter(TLD_PARAM) Optional<String> tld) {
this.stateService = stateService;
this.response = response;
this.gson = gson;
this.tld = tld;
}
@Override
public void run() {
response.setContentType(MediaType.JSON_UTF_8);
try {
if (tld.isPresent()) {
response.setPayload(gson.toJson(stateService.getServiceStateSummary(tld.get())));
} else {
response.setPayload(gson.toJson(stateService.getAllServiceStateSummaries()));
}
} catch (MosApiException e) {
throw new ServiceUnavailableException("Error fetching MoSAPI service state.");
}
}
}
@@ -12,7 +12,9 @@
// See the License for the specific language governing permissions and
// limitations under the License.
package google.registry.mosapi.model;
package google.registry.mosapi;
import com.google.gson.annotations.Expose;
/**
* Represents the generic JSON error response from the MoSAPI service for a 400 Bad Request.
@@ -20,4 +22,5 @@ package google.registry.mosapi.model;
* @see <a href="https://www.icann.org/mosapi-specification.pdf">ICANN MoSAPI Specification, Section
* 8</a>
*/
public record MosApiErrorResponse(String resultCode, String message, String description) {}
public record MosApiErrorResponse(
@Expose String resultCode, @Expose String message, @Expose String description) {}
@@ -17,7 +17,6 @@ package google.registry.mosapi;
import static java.lang.annotation.ElementType.TYPE;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
import google.registry.mosapi.model.MosApiErrorResponse;
import java.io.IOException;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
@@ -42,6 +41,11 @@ public class MosApiException extends IOException {
this.errorResponse = null;
}
public MosApiException(String message) {
super(message);
this.errorResponse = null;
}
public Optional<MosApiErrorResponse> getErrorResponse() {
return Optional.ofNullable(errorResponse);
}
@@ -0,0 +1,122 @@
// 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.mosapi;
import static google.registry.util.CollectionUtils.nullToEmptyImmutableCopy;
import com.google.gson.annotations.Expose;
import com.google.gson.annotations.SerializedName;
import java.util.List;
import java.util.Map;
import javax.annotation.Nullable;
/** Data models for ICANN MoSAPI. */
public final class MosApiModels {
private MosApiModels() {}
/**
* A wrapper response containing the state summaries of all monitored services.
*
* <p>This corresponds to the collection of service statuses returned when monitoring the state of
* a TLD
*
* @see <a href="https://www.icann.org/mosapi-specification.pdf">ICANN MoSAPI Specification,
* Section 5.1</a>
*/
public record AllServicesStateResponse(
// A list of state summaries for each monitored service (e.g. DNS, RDDS, etc.)
@Expose List<ServiceStateSummary> serviceStates) {
public AllServicesStateResponse {
serviceStates = nullToEmptyImmutableCopy(serviceStates);
}
}
/**
* A summary of a service incident.
*
* @see <a href="https://www.icann.org/mosapi-specification.pdf">ICANN MoSAPI Specification,
* Section 5.1</a>
*/
public record IncidentSummary(
@Expose String incidentID,
@Expose long startTime,
@Expose boolean falsePositive,
@Expose String state,
@Expose @Nullable Long endTime) {}
/**
* A curated summary of the service state for a TLD.
*
* <p>This class aggregates the high-level status of a TLD and details of any active incidents
* affecting specific services (like DNS or RDDS), based on the data structures defined in the
* MoSAPI specification.
*
* @see <a href="https://www.icann.org/mosapi-specification.pdf">ICANN MoSAPI Specification,
* Section 5.1</a>
*/
public record ServiceStateSummary(
@Expose String tld,
@Expose String overallStatus,
@Expose List<ServiceStatus> activeIncidents) {
public ServiceStateSummary {
activeIncidents = nullToEmptyImmutableCopy(activeIncidents);
}
}
/** Represents the status of a single monitored service. */
public record ServiceStatus(
/**
* A JSON string that contains the status of the Service as seen from the monitoring system.
* Possible values include "Up", "Down", "Disabled", "UP-inconclusive-no-data", etc.
*/
@Expose String status,
// A JSON number that contains the current percentage of the Emergency Threshold
// of the Service. A value of "0" specifies that there are no Incidents
// affecting the threshold.
@Expose double emergencyThreshold,
@Expose List<IncidentSummary> incidents) {
public ServiceStatus {
incidents = nullToEmptyImmutableCopy(incidents);
}
}
/**
* Represents the overall health of all monitored services for a TLD.
*
* @see <a href="https://www.icann.org/mosapi-specification.pdf">ICANN MoSAPI Specification,
* Section 5.1</a>
*/
public record TldServiceState(
@Expose String tld,
long lastUpdateApiDatabase,
// A JSON string that contains the status of the TLD as seen from the monitoring system
@Expose String status,
// A JSON object containing detailed information for each potential monitored service (i.e.,
// DNS,
// RDDS, EPP, DNSSEC, RDAP).
@Expose @SerializedName("testedServices") Map<String, ServiceStatus> serviceStatuses) {
public TldServiceState {
serviceStatuses = nullToEmptyImmutableCopy(serviceStatuses);
}
}
}
@@ -0,0 +1,110 @@
// 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.mosapi;
import static com.google.common.collect.ImmutableList.toImmutableList;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.common.flogger.FluentLogger;
import google.registry.config.RegistryConfig.Config;
import google.registry.mosapi.MosApiModels.AllServicesStateResponse;
import google.registry.mosapi.MosApiModels.ServiceStateSummary;
import google.registry.mosapi.MosApiModels.ServiceStatus;
import google.registry.mosapi.MosApiModels.TldServiceState;
import jakarta.inject.Inject;
import jakarta.inject.Named;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutorService;
/** A service that provides business logic for interacting with MoSAPI Service State. */
public class MosApiStateService {
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
private final ServiceMonitoringClient serviceMonitoringClient;
private final ExecutorService tldExecutor;
private final ImmutableSet<String> tlds;
private static final String DOWN_STATUS = "Down";
private static final String FETCH_ERROR_STATUS = "ERROR";
@Inject
public MosApiStateService(
ServiceMonitoringClient serviceMonitoringClient,
@Config("mosapiTlds") ImmutableSet<String> tlds,
@Named("mosapiTldExecutor") ExecutorService tldExecutor) {
this.serviceMonitoringClient = serviceMonitoringClient;
this.tlds = tlds;
this.tldExecutor = tldExecutor;
}
/** Fetches and transforms the service state for a given TLD into a summary. */
public ServiceStateSummary getServiceStateSummary(String tld) throws MosApiException {
TldServiceState rawState = serviceMonitoringClient.getTldServiceState(tld);
return transformToSummary(rawState);
}
/** Fetches and transforms the service state for all configured TLDs. */
public AllServicesStateResponse getAllServiceStateSummaries() {
ImmutableList<CompletableFuture<ServiceStateSummary>> futures =
tlds.stream()
.map(
tld ->
CompletableFuture.supplyAsync(
() -> {
try {
return getServiceStateSummary(tld);
} catch (MosApiException e) {
logger.atWarning().withCause(e).log(
"Failed to get service state for TLD %s.", tld);
// we don't want to throw exception if fetch failed
return new ServiceStateSummary(tld, FETCH_ERROR_STATUS, null);
}
},
tldExecutor))
.collect(ImmutableList.toImmutableList());
ImmutableList<ServiceStateSummary> summaries =
futures.stream()
.map(CompletableFuture::join) // Waits for all tasks to complete
.collect(toImmutableList());
return new AllServicesStateResponse(summaries);
}
private ServiceStateSummary transformToSummary(TldServiceState rawState) {
ImmutableList<ServiceStatus> activeIncidents = ImmutableList.of();
if (DOWN_STATUS.equalsIgnoreCase(rawState.status())) {
activeIncidents =
rawState.serviceStatuses().entrySet().stream()
.filter(
entry -> {
ServiceStatus serviceStatus = entry.getValue();
return serviceStatus.incidents() != null
&& !serviceStatus.incidents().isEmpty();
})
.map(
entry ->
new ServiceStatus(
// key is the service name
entry.getKey(),
entry.getValue().emergencyThreshold(),
entry.getValue().incidents()))
.collect(toImmutableList());
}
return new ServiceStateSummary(rawState.tld(), rawState.status(), activeIncidents);
}
}
@@ -0,0 +1,80 @@
// 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.mosapi;
import com.google.common.base.Throwables;
import com.google.gson.Gson;
import com.google.gson.JsonParseException;
import google.registry.mosapi.MosApiModels.TldServiceState;
import jakarta.inject.Inject;
import java.io.IOException;
import java.util.Collections;
import okhttp3.Response;
import okhttp3.ResponseBody;
/** Facade for MoSAPI's service monitoring endpoints. */
public class ServiceMonitoringClient {
private static final String MONITORING_STATE_ENDPOINT = "v2/monitoring/state";
private final MosApiClient mosApiClient;
private final Gson gson;
@Inject
public ServiceMonitoringClient(MosApiClient mosApiClient, Gson gson) {
this.mosApiClient = mosApiClient;
this.gson = gson;
}
/**
* Fetches the current state of all monitored services for a given TLD.
*
* @see <a href="https://www.icann.org/mosapi-specification.pdf">ICANN MoSAPI Specification,
* Section 5.1</a>
*/
public TldServiceState getTldServiceState(String tld) throws MosApiException {
try (Response response =
mosApiClient.sendGetRequest(
tld, MONITORING_STATE_ENDPOINT, Collections.emptyMap(), Collections.emptyMap())) {
ResponseBody responseBody = response.body();
if (responseBody == null) {
throw new MosApiException(
String.format(
"MoSAPI Service Monitoring API " + "returned an empty body with status: %d",
response.code()));
}
String bodyString = responseBody.string();
if (!response.isSuccessful()) {
throw parseErrorResponse(response.code(), bodyString);
}
return gson.fromJson(bodyString, TldServiceState.class);
} catch (IOException | JsonParseException e) {
Throwables.throwIfInstanceOf(e, MosApiException.class);
// Catch Gson's runtime exceptions (parsing errors) and wrap them
throw new MosApiException("Failed to parse TLD service state response", e);
}
}
/** Parses an unsuccessful MoSAPI response into a domain-specific {@link MosApiException}. */
private MosApiException parseErrorResponse(int statusCode, String bodyString) {
try {
MosApiErrorResponse error = gson.fromJson(bodyString, MosApiErrorResponse.class);
return MosApiException.create(error);
} catch (JsonParseException e) {
return new MosApiException(
String.format("MoSAPI json parsing error (%d): %s", statusCode, bodyString), e);
}
}
}
@@ -32,6 +32,8 @@ import java.security.cert.Certificate;
import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;
import java.util.Optional;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import javax.net.ssl.KeyManagerFactory;
import javax.net.ssl.SSLContext;
import javax.net.ssl.TrustManagerFactory;
@@ -184,4 +186,21 @@ public final class MosApiModule {
.sslSocketFactory(sslContext.getSocketFactory(), trustManager)
.build();
}
/**
* Provides a fixed thread pool for parallel TLD processing.
*
* <p>Strictly bound to 4 threads to comply with MoSAPI session limits (4 concurrent sessions per
* certificate). This is used by MosApiStateService to fetch data in parallel.
*
* @see <a href="https://www.icann.org/mosapi-specification.pdf">ICANN MoSAPI Specification,
* Section 12.3</a>
*/
@Provides
@Singleton
@Named("mosapiTldExecutor")
static ExecutorService provideMosapiTldExecutor(
@Config("mosapiTldThreadCnt") int threadPoolSize) {
return Executors.newFixedThreadPool(threadPoolSize);
}
}
@@ -0,0 +1,33 @@
// 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.mosapi.module;
import static google.registry.request.RequestParameters.extractOptionalParameter;
import dagger.Module;
import dagger.Provides;
import google.registry.request.Parameter;
import jakarta.servlet.http.HttpServletRequest;
import java.util.Optional;
/** Dagger module for MoSAPI requests. */
@Module
public final class MosApiRequestModule {
@Provides
@Parameter("tld")
static Optional<String> provideTld(HttpServletRequest req) {
return extractOptionalParameter(req, "tld");
}
}
@@ -43,6 +43,15 @@ public enum IdnTableEnum {
*/
UNCONFUSABLE_LATIN("unconfusable_latin.txt"),
/**
* ICANN LGR 2025 Latin, but with confusable characters removed.
*
* <p>This is based on <a
* href="https://www.icann.org/sites/default/files/packages/lgr/lgr-second-level-latin-full-variant-script-24jan24-en.html">ICANN's
* LGR table</a>, but is simpler.
*/
AUGMENTED_LATIN("augmented_latin.txt"),
/**
* Japanese, as used on our existing TLD launches prior to 2023.
*
@@ -1,4 +1,4 @@
# URL: https://www.iana.org/domains/idn-tables/tables/google_latn_1.0.txt
# URL: https://www.iana.org/domains/idn-tables/tables/google_latn_3.0.txt
# Policy: https://www.registry.google/about/policies/domainabuse/
U+002D # HYPHEN-MINUS
U+0030 # DIGIT ZERO
@@ -0,0 +1,120 @@
// 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.tmch;
import static com.google.common.base.Suppliers.memoize;
import static com.google.common.io.Resources.getResource;
import static com.google.common.io.Resources.readLines;
import static google.registry.tmch.RstTmchUtils.RstEnvironment.OTE;
import static google.registry.tmch.RstTmchUtils.RstEnvironment.PROD;
import static google.registry.util.RegistryEnvironment.SANDBOX;
import static java.nio.charset.StandardCharsets.UTF_8;
import com.google.common.base.Supplier;
import com.google.common.collect.ImmutableMap;
import com.google.common.flogger.FluentLogger;
import google.registry.model.smd.SignedMarkRevocationList;
import google.registry.model.tmch.ClaimsList;
import google.registry.util.RegistryEnvironment;
import java.io.IOException;
import java.net.URL;
import java.util.Locale;
import java.util.Optional;
/**
* Utilities supporting TMCH-related RST testing in the Sandbox environment.
*
* <p>For logistic reasons we must conduct RST testing in the Sandbox environments. RST tests
* require the use of special labels hosted on their website. To isolate these labels from regular
* customers conducting onboarding tests, we manually download the test files as resources, and
* serve them up only to RST TLDs.
*/
public class RstTmchUtils {
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
/**
* The RST environments.
*
* <p>We conduct both OTE and PROD RST tests in Sandbox.
*/
enum RstEnvironment {
OTE,
PROD
}
private static final ImmutableMap<RstEnvironment, Supplier<Optional<ClaimsList>>> CLAIMS_CACHE =
ImmutableMap.of(
OTE, memoize(() -> getClaimsList(OTE)), PROD, memoize(() -> getClaimsList(PROD)));
private static final ImmutableMap<RstEnvironment, Supplier<Optional<SignedMarkRevocationList>>>
SMDRL_CACHE =
ImmutableMap.of(
OTE, memoize(() -> getSmdrList(OTE)), PROD, memoize(() -> getSmdrList(PROD)));
/** Returns appropriate test labels if {@code tld} is for RST testing; otherwise returns empty. */
public static Optional<ClaimsList> getClaimsList(String tld) {
return getRstEnvironment(tld).map(CLAIMS_CACHE::get).flatMap(Supplier::get);
}
/** Returns appropriate test labels if {@code tld} is for RST testing; otherwise returns empty. */
public static Optional<SignedMarkRevocationList> getSmdrList(String tld) {
return getRstEnvironment(tld).map(SMDRL_CACHE::get).flatMap(Supplier::get);
}
static Optional<RstEnvironment> getRstEnvironment(String tld) {
if (!RegistryEnvironment.get().equals(SANDBOX)) {
return Optional.empty();
}
if (tld.startsWith("cc-rst-test-")) {
return Optional.of(OTE);
}
if (tld.startsWith("zz--")) {
return Optional.of(PROD);
}
return Optional.empty();
}
private static Optional<ClaimsList> getClaimsList(RstEnvironment rstEnvironment) {
if (!RegistryEnvironment.get().equals(SANDBOX)) {
return Optional.empty();
}
String resourceName = rstEnvironment.name().toLowerCase(Locale.ROOT) + ".rst.dnl.csv";
URL resource = getResource(RstTmchUtils.class, resourceName);
try {
return Optional.of(ClaimsListParser.parse(readLines(resource, UTF_8)));
} catch (IOException e) {
// Do not throw.
logger.atSevere().withCause(e).log(
"Could not load Claims list %s for %s in Sandbox.", resourceName, rstEnvironment);
return Optional.empty();
}
}
private static Optional<SignedMarkRevocationList> getSmdrList(RstEnvironment rstEnvironment) {
if (!RegistryEnvironment.get().equals(SANDBOX)) {
return Optional.empty();
}
String resourceName = rstEnvironment.name().toLowerCase(Locale.ROOT) + ".rst.smdrl.csv";
URL resource = getResource(RstTmchUtils.class, resourceName);
try {
return Optional.of(SmdrlCsvParser.parse(readLines(resource, UTF_8)));
} catch (IOException e) {
// Do not throw.
logger.atSevere().withCause(e).log(
"Could not load SMDR list %s for %s in Sandbox.", resourceName, rstEnvironment);
return Optional.empty();
}
}
}
@@ -19,12 +19,12 @@ import static com.google.common.base.Strings.isNullOrEmpty;
import static com.google.common.base.Verify.verify;
import static com.google.common.collect.ImmutableList.toImmutableList;
import static com.google.common.collect.ImmutableSet.toImmutableSet;
import static google.registry.util.DomainNameUtils.canonicalizeHostname;
import static google.registry.util.RegistrarUtils.normalizeRegistrarName;
import static java.nio.charset.StandardCharsets.US_ASCII;
import static org.joda.time.DateTimeZone.UTC;
import com.beust.jcommander.Parameter;
import com.google.common.base.Ascii;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import google.registry.flows.certs.CertificateChecker;
@@ -305,20 +305,16 @@ abstract class CreateOrUpdateRegistrarCommand extends MutatingCommand {
if (!allowedTlds.isEmpty()) {
checkArgument(
addAllowedTlds.isEmpty(), "Can't specify both --allowedTlds and --addAllowedTlds");
ImmutableSet.Builder<String> allowedTldsBuilder = new ImmutableSet.Builder<>();
for (String allowedTld : allowedTlds) {
allowedTldsBuilder.add(canonicalizeHostname(allowedTld));
}
builder.setAllowedTlds(allowedTldsBuilder.build());
builder.setAllowedTlds(
allowedTlds.stream().map(Ascii::toLowerCase).collect(toImmutableSet()));
}
if (!addAllowedTlds.isEmpty()) {
ImmutableSet.Builder<String> allowedTldsBuilder = new ImmutableSet.Builder<>();
if (oldRegistrar != null) {
allowedTldsBuilder.addAll(oldRegistrar.getAllowedTlds());
}
for (String allowedTld : addAllowedTlds) {
allowedTldsBuilder.add(canonicalizeHostname(allowedTld));
}
allowedTldsBuilder.addAll(
addAllowedTlds.stream().map(Ascii::toLowerCase).collect(toImmutableSet()));
builder.setAllowedTlds(allowedTldsBuilder.build());
}
if (ipAllowList != null) {
@@ -0,0 +1,10 @@
1,2024-09-13T02:21:12.0Z
DNL,lookup-key,insertion-datetime
test---validate,2024091300/6/a/b/arJyPPf2CK7f21bVGne0qMgW0000000001,2024-09-13T02:21:12.0Z
test--validate,2024091300/6/a/b/arJyPPf2CK7f21bVGne0qMgW0000000001,2024-09-13T02:21:12.0Z
test-and-validate,2024091300/6/a/b/arJyPPf2CK7f21bVGne0qMgW0000000001,2024-09-13T02:21:12.0Z
test-andvalidate,2024091300/6/a/b/arJyPPf2CK7f21bVGne0qMgW0000000001,2024-09-13T02:21:12.0Z
test-validate,2024091300/6/a/b/arJyPPf2CK7f21bVGne0qMgW0000000001,2024-09-13T02:21:12.0Z
testand-validate,2024091300/6/a/b/arJyPPf2CK7f21bVGne0qMgW0000000001,2024-09-13T02:21:12.0Z
testandvalidate,2024091300/6/a/b/arJyPPf2CK7f21bVGne0qMgW0000000001,2024-09-13T02:21:12.0Z
testvalidate,2024091300/6/a/b/arJyPPf2CK7f21bVGne0qMgW0000000001,2024-09-13T02:21:12.0Z
1 1,2024-09-13T02:21:12.0Z
2 DNL,lookup-key,insertion-datetime
3 test---validate,2024091300/6/a/b/arJyPPf2CK7f21bVGne0qMgW0000000001,2024-09-13T02:21:12.0Z
4 test--validate,2024091300/6/a/b/arJyPPf2CK7f21bVGne0qMgW0000000001,2024-09-13T02:21:12.0Z
5 test-and-validate,2024091300/6/a/b/arJyPPf2CK7f21bVGne0qMgW0000000001,2024-09-13T02:21:12.0Z
6 test-andvalidate,2024091300/6/a/b/arJyPPf2CK7f21bVGne0qMgW0000000001,2024-09-13T02:21:12.0Z
7 test-validate,2024091300/6/a/b/arJyPPf2CK7f21bVGne0qMgW0000000001,2024-09-13T02:21:12.0Z
8 testand-validate,2024091300/6/a/b/arJyPPf2CK7f21bVGne0qMgW0000000001,2024-09-13T02:21:12.0Z
9 testandvalidate,2024091300/6/a/b/arJyPPf2CK7f21bVGne0qMgW0000000001,2024-09-13T02:21:12.0Z
10 testvalidate,2024091300/6/a/b/arJyPPf2CK7f21bVGne0qMgW0000000001,2024-09-13T02:21:12.0Z
@@ -0,0 +1,7 @@
1,2022-11-22T01:49:36.9Z
smd-id,insertion-datetime
0000001761385117375880-65535,2013-07-15T00:00:00.0Z
0000001751501056761969-65535,2017-07-26T10:12:41.9Z
000000541526299609231-65535,2018-05-14T17:52:23.7Z
000000541602140609520-65535,2020-10-08T07:07:25.0Z
000000541669081776937-65535,2022-11-22T01:49:36.9Z
1 1 2022-11-22T01:49:36.9Z
2 smd-id insertion-datetime
3 0000001761385117375880-65535 2013-07-15T00:00:00.0Z
4 0000001751501056761969-65535 2017-07-26T10:12:41.9Z
5 000000541526299609231-65535 2018-05-14T17:52:23.7Z
6 000000541602140609520-65535 2020-10-08T07:07:25.0Z
7 000000541669081776937-65535 2022-11-22T01:49:36.9Z
@@ -0,0 +1,10 @@
1,2024-09-13T02:21:12.0Z
DNL,lookup-key,insertion-datetime
test---validate,2024091300/6/a/b/arJyPPf2CK7f21bVGne0qMgW0000000001,2024-09-13T02:21:12.0Z
test--validate,2024091300/6/a/b/arJyPPf2CK7f21bVGne0qMgW0000000001,2024-09-13T02:21:12.0Z
test-and-validate,2024091300/6/a/b/arJyPPf2CK7f21bVGne0qMgW0000000001,2024-09-13T02:21:12.0Z
test-andvalidate,2024091300/6/a/b/arJyPPf2CK7f21bVGne0qMgW0000000001,2024-09-13T02:21:12.0Z
test-validate,2024091300/6/a/b/arJyPPf2CK7f21bVGne0qMgW0000000001,2024-09-13T02:21:12.0Z
testand-validate,2024091300/6/a/b/arJyPPf2CK7f21bVGne0qMgW0000000001,2024-09-13T02:21:12.0Z
testandvalidate,2024091300/6/a/b/arJyPPf2CK7f21bVGne0qMgW0000000001,2024-09-13T02:21:12.0Z
testvalidate,2024091300/6/a/b/arJyPPf2CK7f21bVGne0qMgW0000000001,2024-09-13T02:21:12.0Z
1 1,2024-09-13T02:21:12.0Z
2 DNL,lookup-key,insertion-datetime
3 test---validate,2024091300/6/a/b/arJyPPf2CK7f21bVGne0qMgW0000000001,2024-09-13T02:21:12.0Z
4 test--validate,2024091300/6/a/b/arJyPPf2CK7f21bVGne0qMgW0000000001,2024-09-13T02:21:12.0Z
5 test-and-validate,2024091300/6/a/b/arJyPPf2CK7f21bVGne0qMgW0000000001,2024-09-13T02:21:12.0Z
6 test-andvalidate,2024091300/6/a/b/arJyPPf2CK7f21bVGne0qMgW0000000001,2024-09-13T02:21:12.0Z
7 test-validate,2024091300/6/a/b/arJyPPf2CK7f21bVGne0qMgW0000000001,2024-09-13T02:21:12.0Z
8 testand-validate,2024091300/6/a/b/arJyPPf2CK7f21bVGne0qMgW0000000001,2024-09-13T02:21:12.0Z
9 testandvalidate,2024091300/6/a/b/arJyPPf2CK7f21bVGne0qMgW0000000001,2024-09-13T02:21:12.0Z
10 testvalidate,2024091300/6/a/b/arJyPPf2CK7f21bVGne0qMgW0000000001,2024-09-13T02:21:12.0Z
@@ -0,0 +1,7 @@
1,2022-11-22T01:49:36.9Z
smd-id,insertion-datetime
0000001761385117375880-65535,2013-07-15T00:00:00.0Z
0000001751501056761969-65535,2017-07-26T10:12:41.9Z
000000541526299609231-65535,2018-05-14T17:52:23.7Z
000000541602140609520-65535,2020-10-08T07:07:25.0Z
000000541669081776937-65535,2022-11-22T01:49:36.9Z
1 1 2022-11-22T01:49:36.9Z
2 smd-id insertion-datetime
3 0000001761385117375880-65535 2013-07-15T00:00:00.0Z
4 0000001751501056761969-65535 2017-07-26T10:12:41.9Z
5 000000541526299609231-65535 2018-05-14T17:52:23.7Z
6 000000541602140609520-65535 2020-10-08T07:07:25.0Z
7 000000541669081776937-65535 2022-11-22T01:49:36.9Z
@@ -335,7 +335,7 @@ class InvoicingPipelineTest {
.build();
persistResource(registrar);
Tld test =
newTld("test", "_TEST", ImmutableSortedMap.of(START_OF_TIME, GENERAL_AVAILABILITY))
newTld("test", "TEST", ImmutableSortedMap.of(START_OF_TIME, GENERAL_AVAILABILITY))
.asBuilder()
.setInvoicingEnabled(true)
.build();
@@ -391,7 +391,7 @@ class InvoicingPipelineTest {
// Test that comments are removed from the .sql file correctly
assertThat(InvoicingPipeline.makeCloudSqlQuery("2017-10"))
.isEqualTo(
"""
"""
SELECT b, r FROM BillingEvent b
JOIN Registrar r ON b.clientId = r.registrarId
@@ -449,13 +449,13 @@ AND cr.id IS NULL
persistResource(registrar3);
Tld test =
newTld("test", "_TEST", ImmutableSortedMap.of(START_OF_TIME, GENERAL_AVAILABILITY))
newTld("test", "TEST", ImmutableSortedMap.of(START_OF_TIME, GENERAL_AVAILABILITY))
.asBuilder()
.setInvoicingEnabled(true)
.build();
persistResource(test);
Tld hello =
newTld("hello", "_HELLO", ImmutableSortedMap.of(START_OF_TIME, GENERAL_AVAILABILITY))
newTld("hello", "HELLO", ImmutableSortedMap.of(START_OF_TIME, GENERAL_AVAILABILITY))
.asBuilder()
.setInvoicingEnabled(true)
.build();
@@ -163,7 +163,7 @@ public class RegistryJpaReadTest {
}
private void setupForJoinQuery() {
Tld registry = newTld("com", "ABCD_APP");
Tld registry = newTld("com", "ABCDAPP");
Registrar registrar =
makeRegistrar1()
.asBuilder()
@@ -17,6 +17,7 @@ package google.registry.bsa;
import static com.google.common.truth.Truth.assertThat;
import static google.registry.testing.DatabaseHelper.createTld;
import static google.registry.testing.DatabaseHelper.persistResource;
import static google.registry.tldconfig.idn.IdnTableEnum.AUGMENTED_LATIN;
import static google.registry.tldconfig.idn.IdnTableEnum.EXTENDED_LATIN;
import static google.registry.tldconfig.idn.IdnTableEnum.JA;
import static google.registry.tldconfig.idn.IdnTableEnum.UNCONFUSABLE_LATIN;
@@ -43,6 +44,7 @@ public class IdnCheckerTest {
Tld jaonly;
Tld jandelatin;
Tld strictlatin;
Tld auglatin;
IdnChecker idnChecker;
@BeforeEach
@@ -50,6 +52,7 @@ public class IdnCheckerTest {
jaonly = createTld("jaonly");
jandelatin = createTld("jandelatin");
strictlatin = createTld("strictlatin");
auglatin = createTld("auglatin");
jaonly =
persistResource(
@@ -72,6 +75,13 @@ public class IdnCheckerTest {
.setBsaEnrollStartTime(Optional.of(fakeClock.nowUtc()))
.setIdnTables(ImmutableSet.of(UNCONFUSABLE_LATIN))
.build());
auglatin =
persistResource(
auglatin
.asBuilder()
.setBsaEnrollStartTime(Optional.of(fakeClock.nowUtc()))
.setIdnTables(ImmutableSet.of(AUGMENTED_LATIN))
.build());
fakeClock.advanceOneMilli();
idnChecker = new IdnChecker(fakeClock);
}
@@ -79,12 +89,13 @@ public class IdnCheckerTest {
@Test
void getAllValidIdns_allTlds() {
assertThat(idnChecker.getAllValidIdns("all"))
.containsExactly(EXTENDED_LATIN, JA, UNCONFUSABLE_LATIN);
.containsExactly(EXTENDED_LATIN, JA, UNCONFUSABLE_LATIN, AUGMENTED_LATIN);
}
@Test
void getAllValidIdns_notJa() {
assertThat(idnChecker.getAllValidIdns("à")).containsExactly(EXTENDED_LATIN, UNCONFUSABLE_LATIN);
assertThat(idnChecker.getAllValidIdns("à"))
.containsExactly(EXTENDED_LATIN, UNCONFUSABLE_LATIN, AUGMENTED_LATIN);
}
@Test
@@ -116,6 +127,7 @@ public class IdnCheckerTest {
@Test
void getForbiddingTlds_success() {
assertThat(idnChecker.getForbiddingTlds(ImmutableSet.of("JA"))).containsExactly(strictlatin);
assertThat(idnChecker.getForbiddingTlds(ImmutableSet.of("JA")))
.containsExactly(strictlatin, auglatin);
}
}
@@ -1,110 +0,0 @@
// Copyright 2017 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;
import static google.registry.model.eppoutput.Result.Code.SUCCESS;
import static google.registry.model.eppoutput.Result.Code.SUCCESS_WITH_ACK_MESSAGE;
import static google.registry.model.eppoutput.Result.Code.SUCCESS_WITH_NO_MESSAGES;
import static google.registry.testing.EppMetricSubject.assertThat;
import com.google.common.collect.ImmutableMap;
import google.registry.persistence.transaction.JpaTestExtensions;
import google.registry.persistence.transaction.JpaTestExtensions.JpaIntegrationTestExtension;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;
/** Tests for contact lifecycle. */
class EppLifecycleContactTest extends EppTestCase {
@RegisterExtension
final JpaIntegrationTestExtension jpa =
new JpaTestExtensions.Builder().withClock(clock).buildIntegrationTestExtension();
@Test
void testContactLifecycle() throws Exception {
assertThatLoginSucceeds("NewRegistrar", "foo-BAR2");
assertThatCommand("contact_create_sh8013.xml")
.atTime("2000-06-01T00:00:00Z")
.hasResponse(
"contact_create_response_sh8013.xml",
ImmutableMap.of("CRDATE", "2000-06-01T00:00:00Z"));
assertThat(getRecordedEppMetric())
.hasClientId("NewRegistrar")
.and()
.hasNoTld()
.and()
.hasCommandName("ContactCreate")
.and()
.hasStatus(SUCCESS);
assertThatCommand("contact_info.xml")
.atTime("2000-06-01T00:01:00Z")
.hasResponse("contact_info_from_create_response.xml");
assertThat(getRecordedEppMetric())
.hasClientId("NewRegistrar")
.and()
.hasCommandName("ContactInfo")
.and()
.hasStatus(SUCCESS);
assertThatCommand("contact_delete_sh8013.xml")
.hasResponse("contact_delete_response_sh8013.xml");
assertThat(getRecordedEppMetric())
.hasClientId("NewRegistrar")
.and()
.hasCommandName("ContactDelete")
.and()
.hasStatus(SUCCESS);
assertThatLogoutSucceeds();
}
@Test
void testContactTransferPollMessage() throws Exception {
assertThatLoginSucceeds("NewRegistrar", "foo-BAR2");
assertThatCommand("contact_create_sh8013.xml")
.atTime("2000-06-01T00:00:00Z")
.hasResponse(
"contact_create_response_sh8013.xml",
ImmutableMap.of("CRDATE", "2000-06-01T00:00:00Z"));
assertThatLogoutSucceeds();
// Initiate a transfer of the newly created contact.
assertThatLoginSucceeds("TheRegistrar", "password2");
assertThatCommand("contact_transfer_request.xml")
.atTime("2000-06-08T22:00:00Z")
.hasResponse("contact_transfer_request_response_alternate.xml");
assertThatLogoutSucceeds();
// Log back in with the losing registrar, read the poll message, and then ack it.
assertThatLoginSucceeds("NewRegistrar", "foo-BAR2");
assertThatCommand("poll.xml")
.atTime("2000-06-08T22:01:00Z")
.hasResponse("poll_response_contact_transfer.xml");
assertThat(getRecordedEppMetric())
.hasClientId("NewRegistrar")
.and()
.hasCommandName("PollRequest")
.and()
.hasStatus(SUCCESS_WITH_ACK_MESSAGE);
assertThatCommand("poll_ack.xml", ImmutableMap.of("ID", "6-2000"))
.atTime("2000-06-08T22:02:00Z")
.hasResponse("poll_ack_response_empty.xml");
assertThat(getRecordedEppMetric())
.hasClientId("NewRegistrar")
.and()
.hasCommandName("PollAck")
.and()
.hasStatus(SUCCESS_WITH_NO_MESSAGES);
assertThatLogoutSucceeds();
}
}
@@ -91,14 +91,6 @@ class EppLifecycleHostTest extends EppTestCase {
createTld("example");
assertThatLoginSucceeds("NewRegistrar", "foo-BAR2");
// Create the fakesite domain.
assertThatCommand("contact_create_sh8013.xml")
.atTime("2000-06-01T00:00:00Z")
.hasResponse(
"contact_create_response_sh8013.xml",
ImmutableMap.of("CRDATE", "2000-06-01T00:00:00Z"));
assertThatCommand("contact_create_jd1234.xml")
.atTime("2000-06-01T00:01:00Z")
.hasResponse("contact_create_response_jd1234.xml");
assertThatCommand("domain_create_fakesite_no_nameservers.xml")
.atTime("2000-06-01T00:04:00Z")
.hasResponse(
@@ -142,15 +134,6 @@ class EppLifecycleHostTest extends EppTestCase {
assertThatLoginSucceeds("NewRegistrar", "foo-BAR2");
assertThatCommand("contact_create_sh8013.xml")
.atTime("2000-06-01T00:00:00Z")
.hasResponse(
"contact_create_response_sh8013.xml",
ImmutableMap.of("CRDATE", "2000-06-01T00:00:00Z"));
assertThatCommand("contact_create_jd1234.xml")
.atTime("2000-06-01T00:01:00Z")
.hasResponse("contact_create_response_jd1234.xml");
// Create domain example.bar.foo.tld
assertThatCommand(
"domain_create_no_hosts_or_dsdata.xml",
@@ -223,7 +223,7 @@ public class EppTestCase {
return eppMetricBuilder.build();
}
/** Create the two administrative contacts and two hosts. */
/** Create the two hosts. */
void createHosts() throws Exception {
DateTime createTime = DateTime.parse("2000-06-01T00:00:00Z");
assertThatCommand("host_create.xml", ImmutableMap.of("HOSTNAME", "ns1.example.external"))
@@ -14,85 +14,25 @@
package google.registry.flows.contact;
import static google.registry.model.eppoutput.CheckData.ContactCheck.create;
import static google.registry.testing.DatabaseHelper.persistActiveContact;
import static google.registry.testing.DatabaseHelper.persistDeletedContact;
import static google.registry.testing.EppExceptionSubject.assertAboutEppExceptions;
import static org.junit.jupiter.api.Assertions.assertThrows;
import google.registry.flows.EppException;
import google.registry.flows.FlowUtils.NotLoggedInException;
import google.registry.flows.ResourceCheckFlowTestCase;
import google.registry.flows.exceptions.TooManyResourceChecksException;
import google.registry.model.contact.Contact;
import google.registry.flows.FlowTestCase;
import google.registry.flows.exceptions.ContactsProhibitedException;
import org.junit.jupiter.api.Test;
/** Unit tests for {@link ContactCheckFlow}. */
class ContactCheckFlowTest extends ResourceCheckFlowTestCase<ContactCheckFlow, Contact> {
class ContactCheckFlowTest extends FlowTestCase<ContactCheckFlow> {
ContactCheckFlowTest() {
setEppInput("contact_check.xml");
}
@Test
void testNotLoggedIn() {
sessionMetadata.setRegistrarId(null);
EppException thrown = assertThrows(NotLoggedInException.class, this::runFlow);
assertAboutEppExceptions().that(thrown).marshalsToXml();
void testThrowsException() {
assertAboutEppExceptions()
.that(assertThrows(ContactsProhibitedException.class, this::runFlow))
.marshalsToXml();
}
@Test
void testNothingExists() throws Exception {
// These ids come from the check xml.
doCheckTest(
create(true, "sh8013", null),
create(true, "sah8013", null),
create(true, "8013sah", null));
}
@Test
void testOneExists() throws Exception {
persistActiveContact("sh8013");
// These ids come from the check xml.
doCheckTest(
create(false, "sh8013", "In use"),
create(true, "sah8013", null),
create(true, "8013sah", null));
}
@Test
void testOneExistsButWasDeleted() throws Exception {
persistDeletedContact("sh8013", clock.nowUtc().minusDays(1));
// These ids come from the check xml.
doCheckTest(
create(true, "sh8013", null),
create(true, "sah8013", null),
create(true, "8013sah", null));
}
@Test
void testXmlMatches() throws Exception {
persistActiveContact("sah8013");
runFlowAssertResponse(loadFile("contact_check_response.xml"));
}
@Test
void test50IdsAllowed() throws Exception {
// Make sure we don't have a regression that reduces the number of allowed checks.
setEppInput("contact_check_50.xml");
runFlow();
}
@Test
void testTooManyIds() {
setEppInput("contact_check_51.xml");
EppException thrown = assertThrows(TooManyResourceChecksException.class, this::runFlow);
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
void testIcannActivityReportField_getsLogged() throws Exception {
runFlow();
assertIcannReportingActivityFieldLogged("srs-cont-check");
}
}
@@ -14,141 +14,24 @@
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;
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.FlowTestCase;
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;
/** Unit tests for {@link ContactCreateFlow}. */
class ContactCreateFlowTest extends ResourceFlowTestCase<ContactCreateFlow, Contact> {
class ContactCreateFlowTest extends FlowTestCase<ContactCreateFlow> {
ContactCreateFlowTest() {
setEppInput("contact_create.xml");
clock.setTo(DateTime.parse("1999-04-03T22:00:00.0Z"));
}
private void doSuccessfulTest() throws Exception {
assertMutatingFlow(true);
runFlowAssertResponse(loadFile("contact_create_response.xml"));
// Check that the contact was created and persisted with a history entry.
Contact contact = reloadResourceByForeignKey();
assertAboutContacts().that(contact).hasOnlyOneHistoryEntryWhich().hasNoXml();
assertNoBillingEvents();
assertLastHistoryContainsResource(contact);
}
@Test
void testNotLoggedIn() {
sessionMetadata.setRegistrarId(null);
EppException thrown = assertThrows(NotLoggedInException.class, this::runFlow);
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
void testDryRun() throws Exception {
dryRunFlowAssertResponse(loadFile("contact_create_response.xml"));
}
@Test
void testSuccess_neverExisted() throws Exception {
doSuccessfulTest();
}
@Test
void testSuccess_existedButWasDeleted() throws Exception {
persistDeletedContact(getUniqueIdFromCommand(), clock.nowUtc().minusDays(1));
clock.advanceOneMilli();
doSuccessfulTest();
}
@Test
void testFailure_alreadyExists() throws Exception {
persistActiveContact(getUniqueIdFromCommand());
ResourceAlreadyExistsForThisClientException thrown =
assertThrows(ResourceAlreadyExistsForThisClientException.class, this::runFlow);
assertThat(thrown)
.hasMessageThat()
.contains(
String.format("Object with given ID (%s) already exists", getUniqueIdFromCommand()));
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
void testFailure_minimumDatasetPhase2_cannotCreateContacts() throws Exception {
persistResource(
new FeatureFlag.Builder()
.setFeatureName(MINIMUM_DATASET_CONTACTS_PROHIBITED)
.setStatusMap(
ImmutableSortedMap.of(START_OF_TIME, INACTIVE, clock.nowUtc().minusDays(5), ACTIVE))
.build());
EppException thrown = assertThrows(ContactsProhibitedException.class, this::runFlow);
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
void testFailure_resourceContention() throws Exception {
String targetId = getUniqueIdFromCommand();
persistResource(
newContact(targetId)
.asBuilder()
.setPersistedCurrentSponsorRegistrarId("NewRegistrar")
.build());
ResourceCreateContentionException thrown =
assertThrows(ResourceCreateContentionException.class, this::runFlow);
assertThat(thrown)
.hasMessageThat()
.contains(String.format("Object with given ID (%s) already exists", targetId));
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
void testSuccess_nonAsciiInLocAddress() throws Exception {
setEppInput("contact_create_hebrew_loc.xml");
doSuccessfulTest();
}
@Test
void testFailure_nonAsciiInIntAddress() {
setEppInput("contact_create_hebrew_int.xml");
EppException thrown =
assertThrows(BadInternationalizedPostalInfoException.class, this::runFlow);
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
void testFailure_declineDisclosure() {
setEppInput("contact_create_decline_disclosure.xml");
EppException thrown =
assertThrows(DeclineContactDisclosureFieldDisallowedPolicyException.class, this::runFlow);
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
void testIcannActivityReportField_getsLogged() throws Exception {
runFlow();
assertIcannReportingActivityFieldLogged("srs-cont-create");
void testThrowsException() {
assertAboutEppExceptions()
.that(assertThrows(ContactsProhibitedException.class, this::runFlow))
.marshalsToXml();
}
}
@@ -14,269 +14,24 @@
package google.registry.flows.contact;
import static com.google.common.collect.MoreCollectors.onlyElement;
import static com.google.common.truth.Truth.assertThat;
import static google.registry.testing.ContactSubject.assertAboutContacts;
import static google.registry.testing.DatabaseHelper.assertNoBillingEvents;
import static google.registry.testing.DatabaseHelper.createTld;
import static google.registry.testing.DatabaseHelper.getPollMessages;
import static google.registry.testing.DatabaseHelper.newContact;
import static google.registry.testing.DatabaseHelper.persistActiveContact;
import static google.registry.testing.DatabaseHelper.persistContactWithPendingTransfer;
import static google.registry.testing.DatabaseHelper.persistDeletedContact;
import static google.registry.testing.DatabaseHelper.persistResource;
import static google.registry.testing.EppExceptionSubject.assertAboutEppExceptions;
import static org.junit.jupiter.api.Assertions.assertThrows;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
import google.registry.flows.EppException;
import google.registry.flows.FlowUtils.NotLoggedInException;
import google.registry.flows.ResourceFlowTestCase;
import google.registry.flows.ResourceFlowUtils.ResourceDoesNotExistException;
import google.registry.flows.ResourceFlowUtils.ResourceNotOwnedException;
import google.registry.flows.exceptions.ResourceStatusProhibitsOperationException;
import google.registry.flows.exceptions.ResourceToDeleteIsReferencedException;
import google.registry.model.contact.Contact;
import google.registry.model.eppcommon.StatusValue;
import google.registry.model.eppcommon.Trid;
import google.registry.model.poll.PendingActionNotificationResponse;
import google.registry.model.poll.PollMessage;
import google.registry.model.reporting.HistoryEntry;
import google.registry.model.reporting.HistoryEntry.Type;
import google.registry.model.tld.Tld;
import google.registry.model.transfer.TransferData;
import google.registry.model.transfer.TransferResponse;
import google.registry.model.transfer.TransferStatus;
import google.registry.testing.DatabaseHelper;
import org.joda.time.DateTime;
import org.junit.jupiter.api.BeforeEach;
import google.registry.flows.FlowTestCase;
import google.registry.flows.exceptions.ContactsProhibitedException;
import org.junit.jupiter.api.Test;
/** Unit tests for {@link ContactDeleteFlow}. */
class ContactDeleteFlowTest extends ResourceFlowTestCase<ContactDeleteFlow, Contact> {
class ContactDeleteFlowTest extends FlowTestCase<ContactDeleteFlow> {
@BeforeEach
void initFlowTest() {
ContactDeleteFlowTest() {
setEppInput("contact_delete.xml");
}
@Test
void testNotLoggedIn() {
sessionMetadata.setRegistrarId(null);
EppException thrown = assertThrows(NotLoggedInException.class, this::runFlow);
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
void testDryRun() throws Exception {
persistActiveContact(getUniqueIdFromCommand());
dryRunFlowAssertResponse(loadFile("contact_delete_response.xml"));
}
@Test
void testSuccess() throws Exception {
persistActiveContact(getUniqueIdFromCommand());
clock.advanceOneMilli();
assertMutatingFlow(true);
runFlowAssertResponse(loadFile("contact_delete_response.xml"));
assertSqlDeleteSuccess();
}
@Test
void testSuccess_pendingTransfer_sql() throws Exception {
DateTime transferRequestTime = clock.nowUtc().minusDays(3);
TransferData oldTransferData =
persistContactWithPendingTransfer(
persistActiveContact(getUniqueIdFromCommand()),
transferRequestTime,
transferRequestTime.plus(Tld.DEFAULT_TRANSFER_GRACE_PERIOD),
clock.nowUtc())
.getTransferData();
clock.advanceOneMilli();
assertMutatingFlow(true);
runFlowAssertResponse(loadFile("contact_delete_response.xml"));
assertSqlDeleteSuccess(Type.CONTACT_DELETE, Type.CONTACT_TRANSFER_REQUEST);
Contact softDeletedContact = reloadResourceByForeignKey(clock.nowUtc().minusMillis(1));
assertThat(softDeletedContact.getTransferData())
.isEqualTo(
oldTransferData
.copyConstantFieldsToBuilder()
.setTransferStatus(TransferStatus.SERVER_CANCELLED)
.setPendingTransferExpirationTime(softDeletedContact.getDeletionTime())
.build());
PollMessage gainingPollMessage =
Iterables.getOnlyElement(getPollMessages("NewRegistrar", clock.nowUtc()));
assertThat(gainingPollMessage.getEventTime()).isEqualTo(clock.nowUtc());
assertThat(
gainingPollMessage.getResponseData().stream()
.filter(TransferResponse.class::isInstance)
.map(TransferResponse.class::cast)
.collect(onlyElement())
.getTransferStatus())
.isEqualTo(TransferStatus.SERVER_CANCELLED);
PendingActionNotificationResponse panData =
gainingPollMessage.getResponseData().stream()
.filter(PendingActionNotificationResponse.class::isInstance)
.map(PendingActionNotificationResponse.class::cast)
.collect(onlyElement());
assertThat(panData.getTrid())
.isEqualTo(Trid.create("transferClient-trid", "transferServer-trid"));
assertThat(panData.getActionResult()).isFalse();
}
@Test
void testSuccess_clTridNotSpecified() throws Exception {
setEppInput("contact_delete_no_cltrid.xml");
persistActiveContact(getUniqueIdFromCommand());
clock.advanceOneMilli();
assertMutatingFlow(true);
runFlowAssertResponse(loadFile("contact_delete_response_no_cltrid.xml"));
assertSqlDeleteSuccess();
}
@Test
void testFailure_neverExisted() throws Exception {
ResourceDoesNotExistException thrown =
assertThrows(ResourceDoesNotExistException.class, this::runFlow);
assertThat(thrown).hasMessageThat().contains(String.format("(%s)", getUniqueIdFromCommand()));
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
void testFailure_existedButWasDeleted() throws Exception {
persistDeletedContact(getUniqueIdFromCommand(), clock.nowUtc().minusDays(1));
ResourceDoesNotExistException thrown =
assertThrows(ResourceDoesNotExistException.class, this::runFlow);
assertThat(thrown).hasMessageThat().contains(String.format("(%s)", getUniqueIdFromCommand()));
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
void testFailure_existedButWasClientDeleteProhibited() throws Exception {
doFailingStatusTest(
StatusValue.CLIENT_DELETE_PROHIBITED, ResourceStatusProhibitsOperationException.class);
}
@Test
void testFailure_existedButWasServerDeleteProhibited() throws Exception {
doFailingStatusTest(
StatusValue.SERVER_DELETE_PROHIBITED, ResourceStatusProhibitsOperationException.class);
}
@Test
void testFailure_existedButWasPendingDelete() throws Exception {
doFailingStatusTest(
StatusValue.PENDING_DELETE, ResourceStatusProhibitsOperationException.class);
}
private void doFailingStatusTest(StatusValue statusValue, Class<? extends EppException> exception)
throws Exception {
persistResource(
newContact(getUniqueIdFromCommand())
.asBuilder()
.setStatusValues(ImmutableSet.of(statusValue))
.build());
EppException thrown = assertThrows(exception, this::runFlow);
assertThat(thrown).hasMessageThat().contains(statusValue.getXmlName());
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
void testSuccess_clientDeleteProhibited_superuser() throws Exception {
persistResource(
persistActiveContact(getUniqueIdFromCommand())
.asBuilder()
.addStatusValue(StatusValue.CLIENT_DELETE_PROHIBITED)
.build());
runFlowAssertResponse(
CommitMode.LIVE, UserPrivileges.SUPERUSER, loadFile("contact_delete_response.xml"));
}
@Test
void testSuccess_serverDeleteProhibited_superuser() throws Exception {
persistResource(
persistActiveContact(getUniqueIdFromCommand())
.asBuilder()
.addStatusValue(StatusValue.SERVER_DELETE_PROHIBITED)
.build());
runFlowAssertResponse(
CommitMode.LIVE, UserPrivileges.SUPERUSER, loadFile("contact_delete_response.xml"));
}
@Test
void testFailure_pendingDelete_superuser() throws Exception {
persistResource(
persistActiveContact(getUniqueIdFromCommand())
.asBuilder()
.addStatusValue(StatusValue.PENDING_DELETE)
.build());
void testThrowsException() {
assertAboutEppExceptions()
.that(
assertThrows(
ResourceStatusProhibitsOperationException.class,
() -> runFlow(CommitMode.LIVE, UserPrivileges.SUPERUSER)))
.that(assertThrows(ContactsProhibitedException.class, this::runFlow))
.marshalsToXml();
}
@Test
void testFailure_unauthorizedClient() throws Exception {
sessionMetadata.setRegistrarId("NewRegistrar");
persistActiveContact(getUniqueIdFromCommand());
EppException thrown = assertThrows(ResourceNotOwnedException.class, this::runFlow);
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
void testSuccess_superuserUnauthorizedClient() throws Exception {
sessionMetadata.setRegistrarId("NewRegistrar");
persistActiveContact(getUniqueIdFromCommand());
clock.advanceOneMilli();
runFlowAssertResponse(
CommitMode.LIVE, UserPrivileges.SUPERUSER, loadFile("contact_delete_response.xml"));
assertSqlDeleteSuccess();
}
@Test
void testFailure_failfastWhenLinkedToDomain() throws Exception {
createTld("tld");
persistResource(
DatabaseHelper.newDomain("example.tld", persistActiveContact(getUniqueIdFromCommand())));
EppException thrown = assertThrows(ResourceToDeleteIsReferencedException.class, this::runFlow);
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
void testIcannActivityReportField_getsLogged() throws Exception {
persistActiveContact(getUniqueIdFromCommand());
clock.advanceOneMilli();
runFlow();
assertIcannReportingActivityFieldLogged("srs-cont-delete");
}
private void assertSqlDeleteSuccess(HistoryEntry.Type... historyEntryTypes) throws Exception {
assertThat(reloadResourceByForeignKey()).isNull();
assertAboutContacts()
.that(reloadResourceByForeignKey(clock.nowUtc().minusMillis(1)))
.isNotActiveAt(clock.nowUtc())
.and()
.hasNullLocalizedPostalInfo()
.and()
.hasNullInternationalizedPostalInfo()
.and()
.hasNullEmailAddress()
.and()
.hasNullVoiceNumber()
.and()
.hasNullFaxNumber()
.and()
.hasExactlyStatusValues(StatusValue.OK)
.and()
.hasOneHistoryEntryEachOfTypes(historyEntryTypes);
assertNoBillingEvents();
}
private void assertSqlDeleteSuccess() throws Exception {
assertSqlDeleteSuccess(Type.CONTACT_DELETE);
}
}
@@ -14,202 +14,24 @@
package google.registry.flows.contact;
import static com.google.common.truth.Truth.assertThat;
import static google.registry.model.EppResourceUtils.isDeleted;
import static google.registry.testing.DatabaseHelper.assertNoBillingEvents;
import static google.registry.testing.DatabaseHelper.createTld;
import static google.registry.testing.DatabaseHelper.persistResource;
import static google.registry.testing.EppExceptionSubject.assertAboutEppExceptions;
import static org.junit.jupiter.api.Assertions.assertThrows;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import google.registry.flows.EppException;
import google.registry.flows.FlowUtils.NotLoggedInException;
import google.registry.flows.ResourceFlowTestCase;
import google.registry.flows.ResourceFlowUtils.ResourceDoesNotExistException;
import google.registry.flows.ResourceFlowUtils.ResourceNotOwnedException;
import google.registry.model.contact.Contact;
import google.registry.model.contact.ContactAddress;
import google.registry.model.contact.ContactAuthInfo;
import google.registry.model.contact.ContactPhoneNumber;
import google.registry.model.contact.Disclose;
import google.registry.model.contact.PostalInfo;
import google.registry.model.contact.PostalInfo.Type;
import google.registry.model.eppcommon.AuthInfo.PasswordAuth;
import google.registry.model.eppcommon.PresenceMarker;
import google.registry.model.eppcommon.StatusValue;
import google.registry.testing.DatabaseHelper;
import org.joda.time.DateTime;
import google.registry.flows.FlowTestCase;
import google.registry.flows.exceptions.ContactsProhibitedException;
import org.junit.jupiter.api.Test;
/** Unit tests for {@link ContactInfoFlow}. */
class ContactInfoFlowTest extends ResourceFlowTestCase<ContactInfoFlow, Contact> {
class ContactInfoFlowTest extends FlowTestCase<ContactInfoFlow> {
ContactInfoFlowTest() {
setEppInput("contact_info.xml");
}
private Contact persistContact(boolean active) {
Contact contact =
persistResource(
new Contact.Builder()
.setContactId("sh8013")
.setRepoId("2FF-ROID")
.setDeletionTime(active ? null : clock.nowUtc().minusDays(1))
.setStatusValues(ImmutableSet.of(StatusValue.CLIENT_DELETE_PROHIBITED))
.setInternationalizedPostalInfo(
new PostalInfo.Builder()
.setType(Type.INTERNATIONALIZED)
.setName("John Doe")
.setOrg("Example Inc.")
.setAddress(
new ContactAddress.Builder()
.setStreet(ImmutableList.of("123 Example Dr.", "Suite 100"))
.setCity("Dulles")
.setState("VA")
.setZip("20166-6503")
.setCountryCode("US")
.build())
.build())
.setVoiceNumber(
new ContactPhoneNumber.Builder()
.setPhoneNumber("+1.7035555555")
.setExtension("1234")
.build())
.setFaxNumber(
new ContactPhoneNumber.Builder().setPhoneNumber("+1.7035555556").build())
.setEmailAddress("jdoe@example.com")
.setPersistedCurrentSponsorRegistrarId("TheRegistrar")
.setCreationRegistrarId("NewRegistrar")
.setLastEppUpdateRegistrarId("NewRegistrar")
.setCreationTimeForTest(DateTime.parse("1999-04-03T22:00:00.0Z"))
.setLastEppUpdateTime(DateTime.parse("1999-12-03T09:00:00.0Z"))
.setLastTransferTime(DateTime.parse("2000-04-08T09:00:00.0Z"))
.setAuthInfo(ContactAuthInfo.create(PasswordAuth.create("2fooBAR")))
.setDisclose(
new Disclose.Builder()
.setFlag(true)
.setVoice(new PresenceMarker())
.setEmail(new PresenceMarker())
.build())
.build());
assertThat(isDeleted(contact, clock.nowUtc())).isNotEqualTo(active);
return contact;
}
@Test
void testNotLoggedIn() {
sessionMetadata.setRegistrarId(null);
EppException thrown = assertThrows(NotLoggedInException.class, this::runFlow);
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
void testSuccess() throws Exception {
persistContact(true);
// Check that the persisted contact info was returned.
assertMutatingFlow(false);
runFlowAssertResponse(
loadFile("contact_info_response.xml"),
// We use a different roid scheme than the samples so ignore it.
"epp.response.resData.infData.roid");
assertNoHistory();
assertNoBillingEvents();
}
@Test
void testSuccess_linked() throws Exception {
createTld("foobar");
persistResource(DatabaseHelper.newDomain("example.foobar", persistContact(true)));
// Check that the persisted contact info was returned.
assertMutatingFlow(false);
runFlowAssertResponse(
loadFile("contact_info_response_linked.xml"),
// We use a different roid scheme than the samples so ignore it.
"epp.response.resData.infData.roid");
assertNoHistory();
assertNoBillingEvents();
}
@Test
void testSuccess_owningRegistrarWithoutAuthInfo_seesAuthInfo() throws Exception {
setEppInput("contact_info_no_authinfo.xml");
persistContact(true);
// Check that the persisted contact info was returned.
assertMutatingFlow(false);
runFlowAssertResponse(
loadFile("contact_info_response.xml"),
// We use a different roid scheme than the samples so ignore it.
"epp.response.resData.infData.roid");
assertNoHistory();
assertNoBillingEvents();
}
@Test
void testFailure_otherRegistrar_notAuthorized() throws Exception {
setRegistrarIdForFlow("NewRegistrar");
persistContact(true);
// Check that the persisted contact info was returned.
assertMutatingFlow(false);
ResourceNotOwnedException thrown = assertThrows(ResourceNotOwnedException.class, this::runFlow);
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
void testSuccess_otherRegistrarWithoutAuthInfoAsSuperuser_doesNotSeeAuthInfo() throws Exception {
setRegistrarIdForFlow("NewRegistrar");
setEppInput("contact_info_no_authinfo.xml");
persistContact(true);
// Check that the persisted contact info was returned.
assertMutatingFlow(false);
runFlowAssertResponse(
CommitMode.LIVE,
UserPrivileges.SUPERUSER,
loadFile("contact_info_response_no_authinfo.xml"),
// We use a different roid scheme than the samples so ignore it.
"epp.response.resData.infData.roid");
assertNoHistory();
assertNoBillingEvents();
}
@Test
void testSuccess_otherRegistrarWithAuthInfoAsSuperuser_seesAuthInfo() throws Exception {
setRegistrarIdForFlow("NewRegistrar");
persistContact(true);
// Check that the persisted contact info was returned.
assertMutatingFlow(false);
runFlowAssertResponse(
CommitMode.LIVE,
UserPrivileges.SUPERUSER,
loadFile("contact_info_response.xml"),
// We use a different roid scheme than the samples so ignore it.
"epp.response.resData.infData.roid");
assertNoHistory();
assertNoBillingEvents();
}
@Test
void testFailure_neverExisted() throws Exception {
ResourceDoesNotExistException thrown =
assertThrows(ResourceDoesNotExistException.class, this::runFlow);
assertThat(thrown).hasMessageThat().contains(String.format("(%s)", getUniqueIdFromCommand()));
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
void testFailure_existedButWasDeleted() throws Exception {
persistContact(false);
ResourceDoesNotExistException thrown =
assertThrows(ResourceDoesNotExistException.class, this::runFlow);
assertThat(thrown).hasMessageThat().contains(String.format("(%s)", getUniqueIdFromCommand()));
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
void testIcannActivityReportField_getsLogged() throws Exception {
persistContact(true);
runFlow();
assertIcannReportingActivityFieldLogged("srs-cont-info");
void testThrowsException() {
assertAboutEppExceptions()
.that(assertThrows(ContactsProhibitedException.class, this::runFlow))
.marshalsToXml();
}
}
@@ -14,254 +14,24 @@
package google.registry.flows.contact;
import static com.google.common.collect.MoreCollectors.onlyElement;
import static com.google.common.truth.Truth.assertThat;
import static google.registry.testing.ContactSubject.assertAboutContacts;
import static google.registry.testing.DatabaseHelper.assertNoBillingEvents;
import static google.registry.testing.DatabaseHelper.createTld;
import static google.registry.testing.DatabaseHelper.getOnlyPollMessage;
import static google.registry.testing.DatabaseHelper.getPollMessages;
import static google.registry.testing.DatabaseHelper.persistResource;
import static google.registry.testing.EppExceptionSubject.assertAboutEppExceptions;
import static org.junit.jupiter.api.Assertions.assertThrows;
import google.registry.flows.EppException;
import google.registry.flows.FlowUtils.NotLoggedInException;
import google.registry.flows.ResourceFlowUtils.BadAuthInfoForResourceException;
import google.registry.flows.ResourceFlowUtils.ResourceDoesNotExistException;
import google.registry.flows.ResourceFlowUtils.ResourceNotOwnedException;
import google.registry.flows.exceptions.NotPendingTransferException;
import google.registry.model.contact.Contact;
import google.registry.model.contact.ContactAuthInfo;
import google.registry.model.eppcommon.AuthInfo.PasswordAuth;
import google.registry.model.eppcommon.Trid;
import google.registry.model.poll.PendingActionNotificationResponse;
import google.registry.model.poll.PollMessage;
import google.registry.model.reporting.HistoryEntry;
import google.registry.model.transfer.TransferData;
import google.registry.model.transfer.TransferResponse;
import google.registry.model.transfer.TransferStatus;
import org.junit.jupiter.api.BeforeEach;
import google.registry.flows.FlowTestCase;
import google.registry.flows.exceptions.ContactsProhibitedException;
import org.junit.jupiter.api.Test;
/** Unit tests for {@link ContactTransferApproveFlow}. */
class ContactTransferApproveFlowTest
extends ContactTransferFlowTestCase<ContactTransferApproveFlow, Contact> {
class ContactTransferApproveFlowTest extends FlowTestCase<ContactTransferApproveFlow> {
@BeforeEach
void setUp() {
ContactTransferApproveFlowTest() {
setEppInput("contact_transfer_approve.xml");
setRegistrarIdForFlow("TheRegistrar");
setupContactWithPendingTransfer();
clock.advanceOneMilli();
createTld("foobar");
}
private void doSuccessfulTest(String commandFilename, String expectedXmlFilename)
throws Exception {
setEppInput(commandFilename);
// Look in the future and make sure the poll messages for implicit ack are there.
assertThat(getPollMessages("NewRegistrar", clock.nowUtc().plusMonths(1)))
.hasSize(1);
assertThat(getPollMessages("TheRegistrar", clock.nowUtc().plusMonths(1)))
.hasSize(1);
// Setup done; run the test.
contact = reloadResourceByForeignKey();
TransferData originalTransferData = contact.getTransferData();
assertMutatingFlow(true);
runFlowAssertResponse(loadFile(expectedXmlFilename));
// Transfer should have succeeded. Verify correct fields were set.
contact = reloadResourceByForeignKey();
assertAboutContacts()
.that(contact)
.hasCurrentSponsorRegistrarId("NewRegistrar")
.and()
.hasLastTransferTime(clock.nowUtc())
.and()
.hasOneHistoryEntryEachOfTypes(
HistoryEntry.Type.CONTACT_TRANSFER_REQUEST, HistoryEntry.Type.CONTACT_TRANSFER_APPROVE);
assertThat(contact.getTransferData())
.isEqualTo(
originalTransferData.copyConstantFieldsToBuilder()
.setTransferStatus(TransferStatus.CLIENT_APPROVED)
.setPendingTransferExpirationTime(clock.nowUtc())
.build());
assertNoBillingEvents();
// The poll message (in the future) to the losing registrar for implicit ack should be gone.
assertThat(getPollMessages("TheRegistrar", clock.nowUtc().plusMonths(1))).isEmpty();
// The poll message in the future to the gaining registrar should be gone too, but there
// should be one at the current time to the gaining registrar.
PollMessage gainingPollMessage = getOnlyPollMessage("NewRegistrar");
assertThat(gainingPollMessage.getEventTime()).isEqualTo(clock.nowUtc());
assertThat(
gainingPollMessage
.getResponseData()
.stream()
.filter(TransferResponse.class::isInstance)
.map(TransferResponse.class::cast)
.collect(onlyElement())
.getTransferStatus())
.isEqualTo(TransferStatus.CLIENT_APPROVED);
PendingActionNotificationResponse panData =
gainingPollMessage
.getResponseData()
.stream()
.filter(PendingActionNotificationResponse.class::isInstance)
.map(PendingActionNotificationResponse.class::cast)
.collect(onlyElement());
assertThat(panData.getTrid())
.isEqualTo(Trid.create("transferClient-trid", "transferServer-trid"));
assertThat(panData.getActionResult()).isTrue();
assertLastHistoryContainsResource(contact);
}
private void doFailingTest(String commandFilename) throws Exception {
setEppInput(commandFilename);
// Setup done; run the test.
assertMutatingFlow(true);
runFlow();
}
@Test
void testNotLoggedIn() {
sessionMetadata.setRegistrarId(null);
EppException thrown = assertThrows(NotLoggedInException.class, this::runFlow);
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
void testDryRun() throws Exception {
setEppInput("contact_transfer_approve.xml");
dryRunFlowAssertResponse(loadFile("contact_transfer_approve_response.xml"));
}
@Test
void testSuccess() throws Exception {
doSuccessfulTest("contact_transfer_approve.xml", "contact_transfer_approve_response.xml");
}
@Test
void testSuccess_withAuthinfo() throws Exception {
doSuccessfulTest("contact_transfer_approve_with_authinfo.xml",
"contact_transfer_approve_response.xml");
}
@Test
void testFailure_badContactPassword() {
// Change the contact's password so it does not match the password in the file.
contact = persistResource(
contact.asBuilder()
.setAuthInfo(ContactAuthInfo.create(PasswordAuth.create("badpassword")))
.build());
EppException thrown =
assertThrows(
BadAuthInfoForResourceException.class,
() -> doFailingTest("contact_transfer_approve_with_authinfo.xml"));
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
void testFailure_neverBeenTransferred() {
changeTransferStatus(null);
EppException thrown =
assertThrows(
NotPendingTransferException.class, () -> doFailingTest("contact_transfer_approve.xml"));
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
void testFailure_clientApproved() {
changeTransferStatus(TransferStatus.CLIENT_APPROVED);
EppException thrown =
assertThrows(
NotPendingTransferException.class, () -> doFailingTest("contact_transfer_approve.xml"));
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
void testFailure_clientRejected() {
changeTransferStatus(TransferStatus.CLIENT_REJECTED);
EppException thrown =
assertThrows(
NotPendingTransferException.class, () -> doFailingTest("contact_transfer_approve.xml"));
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
void testFailure_clientCancelled() {
changeTransferStatus(TransferStatus.CLIENT_CANCELLED);
EppException thrown =
assertThrows(
NotPendingTransferException.class, () -> doFailingTest("contact_transfer_approve.xml"));
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
void testFailure_serverApproved() {
changeTransferStatus(TransferStatus.SERVER_APPROVED);
EppException thrown =
assertThrows(
NotPendingTransferException.class, () -> doFailingTest("contact_transfer_approve.xml"));
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
void testFailure_serverCancelled() {
changeTransferStatus(TransferStatus.SERVER_CANCELLED);
EppException thrown =
assertThrows(
NotPendingTransferException.class, () -> doFailingTest("contact_transfer_approve.xml"));
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
void testFailure_gainingClient() {
setRegistrarIdForFlow("NewRegistrar");
EppException thrown =
assertThrows(
ResourceNotOwnedException.class, () -> doFailingTest("contact_transfer_approve.xml"));
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
void testFailure_unrelatedClient() {
setRegistrarIdForFlow("ClientZ");
EppException thrown =
assertThrows(
ResourceNotOwnedException.class, () -> doFailingTest("contact_transfer_approve.xml"));
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
void testFailure_deletedContact() throws Exception {
contact = persistResource(
contact.asBuilder().setDeletionTime(clock.nowUtc().minusDays(1)).build());
ResourceDoesNotExistException thrown =
assertThrows(
ResourceDoesNotExistException.class,
() -> doFailingTest("contact_transfer_approve.xml"));
assertThat(thrown).hasMessageThat().contains(String.format("(%s)", getUniqueIdFromCommand()));
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
void testFailure_nonexistentContact() throws Exception {
persistResource(contact.asBuilder().setDeletionTime(clock.nowUtc().minusDays(1)).build());
contact = persistResource(
contact.asBuilder().setDeletionTime(clock.nowUtc().minusDays(1)).build());
ResourceDoesNotExistException thrown =
assertThrows(
ResourceDoesNotExistException.class,
() -> doFailingTest("contact_transfer_approve.xml"));
assertThat(thrown).hasMessageThat().contains(String.format("(%s)", getUniqueIdFromCommand()));
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
void testIcannActivityReportField_getsLogged() throws Exception {
runFlow();
assertIcannReportingActivityFieldLogged("srs-cont-transfer-approve");
void testThrowsException() {
assertAboutEppExceptions()
.that(assertThrows(ContactsProhibitedException.class, this::runFlow))
.marshalsToXml();
}
}
@@ -14,240 +14,24 @@
package google.registry.flows.contact;
import static com.google.common.collect.MoreCollectors.onlyElement;
import static com.google.common.truth.Truth.assertThat;
import static google.registry.testing.ContactSubject.assertAboutContacts;
import static google.registry.testing.DatabaseHelper.assertNoBillingEvents;
import static google.registry.testing.DatabaseHelper.getOnlyPollMessage;
import static google.registry.testing.DatabaseHelper.getPollMessages;
import static google.registry.testing.DatabaseHelper.persistResource;
import static google.registry.testing.EppExceptionSubject.assertAboutEppExceptions;
import static org.junit.jupiter.api.Assertions.assertThrows;
import google.registry.flows.EppException;
import google.registry.flows.FlowUtils.NotLoggedInException;
import google.registry.flows.ResourceFlowUtils.BadAuthInfoForResourceException;
import google.registry.flows.ResourceFlowUtils.ResourceDoesNotExistException;
import google.registry.flows.exceptions.NotPendingTransferException;
import google.registry.flows.exceptions.NotTransferInitiatorException;
import google.registry.model.contact.Contact;
import google.registry.model.contact.ContactAuthInfo;
import google.registry.model.eppcommon.AuthInfo.PasswordAuth;
import google.registry.model.poll.PollMessage;
import google.registry.model.reporting.HistoryEntry;
import google.registry.model.transfer.TransferData;
import google.registry.model.transfer.TransferResponse;
import google.registry.model.transfer.TransferStatus;
import org.junit.jupiter.api.BeforeEach;
import google.registry.flows.FlowTestCase;
import google.registry.flows.exceptions.ContactsProhibitedException;
import org.junit.jupiter.api.Test;
/** Unit tests for {@link ContactTransferCancelFlow}. */
class ContactTransferCancelFlowTest
extends ContactTransferFlowTestCase<ContactTransferCancelFlow, Contact> {
class ContactTransferCancelFlowTest extends FlowTestCase<ContactTransferCancelFlow> {
@BeforeEach
void setUp() {
this.setEppInput("contact_transfer_cancel.xml");
setRegistrarIdForFlow("NewRegistrar");
setupContactWithPendingTransfer();
clock.advanceOneMilli();
}
private void doSuccessfulTest(String commandFilename, String expectedXmlFilename)
throws Exception {
this.setEppInput(commandFilename);
// Look in the future and make sure the poll messages for implicit ack are there.
assertThat(getPollMessages("NewRegistrar", clock.nowUtc().plusMonths(1))).hasSize(1);
assertThat(getPollMessages("TheRegistrar", clock.nowUtc().plusMonths(1))).hasSize(1);
// Setup done; run the test.
contact = reloadResourceByForeignKey();
TransferData originalTransferData = contact.getTransferData();
assertMutatingFlow(true);
runFlowAssertResponse(loadFile(expectedXmlFilename));
// Transfer should have been cancelled. Verify correct fields were set.
contact = reloadResourceByForeignKey();
assertAboutContacts()
.that(contact)
.hasCurrentSponsorRegistrarId("TheRegistrar")
.and()
.hasLastTransferTimeNotEqualTo(clock.nowUtc())
.and()
.hasOneHistoryEntryEachOfTypes(
HistoryEntry.Type.CONTACT_TRANSFER_REQUEST, HistoryEntry.Type.CONTACT_TRANSFER_CANCEL);
assertThat(contact.getTransferData())
.isEqualTo(
originalTransferData.copyConstantFieldsToBuilder()
.setTransferStatus(TransferStatus.CLIENT_CANCELLED)
.setPendingTransferExpirationTime(clock.nowUtc())
.build());
assertNoBillingEvents();
// The poll message (in the future) to the gaining registrar for implicit ack should be gone.
assertThat(getPollMessages("NewRegistrar", clock.nowUtc().plusMonths(1))).isEmpty();
// The poll message in the future to the losing registrar should be gone too, but there
// should be one at the current time to the losing registrar.
PollMessage losingPollMessage = getOnlyPollMessage("TheRegistrar");
assertThat(losingPollMessage.getEventTime()).isEqualTo(clock.nowUtc());
assertThat(
losingPollMessage
.getResponseData()
.stream()
.filter(TransferResponse.class::isInstance)
.map(TransferResponse.class::cast)
.collect(onlyElement())
.getTransferStatus())
.isEqualTo(TransferStatus.CLIENT_CANCELLED);
assertLastHistoryContainsResource(contact);
}
private void doFailingTest(String commandFilename) throws Exception {
this.setEppInput(commandFilename);
// Setup done; run the test.
assertMutatingFlow(true);
runFlow();
}
@Test
void testNotLoggedIn() {
sessionMetadata.setRegistrarId(null);
EppException thrown = assertThrows(NotLoggedInException.class, this::runFlow);
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
void testDryRun() throws Exception {
ContactTransferCancelFlowTest() {
setEppInput("contact_transfer_cancel.xml");
dryRunFlowAssertResponse(loadFile("contact_transfer_cancel_response.xml"));
}
@Test
void testSuccess() throws Exception {
doSuccessfulTest("contact_transfer_cancel.xml", "contact_transfer_cancel_response.xml");
}
@Test
void testSuccess_withAuthinfo() throws Exception {
doSuccessfulTest("contact_transfer_cancel_with_authinfo.xml",
"contact_transfer_cancel_response.xml");
}
@Test
void testFailure_badContactPassword() {
// Change the contact's password so it does not match the password in the file.
contact =
persistResource(
contact
.asBuilder()
.setAuthInfo(ContactAuthInfo.create(PasswordAuth.create("badpassword")))
.build());
EppException thrown =
assertThrows(
BadAuthInfoForResourceException.class,
() -> doFailingTest("contact_transfer_cancel_with_authinfo.xml"));
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
void testFailure_neverBeenTransferred() {
changeTransferStatus(null);
EppException thrown =
assertThrows(
NotPendingTransferException.class, () -> doFailingTest("contact_transfer_cancel.xml"));
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
void testFailure_clientApproved() {
changeTransferStatus(TransferStatus.CLIENT_APPROVED);
EppException thrown =
assertThrows(
NotPendingTransferException.class, () -> doFailingTest("contact_transfer_cancel.xml"));
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
void testFailure_clientRejected() {
changeTransferStatus(TransferStatus.CLIENT_REJECTED);
EppException thrown =
assertThrows(
NotPendingTransferException.class, () -> doFailingTest("contact_transfer_cancel.xml"));
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
void testFailure_clientCancelled() {
changeTransferStatus(TransferStatus.CLIENT_CANCELLED);
EppException thrown =
assertThrows(
NotPendingTransferException.class, () -> doFailingTest("contact_transfer_cancel.xml"));
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
void testFailure_serverApproved() {
changeTransferStatus(TransferStatus.SERVER_APPROVED);
EppException thrown =
assertThrows(
NotPendingTransferException.class, () -> doFailingTest("contact_transfer_cancel.xml"));
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
void testFailure_serverCancelled() {
changeTransferStatus(TransferStatus.SERVER_CANCELLED);
EppException thrown =
assertThrows(
NotPendingTransferException.class, () -> doFailingTest("contact_transfer_cancel.xml"));
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
void testFailure_sponsoringClient() {
setRegistrarIdForFlow("TheRegistrar");
EppException thrown =
assertThrows(
NotTransferInitiatorException.class,
() -> doFailingTest("contact_transfer_cancel.xml"));
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
void testFailure_unrelatedClient() {
setRegistrarIdForFlow("ClientZ");
EppException thrown =
assertThrows(
NotTransferInitiatorException.class,
() -> doFailingTest("contact_transfer_cancel.xml"));
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
void testFailure_deletedContact() throws Exception {
contact =
persistResource(contact.asBuilder().setDeletionTime(clock.nowUtc().minusDays(1)).build());
ResourceDoesNotExistException thrown =
assertThrows(
ResourceDoesNotExistException.class,
() -> doFailingTest("contact_transfer_cancel.xml"));
assertThat(thrown).hasMessageThat().contains(String.format("(%s)", getUniqueIdFromCommand()));
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
void testFailure_nonexistentContact() throws Exception {
persistResource(contact.asBuilder().setDeletionTime(clock.nowUtc().minusDays(1)).build());
ResourceDoesNotExistException thrown =
assertThrows(
ResourceDoesNotExistException.class,
() -> doFailingTest("contact_transfer_cancel.xml"));
assertThat(thrown).hasMessageThat().contains(String.format("(%s)", getUniqueIdFromCommand()));
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
void testIcannActivityReportField_getsLogged() throws Exception {
runFlow();
assertIcannReportingActivityFieldLogged("srs-cont-transfer-cancel");
void testThrowsException() {
assertAboutEppExceptions()
.that(assertThrows(ContactsProhibitedException.class, this::runFlow))
.marshalsToXml();
}
}
@@ -1,93 +0,0 @@
// Copyright 2017 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.contact;
import static com.google.common.base.Preconditions.checkState;
import static google.registry.testing.DatabaseHelper.newContact;
import static google.registry.testing.DatabaseHelper.persistContactWithPendingTransfer;
import static google.registry.testing.DatabaseHelper.persistResource;
import google.registry.flows.Flow;
import google.registry.flows.ResourceFlowTestCase;
import google.registry.model.EppResource;
import google.registry.model.contact.Contact;
import google.registry.model.tld.Tld;
import google.registry.model.transfer.TransferStatus;
import google.registry.persistence.transaction.JpaTransactionManagerExtension;
import org.joda.time.DateTime;
import org.joda.time.Duration;
import org.junit.jupiter.api.BeforeEach;
/**
* Base class for contact transfer flow unit tests.
*
* @param <F> the flow type
* @param <R> the resource type
*/
abstract class ContactTransferFlowTestCase<F extends Flow, R extends EppResource>
extends ResourceFlowTestCase<F, R> {
// Transfer is requested on the 6th and expires on the 11th.
// The "now" of this flow is on the 9th, 3 days in.
private static final DateTime TRANSFER_REQUEST_TIME = DateTime.parse("2000-06-06T22:00:00.0Z");
private static final DateTime TRANSFER_EXPIRATION_TIME =
TRANSFER_REQUEST_TIME.plus(Tld.DEFAULT_TRANSFER_GRACE_PERIOD);
private static final Duration TIME_SINCE_REQUEST = Duration.standardDays(3);
protected Contact contact;
ContactTransferFlowTestCase() {
checkState(!Tld.DEFAULT_TRANSFER_GRACE_PERIOD.isShorterThan(TIME_SINCE_REQUEST));
clock.setTo(TRANSFER_REQUEST_TIME.plus(TIME_SINCE_REQUEST));
}
@BeforeEach
void beforeEachContactTransferFlowTestCase() {
// Registrar ClientZ is used in tests that need another registrar that definitely doesn't own
// the resources in question.
persistResource(
JpaTransactionManagerExtension.makeRegistrar1()
.asBuilder()
.setRegistrarId("ClientZ")
.build());
}
/** Adds a contact that has a pending transfer on it from TheRegistrar to NewRegistrar. */
void setupContactWithPendingTransfer() {
contact =
persistContactWithPendingTransfer(
newContact("sh8013"),
TRANSFER_REQUEST_TIME,
TRANSFER_EXPIRATION_TIME,
TRANSFER_REQUEST_TIME);
}
/** Changes the transfer status on the persisted contact. */
protected void changeTransferStatus(TransferStatus transferStatus) {
contact = persistResource(
contact.asBuilder()
.setTransferData(
contact.getTransferData().asBuilder().setTransferStatus(transferStatus).build())
.build());
clock.advanceOneMilli();
}
/** Changes the client ID that the flow will run as. */
@Override
protected void setRegistrarIdForFlow(String registrarId) {
sessionMetadata.setRegistrarId(registrarId);
}
}
@@ -14,206 +14,24 @@
package google.registry.flows.contact;
import static com.google.common.truth.Truth.assertThat;
import static google.registry.testing.ContactSubject.assertAboutContacts;
import static google.registry.testing.DatabaseHelper.assertNoBillingEvents;
import static google.registry.testing.DatabaseHelper.persistResource;
import static google.registry.testing.EppExceptionSubject.assertAboutEppExceptions;
import static org.junit.jupiter.api.Assertions.assertThrows;
import google.registry.flows.EppException;
import google.registry.flows.FlowUtils.NotLoggedInException;
import google.registry.flows.ResourceFlowUtils.BadAuthInfoForResourceException;
import google.registry.flows.ResourceFlowUtils.ResourceDoesNotExistException;
import google.registry.flows.exceptions.NoTransferHistoryToQueryException;
import google.registry.flows.exceptions.NotAuthorizedToViewTransferException;
import google.registry.model.contact.Contact;
import google.registry.model.contact.ContactAuthInfo;
import google.registry.model.eppcommon.AuthInfo.PasswordAuth;
import google.registry.model.reporting.HistoryEntry;
import google.registry.model.transfer.TransferStatus;
import org.joda.time.DateTime;
import org.junit.jupiter.api.BeforeEach;
import google.registry.flows.FlowTestCase;
import google.registry.flows.exceptions.ContactsProhibitedException;
import org.junit.jupiter.api.Test;
/** Unit tests for {@link ContactTransferQueryFlow}. */
class ContactTransferQueryFlowTest
extends ContactTransferFlowTestCase<ContactTransferQueryFlow, Contact> {
class ContactTransferQueryFlowTest extends FlowTestCase<ContactTransferQueryFlow> {
@BeforeEach
void setUp() {
ContactTransferQueryFlowTest() {
setEppInput("contact_transfer_query.xml");
clock.setTo(DateTime.parse("2000-06-10T22:00:00.0Z"));
setRegistrarIdForFlow("NewRegistrar");
setupContactWithPendingTransfer();
}
private void doSuccessfulTest(String commandFilename, String expectedXmlFilename)
throws Exception {
setEppInput(commandFilename);
eppLoader.replaceAll("JD1234-REP", contact.getRepoId());
// Setup done; run the test.
assertMutatingFlow(false);
runFlowAssertResponse(loadFile(expectedXmlFilename));
assertAboutContacts().that(reloadResourceByForeignKey(clock.nowUtc().minusDays(1)))
.hasOneHistoryEntryEachOfTypes(HistoryEntry.Type.CONTACT_TRANSFER_REQUEST);
assertNoBillingEvents();
}
private void doFailingTest(String commandFilename) throws Exception {
setEppInput(commandFilename);
eppLoader.replaceAll("JD1234-REP", contact.getRepoId());
// Setup done; run the test.
assertMutatingFlow(false);
runFlow();
}
@Test
void testNotLoggedIn() {
sessionMetadata.setRegistrarId(null);
EppException thrown = assertThrows(NotLoggedInException.class, this::runFlow);
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
void testSuccess() throws Exception {
doSuccessfulTest("contact_transfer_query.xml", "contact_transfer_query_response.xml");
}
@Test
void testSuccess_withContactRoid() throws Exception {
doSuccessfulTest("contact_transfer_query_with_roid.xml", "contact_transfer_query_response.xml");
}
@Test
void testSuccess_sponsoringClient() throws Exception {
setRegistrarIdForFlow("TheRegistrar");
doSuccessfulTest("contact_transfer_query.xml", "contact_transfer_query_response.xml");
}
@Test
void testSuccess_withAuthinfo() throws Exception {
setRegistrarIdForFlow("ClientZ");
doSuccessfulTest("contact_transfer_query_with_authinfo.xml",
"contact_transfer_query_response.xml");
}
@Test
void testSuccess_clientApproved() throws Exception {
changeTransferStatus(TransferStatus.CLIENT_APPROVED);
doSuccessfulTest("contact_transfer_query.xml",
"contact_transfer_query_response_client_approved.xml");
}
@Test
void testSuccess_clientRejected() throws Exception {
changeTransferStatus(TransferStatus.CLIENT_REJECTED);
doSuccessfulTest("contact_transfer_query.xml",
"contact_transfer_query_response_client_rejected.xml");
}
@Test
void testSuccess_clientCancelled() throws Exception {
changeTransferStatus(TransferStatus.CLIENT_CANCELLED);
doSuccessfulTest("contact_transfer_query.xml",
"contact_transfer_query_response_client_cancelled.xml");
}
@Test
void testSuccess_serverApproved() throws Exception {
changeTransferStatus(TransferStatus.SERVER_APPROVED);
doSuccessfulTest("contact_transfer_query.xml",
"contact_transfer_query_response_server_approved.xml");
}
@Test
void testSuccess_serverCancelled() throws Exception {
changeTransferStatus(TransferStatus.SERVER_CANCELLED);
doSuccessfulTest("contact_transfer_query.xml",
"contact_transfer_query_response_server_cancelled.xml");
}
@Test
void testFailure_pendingDeleteContact() throws Exception {
changeTransferStatus(TransferStatus.SERVER_CANCELLED);
contact = persistResource(
contact.asBuilder().setDeletionTime(clock.nowUtc().plusDays(1)).build());
doSuccessfulTest("contact_transfer_query.xml",
"contact_transfer_query_response_server_cancelled.xml");
}
@Test
void testFailure_badContactPassword() {
// Change the contact's password so it does not match the password in the file.
contact =
persistResource(
contact
.asBuilder()
.setAuthInfo(ContactAuthInfo.create(PasswordAuth.create("badpassword")))
.build());
EppException thrown =
assertThrows(
BadAuthInfoForResourceException.class,
() -> doFailingTest("contact_transfer_query_with_authinfo.xml"));
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
void testFailure_badContactRoid() {
// Set the contact to a different ROID, but don't persist it; this is just so the substitution
// code above will write the wrong ROID into the file.
contact = contact.asBuilder().setRepoId("DEADBEEF_TLD-ROID").build();
EppException thrown =
assertThrows(
BadAuthInfoForResourceException.class,
() -> doFailingTest("contact_transfer_query_with_roid.xml"));
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
void testFailure_neverBeenTransferred() {
changeTransferStatus(null);
EppException thrown =
assertThrows(
NoTransferHistoryToQueryException.class,
() -> doFailingTest("contact_transfer_query.xml"));
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
void testFailure_unrelatedClient() {
setRegistrarIdForFlow("ClientZ");
EppException thrown =
assertThrows(
NotAuthorizedToViewTransferException.class,
() -> doFailingTest("contact_transfer_query.xml"));
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
void testFailure_deletedContact() throws Exception {
contact =
persistResource(contact.asBuilder().setDeletionTime(clock.nowUtc().minusDays(1)).build());
ResourceDoesNotExistException thrown =
assertThrows(
ResourceDoesNotExistException.class, () -> doFailingTest("contact_transfer_query.xml"));
assertThat(thrown).hasMessageThat().contains(String.format("(%s)", getUniqueIdFromCommand()));
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
void testFailure_nonexistentContact() throws Exception {
persistResource(contact.asBuilder().setDeletionTime(clock.nowUtc().minusDays(1)).build());
ResourceDoesNotExistException thrown =
assertThrows(
ResourceDoesNotExistException.class, () -> doFailingTest("contact_transfer_query.xml"));
assertThat(thrown).hasMessageThat().contains(String.format("(%s)", getUniqueIdFromCommand()));
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
void testIcannActivityReportField_getsLogged() throws Exception {
runFlow();
assertIcannReportingActivityFieldLogged("srs-cont-transfer-query");
void testThrowsException() {
assertAboutEppExceptions()
.that(assertThrows(ContactsProhibitedException.class, this::runFlow))
.marshalsToXml();
}
}
@@ -14,253 +14,24 @@
package google.registry.flows.contact;
import static com.google.common.collect.MoreCollectors.onlyElement;
import static com.google.common.truth.Truth.assertThat;
import static google.registry.testing.ContactSubject.assertAboutContacts;
import static google.registry.testing.DatabaseHelper.assertNoBillingEvents;
import static google.registry.testing.DatabaseHelper.getOnlyPollMessage;
import static google.registry.testing.DatabaseHelper.getPollMessages;
import static google.registry.testing.DatabaseHelper.persistResource;
import static google.registry.testing.EppExceptionSubject.assertAboutEppExceptions;
import static org.junit.jupiter.api.Assertions.assertThrows;
import google.registry.flows.EppException;
import google.registry.flows.FlowUtils.NotLoggedInException;
import google.registry.flows.ResourceFlowUtils.BadAuthInfoForResourceException;
import google.registry.flows.ResourceFlowUtils.ResourceDoesNotExistException;
import google.registry.flows.ResourceFlowUtils.ResourceNotOwnedException;
import google.registry.flows.exceptions.NotPendingTransferException;
import google.registry.model.contact.Contact;
import google.registry.model.contact.ContactAuthInfo;
import google.registry.model.eppcommon.AuthInfo.PasswordAuth;
import google.registry.model.eppcommon.Trid;
import google.registry.model.poll.PendingActionNotificationResponse;
import google.registry.model.poll.PollMessage;
import google.registry.model.reporting.HistoryEntry;
import google.registry.model.transfer.TransferData;
import google.registry.model.transfer.TransferResponse;
import google.registry.model.transfer.TransferStatus;
import org.junit.jupiter.api.BeforeEach;
import google.registry.flows.FlowTestCase;
import google.registry.flows.exceptions.ContactsProhibitedException;
import org.junit.jupiter.api.Test;
/** Unit tests for {@link ContactTransferRejectFlow}. */
class ContactTransferRejectFlowTest
extends ContactTransferFlowTestCase<ContactTransferRejectFlow, Contact> {
class ContactTransferRejectFlowTest extends FlowTestCase<ContactTransferRejectFlow> {
@BeforeEach
void setUp() {
ContactTransferRejectFlowTest() {
setEppInput("contact_transfer_reject.xml");
setRegistrarIdForFlow("TheRegistrar");
setupContactWithPendingTransfer();
clock.advanceOneMilli();
}
private void doSuccessfulTest(String commandFilename, String expectedXmlFilename)
throws Exception {
setEppInput(commandFilename);
// Look in the future and make sure the poll messages for implicit ack are there.
assertThat(getPollMessages("NewRegistrar", clock.nowUtc().plusMonths(1)))
.hasSize(1);
assertThat(getPollMessages("TheRegistrar", clock.nowUtc().plusMonths(1)))
.hasSize(1);
// Setup done; run the test.
contact = reloadResourceByForeignKey();
TransferData originalTransferData = contact.getTransferData();
assertMutatingFlow(true);
runFlowAssertResponse(loadFile(expectedXmlFilename));
// Transfer should have failed. Verify correct fields were set.
contact = reloadResourceByForeignKey();
assertAboutContacts()
.that(contact)
.hasCurrentSponsorRegistrarId("TheRegistrar")
.and()
.hasLastTransferTimeNotEqualTo(clock.nowUtc())
.and()
.hasOneHistoryEntryEachOfTypes(
HistoryEntry.Type.CONTACT_TRANSFER_REQUEST, HistoryEntry.Type.CONTACT_TRANSFER_REJECT);
assertThat(contact.getTransferData())
.isEqualTo(
originalTransferData.copyConstantFieldsToBuilder()
.setTransferStatus(TransferStatus.CLIENT_REJECTED)
.setPendingTransferExpirationTime(clock.nowUtc())
.build());
// The poll message (in the future) to the losing registrar for implicit ack should be gone.
assertThat(getPollMessages("TheRegistrar", clock.nowUtc().plusMonths(1)))
.isEmpty();
// The poll message in the future to the gaining registrar should be gone too, but there
// should be one at the current time to the gaining registrar.
PollMessage gainingPollMessage = getOnlyPollMessage("NewRegistrar");
assertThat(gainingPollMessage.getEventTime()).isEqualTo(clock.nowUtc());
assertThat(
gainingPollMessage
.getResponseData()
.stream()
.filter(TransferResponse.class::isInstance)
.map(TransferResponse.class::cast)
.collect(onlyElement())
.getTransferStatus())
.isEqualTo(TransferStatus.CLIENT_REJECTED);
PendingActionNotificationResponse panData =
gainingPollMessage
.getResponseData()
.stream()
.filter(PendingActionNotificationResponse.class::isInstance)
.map(PendingActionNotificationResponse.class::cast)
.collect(onlyElement());
assertThat(panData.getTrid())
.isEqualTo(Trid.create("transferClient-trid", "transferServer-trid"));
assertThat(panData.getActionResult()).isFalse();
assertNoBillingEvents();
assertLastHistoryContainsResource(contact);
}
private void doFailingTest(String commandFilename) throws Exception {
setEppInput(commandFilename);
// Setup done; run the test.
assertMutatingFlow(true);
runFlow();
}
@Test
void testNotLoggedIn() {
sessionMetadata.setRegistrarId(null);
EppException thrown = assertThrows(NotLoggedInException.class, this::runFlow);
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
void testDryRun() throws Exception {
setEppInput("contact_transfer_reject.xml");
dryRunFlowAssertResponse(loadFile("contact_transfer_reject_response.xml"));
}
@Test
void testSuccess() throws Exception {
doSuccessfulTest("contact_transfer_reject.xml", "contact_transfer_reject_response.xml");
}
@Test
void testSuccess_domainAuthInfo() throws Exception {
doSuccessfulTest("contact_transfer_reject_with_authinfo.xml",
"contact_transfer_reject_response.xml");
}
@Test
void testFailure_badPassword() {
// Change the contact's password so it does not match the password in the file.
contact =
persistResource(
contact
.asBuilder()
.setAuthInfo(ContactAuthInfo.create(PasswordAuth.create("badpassword")))
.build());
EppException thrown =
assertThrows(
BadAuthInfoForResourceException.class,
() -> doFailingTest("contact_transfer_reject_with_authinfo.xml"));
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
void testFailure_neverBeenTransferred() {
changeTransferStatus(null);
EppException thrown =
assertThrows(
NotPendingTransferException.class, () -> doFailingTest("contact_transfer_reject.xml"));
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
void testFailure_clientApproved() {
changeTransferStatus(TransferStatus.CLIENT_APPROVED);
EppException thrown =
assertThrows(
NotPendingTransferException.class, () -> doFailingTest("contact_transfer_reject.xml"));
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
void testFailure_clientRejected() {
changeTransferStatus(TransferStatus.CLIENT_REJECTED);
EppException thrown =
assertThrows(
NotPendingTransferException.class, () -> doFailingTest("contact_transfer_reject.xml"));
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
void testFailure_clientCancelled() {
changeTransferStatus(TransferStatus.CLIENT_CANCELLED);
EppException thrown =
assertThrows(
NotPendingTransferException.class, () -> doFailingTest("contact_transfer_reject.xml"));
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
void testFailure_serverApproved() {
changeTransferStatus(TransferStatus.SERVER_APPROVED);
EppException thrown =
assertThrows(
NotPendingTransferException.class, () -> doFailingTest("contact_transfer_reject.xml"));
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
void testFailure_serverCancelled() {
changeTransferStatus(TransferStatus.SERVER_CANCELLED);
EppException thrown =
assertThrows(
NotPendingTransferException.class, () -> doFailingTest("contact_transfer_reject.xml"));
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
void testFailure_gainingClient() {
setRegistrarIdForFlow("NewRegistrar");
EppException thrown =
assertThrows(
ResourceNotOwnedException.class, () -> doFailingTest("contact_transfer_reject.xml"));
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
void testFailure_unrelatedClient() {
setRegistrarIdForFlow("ClientZ");
EppException thrown =
assertThrows(
ResourceNotOwnedException.class, () -> doFailingTest("contact_transfer_reject.xml"));
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
void testFailure_deletedContact() throws Exception {
contact =
persistResource(contact.asBuilder().setDeletionTime(clock.nowUtc().minusDays(1)).build());
ResourceDoesNotExistException thrown =
assertThrows(
ResourceDoesNotExistException.class,
() -> doFailingTest("contact_transfer_reject.xml"));
assertThat(thrown).hasMessageThat().contains(String.format("(%s)", getUniqueIdFromCommand()));
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
void testFailure_nonexistentContact() throws Exception {
persistResource(contact.asBuilder().setDeletionTime(clock.nowUtc().minusDays(1)).build());
ResourceDoesNotExistException thrown =
assertThrows(
ResourceDoesNotExistException.class,
() -> doFailingTest("contact_transfer_reject.xml"));
assertThat(thrown).hasMessageThat().contains(String.format("(%s)", getUniqueIdFromCommand()));
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
void testIcannActivityReportField_getsLogged() throws Exception {
runFlow();
assertIcannReportingActivityFieldLogged("srs-cont-transfer-reject");
void testThrowsException() {
assertAboutEppExceptions()
.that(assertThrows(ContactsProhibitedException.class, this::runFlow))
.marshalsToXml();
}
}
@@ -14,304 +14,24 @@
package google.registry.flows.contact;
import static com.google.common.base.Predicates.equalTo;
import static com.google.common.base.Predicates.not;
import static com.google.common.collect.Iterables.getOnlyElement;
import static com.google.common.collect.MoreCollectors.onlyElement;
import static com.google.common.truth.Truth.assertThat;
import static google.registry.config.RegistryConfig.getContactAutomaticTransferLength;
import static google.registry.testing.ContactSubject.assertAboutContacts;
import static google.registry.testing.DatabaseHelper.assertNoBillingEvents;
import static google.registry.testing.DatabaseHelper.assertPollMessagesEqual;
import static google.registry.testing.DatabaseHelper.deleteResource;
import static google.registry.testing.DatabaseHelper.getPollMessages;
import static google.registry.testing.DatabaseHelper.loadByKeys;
import static google.registry.testing.DatabaseHelper.persistActiveContact;
import static google.registry.testing.DatabaseHelper.persistResource;
import static google.registry.testing.EppExceptionSubject.assertAboutEppExceptions;
import static google.registry.util.CollectionUtils.forceEmptyToNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Iterables;
import google.registry.flows.EppException;
import google.registry.flows.FlowUtils.NotLoggedInException;
import google.registry.flows.ResourceFlowUtils.BadAuthInfoForResourceException;
import google.registry.flows.ResourceFlowUtils.ResourceDoesNotExistException;
import google.registry.flows.exceptions.AlreadyPendingTransferException;
import google.registry.flows.exceptions.MissingTransferRequestAuthInfoException;
import google.registry.flows.exceptions.ObjectAlreadySponsoredException;
import google.registry.flows.exceptions.ResourceStatusProhibitsOperationException;
import google.registry.model.contact.Contact;
import google.registry.model.contact.ContactAuthInfo;
import google.registry.model.eppcommon.AuthInfo.PasswordAuth;
import google.registry.model.eppcommon.StatusValue;
import google.registry.model.eppcommon.Trid;
import google.registry.model.poll.PollMessage;
import google.registry.model.reporting.HistoryEntry;
import google.registry.model.transfer.ContactTransferData;
import google.registry.model.transfer.TransferStatus;
import org.joda.time.DateTime;
import org.junit.jupiter.api.BeforeEach;
import google.registry.flows.FlowTestCase;
import google.registry.flows.exceptions.ContactsProhibitedException;
import org.junit.jupiter.api.Test;
/** Unit tests for {@link ContactTransferRequestFlow}. */
class ContactTransferRequestFlowTest
extends ContactTransferFlowTestCase<ContactTransferRequestFlow, Contact> {
class ContactTransferRequestFlowTest extends FlowTestCase<ContactTransferRequestFlow> {
ContactTransferRequestFlowTest() {
// We need the transfer to happen at exactly this time in order for the response to match up.
clock.setTo(DateTime.parse("2000-06-08T22:00:00.0Z"));
}
@BeforeEach
void beforeEach() {
setEppInput("contact_transfer_request.xml");
setRegistrarIdForFlow("NewRegistrar");
contact = persistActiveContact("sh8013");
clock.advanceOneMilli();
}
private void doSuccessfulTest(String commandFilename, String expectedXmlFilename)
throws Exception {
setEppInput(commandFilename);
DateTime afterTransfer = clock.nowUtc().plus(getContactAutomaticTransferLength());
// Setup done; run the test.
assertMutatingFlow(true);
runFlowAssertResponse(loadFile(expectedXmlFilename));
// Transfer should have been requested. Verify correct fields were set.
contact = reloadResourceByForeignKey();
assertAboutContacts()
.that(contact)
.hasCurrentSponsorRegistrarId("TheRegistrar")
.and()
.hasOnlyOneHistoryEntryWhich()
.hasType(HistoryEntry.Type.CONTACT_TRANSFER_REQUEST);
Trid expectedTrid =
Trid.create(
getClientTrid(),
contact.getTransferData().getTransferRequestTrid().getServerTransactionId());
assertThat(contact.getTransferData())
.isEqualTo(
new ContactTransferData.Builder()
.setTransferRequestTrid(expectedTrid)
.setTransferRequestTime(clock.nowUtc())
.setGainingRegistrarId("NewRegistrar")
.setLosingRegistrarId("TheRegistrar")
.setTransferStatus(TransferStatus.PENDING)
.setPendingTransferExpirationTime(afterTransfer)
// Make the server-approve entities field a no-op comparison; it's easier to
// do this comparison separately below.
.setServerApproveEntities(
contact.getRepoId(),
contact.getTransferData().getHistoryEntryId(),
forceEmptyToNull(contact.getTransferData().getServerApproveEntities()))
.build());
assertNoBillingEvents();
assertThat(getPollMessages("TheRegistrar", clock.nowUtc())).hasSize(1);
PollMessage losingRequestMessage =
getOnlyElement(getPollMessages("TheRegistrar", clock.nowUtc()));
// If we fast forward AUTOMATIC_TRANSFER_DAYS the transfer should have happened.
assertAboutContacts()
.that(contact.cloneProjectedAtTime(afterTransfer))
.hasCurrentSponsorRegistrarId("NewRegistrar");
assertThat(getPollMessages("NewRegistrar", afterTransfer)).hasSize(1);
assertThat(getPollMessages("TheRegistrar", afterTransfer)).hasSize(2);
PollMessage gainingApproveMessage =
getOnlyElement(getPollMessages("NewRegistrar", afterTransfer));
PollMessage losingApproveMessage =
getPollMessages("TheRegistrar", afterTransfer)
.stream()
.filter(not(equalTo(losingRequestMessage)))
.collect(onlyElement());
// Check for TransferData server-approve entities containing what we expect: only
// poll messages, the approval notice ones for gaining and losing registrars.
assertPollMessagesEqual(
Iterables.filter(
loadByKeys(contact.getTransferData().getServerApproveEntities()), PollMessage.class),
ImmutableList.of(gainingApproveMessage, losingApproveMessage));
assertLastHistoryContainsResource(contact);
}
private void doFailingTest(String commandFilename) throws Exception {
setEppInput(commandFilename);
// Setup done; run the test.
assertMutatingFlow(true);
runFlow();
}
@Test
void testNotLoggedIn() {
sessionMetadata.setRegistrarId(null);
EppException thrown = assertThrows(NotLoggedInException.class, this::runFlow);
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
void testDryRun() throws Exception {
setEppInput("contact_transfer_request.xml");
dryRunFlowAssertResponse(loadFile("contact_transfer_request_response.xml"));
}
@Test
void testSuccess() throws Exception {
doSuccessfulTest("contact_transfer_request.xml", "contact_transfer_request_response.xml");
}
@Test
void testFailure_noAuthInfo() {
EppException thrown =
assertThrows(
MissingTransferRequestAuthInfoException.class,
() -> doFailingTest("contact_transfer_request_no_authinfo.xml"));
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
void testFailure_badPassword() {
// Change the contact's password so it does not match the password in the file.
contact =
persistResource(
contact
.asBuilder()
.setAuthInfo(ContactAuthInfo.create(PasswordAuth.create("badpassword")))
.build());
EppException thrown =
assertThrows(
BadAuthInfoForResourceException.class,
() -> doFailingTest("contact_transfer_request.xml"));
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
void testSuccess_clientApproved() throws Exception {
changeTransferStatus(TransferStatus.CLIENT_APPROVED);
doSuccessfulTest("contact_transfer_request.xml", "contact_transfer_request_response.xml");
}
@Test
void testSuccess_clientRejected() throws Exception {
changeTransferStatus(TransferStatus.CLIENT_REJECTED);
doSuccessfulTest("contact_transfer_request.xml", "contact_transfer_request_response.xml");
}
@Test
void testSuccess_clientCancelled() throws Exception {
changeTransferStatus(TransferStatus.CLIENT_CANCELLED);
doSuccessfulTest("contact_transfer_request.xml", "contact_transfer_request_response.xml");
}
@Test
void testSuccess_serverApproved() throws Exception {
changeTransferStatus(TransferStatus.SERVER_APPROVED);
doSuccessfulTest("contact_transfer_request.xml", "contact_transfer_request_response.xml");
}
@Test
void testSuccess_serverCancelled() throws Exception {
changeTransferStatus(TransferStatus.SERVER_CANCELLED);
doSuccessfulTest("contact_transfer_request.xml", "contact_transfer_request_response.xml");
}
@Test
void testFailure_pending() {
contact =
persistResource(
contact
.asBuilder()
.setTransferData(
contact
.getTransferData()
.asBuilder()
.setTransferStatus(TransferStatus.PENDING)
.setPendingTransferExpirationTime(clock.nowUtc().plusDays(1))
.build())
.build());
EppException thrown =
assertThrows(
AlreadyPendingTransferException.class,
() -> doFailingTest("contact_transfer_request.xml"));
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
void testFailure_sponsoringClient() {
setRegistrarIdForFlow("TheRegistrar");
EppException thrown =
assertThrows(
ObjectAlreadySponsoredException.class,
() -> doFailingTest("contact_transfer_request.xml"));
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
void testFailure_deletedContact() throws Exception {
contact =
persistResource(contact.asBuilder().setDeletionTime(clock.nowUtc().minusDays(1)).build());
ResourceDoesNotExistException thrown =
assertThrows(
ResourceDoesNotExistException.class,
() -> doFailingTest("contact_transfer_request.xml"));
assertThat(thrown).hasMessageThat().contains(String.format("(%s)", getUniqueIdFromCommand()));
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
void testFailure_nonexistentContact() throws Exception {
deleteResource(contact);
ResourceDoesNotExistException thrown =
assertThrows(
ResourceDoesNotExistException.class,
() -> doFailingTest("contact_transfer_request.xml"));
assertThat(thrown).hasMessageThat().contains(String.format("(%s)", getUniqueIdFromCommand()));
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
void testFailure_clientTransferProhibited() {
contact =
persistResource(
contact.asBuilder().addStatusValue(StatusValue.CLIENT_TRANSFER_PROHIBITED).build());
ResourceStatusProhibitsOperationException thrown =
assertThrows(
ResourceStatusProhibitsOperationException.class,
() -> doFailingTest("contact_transfer_request.xml"));
assertThat(thrown).hasMessageThat().contains("clientTransferProhibited");
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
void testFailure_serverTransferProhibited() {
contact =
persistResource(
contact.asBuilder().addStatusValue(StatusValue.SERVER_TRANSFER_PROHIBITED).build());
ResourceStatusProhibitsOperationException thrown =
assertThrows(
ResourceStatusProhibitsOperationException.class,
() -> doFailingTest("contact_transfer_request.xml"));
assertThat(thrown).hasMessageThat().contains("serverTransferProhibited");
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
void testFailure_pendingDelete() {
contact =
persistResource(contact.asBuilder().addStatusValue(StatusValue.PENDING_DELETE).build());
ResourceStatusProhibitsOperationException thrown =
assertThrows(
ResourceStatusProhibitsOperationException.class,
() -> doFailingTest("contact_transfer_request.xml"));
assertThat(thrown).hasMessageThat().contains("pendingDelete");
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
void testIcannActivityReportField_getsLogged() throws Exception {
runFlow();
assertIcannReportingActivityFieldLogged("srs-cont-transfer-request");
void testThrowsException() {
assertAboutEppExceptions()
.that(assertThrows(ContactsProhibitedException.class, this::runFlow))
.marshalsToXml();
}
}
@@ -14,449 +14,24 @@
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;
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.ImmutableList;
import com.google.common.collect.ImmutableSet;
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.ResourceFlowUtils.AddRemoveSameValueException;
import google.registry.flows.ResourceFlowUtils.ResourceDoesNotExistException;
import google.registry.flows.ResourceFlowUtils.ResourceNotOwnedException;
import google.registry.flows.ResourceFlowUtils.StatusNotClientSettableException;
import google.registry.flows.contact.ContactFlowUtils.BadInternationalizedPostalInfoException;
import google.registry.flows.contact.ContactFlowUtils.DeclineContactDisclosureFieldDisallowedPolicyException;
import google.registry.flows.FlowTestCase;
import google.registry.flows.exceptions.ContactsProhibitedException;
import google.registry.flows.exceptions.ResourceHasClientUpdateProhibitedException;
import google.registry.flows.exceptions.ResourceStatusProhibitsOperationException;
import google.registry.model.common.FeatureFlag;
import google.registry.model.contact.Contact;
import google.registry.model.contact.ContactAddress;
import google.registry.model.contact.PostalInfo;
import google.registry.model.contact.PostalInfo.Type;
import google.registry.model.eppcommon.StatusValue;
import org.junit.jupiter.api.Test;
/** Unit tests for {@link ContactUpdateFlow}. */
class ContactUpdateFlowTest extends ResourceFlowTestCase<ContactUpdateFlow, Contact> {
class ContactUpdateFlowTest extends FlowTestCase<ContactUpdateFlow> {
ContactUpdateFlowTest() {
setEppInput("contact_update.xml");
}
private void doSuccessfulTest() throws Exception {
clock.advanceOneMilli();
assertMutatingFlow(true);
runFlowAssertResponse(loadFile("generic_success_response.xml"));
Contact contact = reloadResourceByForeignKey();
// Check that the contact was updated. This value came from the xml.
assertAboutContacts()
.that(contact)
.hasAuthInfoPwd("2fooBAR")
.and()
.hasOnlyOneHistoryEntryWhich()
.hasNoXml();
assertNoBillingEvents();
assertLastHistoryContainsResource(contact);
}
@Test
void testNotLoggedIn() {
sessionMetadata.setRegistrarId(null);
EppException thrown = assertThrows(NotLoggedInException.class, this::runFlow);
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
void testDryRun() throws Exception {
persistActiveContact(getUniqueIdFromCommand());
dryRunFlowAssertResponse(loadFile("generic_success_response.xml"));
}
@Test
void testSuccess() throws Exception {
persistActiveContact(getUniqueIdFromCommand());
doSuccessfulTest();
}
@Test
void testFailure_minimumDatasetPhase2_cannotUpdateContacts() throws Exception {
persistResource(
new FeatureFlag.Builder()
.setFeatureName(MINIMUM_DATASET_CONTACTS_PROHIBITED)
.setStatusMap(
ImmutableSortedMap.of(START_OF_TIME, INACTIVE, clock.nowUtc().minusDays(5), ACTIVE))
.build());
EppException thrown = assertThrows(ContactsProhibitedException.class, this::runFlow);
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
void testSuccess_updatingInternationalizedPostalInfoDeletesLocalized() throws Exception {
Contact contact =
persistResource(
newContact(getUniqueIdFromCommand())
.asBuilder()
.setLocalizedPostalInfo(
new PostalInfo.Builder()
.setType(Type.LOCALIZED)
.setAddress(
new ContactAddress.Builder()
.setStreet(ImmutableList.of("111 8th Ave", "4th Floor"))
.setCity("New York")
.setState("NY")
.setZip("10011")
.setCountryCode("US")
.build())
.build())
.build());
clock.advanceOneMilli();
// The test xml updates the internationalized postal info and should therefore implicitly delete
// the localized one since they are treated as a pair for update purposes.
assertAboutContacts().that(contact)
.hasNonNullLocalizedPostalInfo().and()
.hasNullInternationalizedPostalInfo();
runFlowAssertResponse(loadFile("generic_success_response.xml"));
assertAboutContacts().that(reloadResourceByForeignKey())
.hasNullLocalizedPostalInfo().and()
.hasInternationalizedPostalInfo(new PostalInfo.Builder()
.setType(Type.INTERNATIONALIZED)
.setAddress(new ContactAddress.Builder()
.setStreet(ImmutableList.of("124 Example Dr.", "Suite 200"))
.setCity("Dulles")
.setState("VA")
.setZip("20166-6503")
.setCountryCode("US")
.build())
.build());
}
@Test
void testSuccess_updatingLocalizedPostalInfoDeletesInternationalized() throws Exception {
setEppInput("contact_update_localized.xml");
Contact contact =
persistResource(
newContact(getUniqueIdFromCommand())
.asBuilder()
.setInternationalizedPostalInfo(
new PostalInfo.Builder()
.setType(Type.INTERNATIONALIZED)
.setAddress(
new ContactAddress.Builder()
.setStreet(ImmutableList.of("111 8th Ave", "4th Floor"))
.setCity("New York")
.setState("NY")
.setZip("10011")
.setCountryCode("US")
.build())
.build())
.build());
clock.advanceOneMilli();
// The test xml updates the localized postal info and should therefore implicitly delete
// the internationalized one since they are treated as a pair for update purposes.
assertAboutContacts().that(contact)
.hasNonNullInternationalizedPostalInfo().and()
.hasNullLocalizedPostalInfo();
runFlowAssertResponse(loadFile("generic_success_response.xml"));
assertAboutContacts().that(reloadResourceByForeignKey())
.hasNullInternationalizedPostalInfo().and()
.hasLocalizedPostalInfo(new PostalInfo.Builder()
.setType(Type.LOCALIZED)
.setAddress(new ContactAddress.Builder()
.setStreet(ImmutableList.of("124 Example Dr.", "Suite 200"))
.setCity("Dulles")
.setState("VA")
.setZip("20166-6503")
.setCountryCode("US")
.build())
.build());
}
@Test
void testSuccess_partialPostalInfoUpdate() throws Exception {
setEppInput("contact_update_partial_postalinfo.xml");
persistResource(
newContact(getUniqueIdFromCommand())
.asBuilder()
.setLocalizedPostalInfo(
new PostalInfo.Builder()
.setType(Type.LOCALIZED)
.setName("A. Person")
.setOrg("Company Inc.")
.setAddress(
new ContactAddress.Builder()
.setStreet(ImmutableList.of("123 4th st", "5th Floor"))
.setCity("City")
.setState("AB")
.setZip("12345")
.setCountryCode("US")
.build())
.build())
.build());
clock.advanceOneMilli();
// The test xml updates the address of the postal info and should leave the name untouched.
runFlowAssertResponse(loadFile("generic_success_response.xml"));
assertAboutContacts().that(reloadResourceByForeignKey()).hasLocalizedPostalInfo(
new PostalInfo.Builder()
.setType(Type.LOCALIZED)
.setName("A. Person")
.setOrg("Company Inc.")
.setAddress(new ContactAddress.Builder()
.setStreet(ImmutableList.of("456 5th st"))
.setCity("Place")
.setState("CD")
.setZip("54321")
.setCountryCode("US")
.build())
.build());
}
@Test
void testSuccess_updateOnePostalInfo_touchOtherPostalInfoPreservesIt() throws Exception {
setEppInput("contact_update_partial_postalinfo_preserve_int.xml");
persistResource(
newContact(getUniqueIdFromCommand())
.asBuilder()
.setLocalizedPostalInfo(
new PostalInfo.Builder()
.setType(Type.LOCALIZED)
.setName("A. Person")
.setOrg("Company Inc.")
.setAddress(
new ContactAddress.Builder()
.setStreet(ImmutableList.of("123 4th st", "5th Floor"))
.setCity("City")
.setState("AB")
.setZip("12345")
.setCountryCode("US")
.build())
.build())
.setInternationalizedPostalInfo(
new PostalInfo.Builder()
.setType(Type.INTERNATIONALIZED)
.setName("B. Person")
.setOrg("Company Co.")
.setAddress(
new ContactAddress.Builder()
.setStreet(ImmutableList.of("100 200th Dr.", "6th Floor"))
.setCity("Town")
.setState("CD")
.setZip("67890")
.setCountryCode("US")
.build())
.build())
.build());
clock.advanceOneMilli();
// The test xml updates the address of the localized postal info. It also sets the name of the
// internationalized postal info to the same value it previously had, which causes it to be
// preserved. If the xml had not mentioned the internationalized one at all it would have been
// deleted.
runFlowAssertResponse(loadFile("generic_success_response.xml"));
assertAboutContacts().that(reloadResourceByForeignKey())
.hasLocalizedPostalInfo(
new PostalInfo.Builder()
.setType(Type.LOCALIZED)
.setName("A. Person")
.setOrg("Company Inc.")
.setAddress(new ContactAddress.Builder()
.setStreet(ImmutableList.of("456 5th st"))
.setCity("Place")
.setState("CD")
.setZip("54321")
.setCountryCode("US")
.build())
.build())
.and()
.hasInternationalizedPostalInfo(
new PostalInfo.Builder()
.setType(Type.INTERNATIONALIZED)
.setName("B. Person")
.setOrg("Company Co.")
.setAddress(new ContactAddress.Builder()
.setStreet(ImmutableList.of("100 200th Dr.", "6th Floor"))
.setCity("Town")
.setState("CD")
.setZip("67890")
.setCountryCode("US")
.build())
.build());
}
@Test
void testFailure_neverExisted() throws Exception {
ResourceDoesNotExistException thrown =
assertThrows(ResourceDoesNotExistException.class, this::runFlow);
assertThat(thrown).hasMessageThat().contains(String.format("(%s)", getUniqueIdFromCommand()));
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
void testFailure_existedButWasDeleted() throws Exception {
persistDeletedContact(getUniqueIdFromCommand(), clock.nowUtc().minusDays(1));
ResourceDoesNotExistException thrown =
assertThrows(ResourceDoesNotExistException.class, this::runFlow);
assertThat(thrown).hasMessageThat().contains(String.format("(%s)", getUniqueIdFromCommand()));
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
void testFailure_statusValueNotClientSettable() throws Exception {
setEppInput("contact_update_prohibited_status.xml");
persistActiveContact(getUniqueIdFromCommand());
EppException thrown = assertThrows(StatusNotClientSettableException.class, this::runFlow);
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
void testSuccess_superuserStatusValueNotClientSettable() throws Exception {
setEppInput("contact_update_prohibited_status.xml");
persistActiveContact(getUniqueIdFromCommand());
clock.advanceOneMilli();
runFlowAssertResponse(
CommitMode.LIVE, UserPrivileges.SUPERUSER, loadFile("generic_success_response.xml"));
}
@Test
void testFailure_unauthorizedClient() throws Exception {
sessionMetadata.setRegistrarId("NewRegistrar");
persistActiveContact(getUniqueIdFromCommand());
EppException thrown = assertThrows(ResourceNotOwnedException.class, this::runFlow);
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
void testSuccess_superuserUnauthorizedClient() throws Exception {
sessionMetadata.setRegistrarId("NewRegistrar");
persistActiveContact(getUniqueIdFromCommand());
clock.advanceOneMilli();
runFlowAssertResponse(
CommitMode.LIVE, UserPrivileges.SUPERUSER, loadFile("generic_success_response.xml"));
}
@Test
void testSuccess_clientUpdateProhibited_removed() throws Exception {
setEppInput("contact_update_remove_client_update_prohibited.xml");
persistResource(
newContact(getUniqueIdFromCommand())
.asBuilder()
.setStatusValues(ImmutableSet.of(StatusValue.CLIENT_UPDATE_PROHIBITED))
.build());
doSuccessfulTest();
assertAboutContacts()
.that(reloadResourceByForeignKey())
.doesNotHaveStatusValue(StatusValue.CLIENT_UPDATE_PROHIBITED);
}
@Test
void testSuccess_superuserClientUpdateProhibited_notRemoved() throws Exception {
setEppInput("contact_update_prohibited_status.xml");
persistResource(
newContact(getUniqueIdFromCommand())
.asBuilder()
.setStatusValues(ImmutableSet.of(StatusValue.CLIENT_UPDATE_PROHIBITED))
.build());
clock.advanceOneMilli();
runFlowAssertResponse(
CommitMode.LIVE, UserPrivileges.SUPERUSER, loadFile("generic_success_response.xml"));
assertAboutContacts()
.that(reloadResourceByForeignKey())
.hasStatusValue(StatusValue.CLIENT_UPDATE_PROHIBITED)
.and()
.hasStatusValue(StatusValue.SERVER_DELETE_PROHIBITED);
}
@Test
void testFailure_clientUpdateProhibited_notRemoved() throws Exception {
persistResource(
newContact(getUniqueIdFromCommand())
.asBuilder()
.setStatusValues(ImmutableSet.of(StatusValue.CLIENT_UPDATE_PROHIBITED))
.build());
EppException thrown =
assertThrows(ResourceHasClientUpdateProhibitedException.class, this::runFlow);
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
void testFailure_serverUpdateProhibited() throws Exception {
persistResource(
newContact(getUniqueIdFromCommand())
.asBuilder()
.setStatusValues(ImmutableSet.of(StatusValue.SERVER_UPDATE_PROHIBITED))
.build());
ResourceStatusProhibitsOperationException thrown =
assertThrows(ResourceStatusProhibitsOperationException.class, this::runFlow);
assertThat(thrown).hasMessageThat().contains("serverUpdateProhibited");
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
void testFailure_pendingDeleteProhibited() throws Exception {
persistResource(
newContact(getUniqueIdFromCommand())
.asBuilder()
.setStatusValues(ImmutableSet.of(StatusValue.PENDING_DELETE))
.build());
ResourceStatusProhibitsOperationException thrown =
assertThrows(ResourceStatusProhibitsOperationException.class, this::runFlow);
assertThat(thrown).hasMessageThat().contains("pendingDelete");
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
void testSuccess_nonAsciiInLocAddress() throws Exception {
setEppInput("contact_update_hebrew_loc.xml");
persistActiveContact(getUniqueIdFromCommand());
doSuccessfulTest();
}
@Test
void testFailure_nonAsciiInIntAddress() throws Exception {
setEppInput("contact_update_hebrew_int.xml");
persistActiveContact(getUniqueIdFromCommand());
EppException thrown =
assertThrows(BadInternationalizedPostalInfoException.class, this::runFlow);
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
void testFailure_declineDisclosure() throws Exception {
setEppInput("contact_update_decline_disclosure.xml");
persistActiveContact(getUniqueIdFromCommand());
EppException thrown =
assertThrows(DeclineContactDisclosureFieldDisallowedPolicyException.class, this::runFlow);
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
void testFailure_addRemoveSameValue() throws Exception {
setEppInput("contact_update_add_remove_same.xml");
persistActiveContact(getUniqueIdFromCommand());
EppException thrown = assertThrows(AddRemoveSameValueException.class, this::runFlow);
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
void testIcannActivityReportField_getsLogged() throws Exception {
persistActiveContact(getUniqueIdFromCommand());
clock.advanceOneMilli();
runFlow();
assertIcannReportingActivityFieldLogged("srs-cont-update");
void testThrowsException() {
assertAboutEppExceptions()
.that(assertThrows(ContactsProhibitedException.class, this::runFlow))
.marshalsToXml();
}
}
@@ -20,7 +20,9 @@ import static google.registry.testing.DatabaseHelper.persistDeletedHost;
import static google.registry.testing.EppExceptionSubject.assertAboutEppExceptions;
import static org.junit.jupiter.api.Assertions.assertThrows;
import com.google.common.collect.ImmutableMap;
import google.registry.flows.EppException;
import google.registry.flows.EppException.ParameterValueSyntaxErrorException;
import google.registry.flows.FlowUtils.NotLoggedInException;
import google.registry.flows.ResourceCheckFlowTestCase;
import google.registry.flows.exceptions.TooManyResourceChecksException;
@@ -95,4 +97,36 @@ class HostCheckFlowTest extends ResourceCheckFlowTestCase<HostCheckFlow, Host> {
runFlow();
assertIcannReportingActivityFieldLogged("srs-host-check");
}
@Test
void testFailure_dotHost() throws Exception {
setEppInput("host_check_generic.xml", ImmutableMap.of("HOSTNAME", ".host"));
assertAboutEppExceptions()
.that(assertThrows(ParameterValueSyntaxErrorException.class, this::runFlow))
.marshalsToXml();
}
@Test
void testFailure_dashHost() {
setEppInput("host_check_generic.xml", ImmutableMap.of("HOSTNAME", "-host"));
assertAboutEppExceptions()
.that(assertThrows(ParameterValueSyntaxErrorException.class, this::runFlow))
.marshalsToXml();
}
@Test
void testFailure_underscoreHost() {
setEppInput("host_check_generic.xml", ImmutableMap.of("HOSTNAME", "_host"));
assertAboutEppExceptions()
.that(assertThrows(ParameterValueSyntaxErrorException.class, this::runFlow))
.marshalsToXml();
}
@Test
void testFailure_hostDash() {
setEppInput("host_check_generic.xml", ImmutableMap.of("HOSTNAME", "host-"));
assertAboutEppExceptions()
.that(assertThrows(ParameterValueSyntaxErrorException.class, this::runFlow))
.marshalsToXml();
}
}
@@ -39,12 +39,13 @@ import google.registry.flows.exceptions.ResourceAlreadyExistsForThisClientExcept
import google.registry.flows.exceptions.ResourceCreateContentionException;
import google.registry.flows.host.HostCreateFlow.SubordinateHostMustHaveIpException;
import google.registry.flows.host.HostCreateFlow.UnexpectedExternalHostIpException;
import google.registry.flows.host.HostFlowUtils.BadHostNameCharacterException;
import google.registry.flows.host.HostFlowUtils.HostNameNotLowerCaseException;
import google.registry.flows.host.HostFlowUtils.HostNameNotNormalizedException;
import google.registry.flows.host.HostFlowUtils.HostNameNotPunyCodedException;
import google.registry.flows.host.HostFlowUtils.HostNameTooLongException;
import google.registry.flows.host.HostFlowUtils.HostNameTooShallowException;
import google.registry.flows.host.HostFlowUtils.InvalidHostNameException;
import google.registry.flows.host.HostFlowUtils.LoopbackIpNotValidForHostException;
import google.registry.flows.host.HostFlowUtils.SuperordinateDomainDoesNotExistException;
import google.registry.flows.host.HostFlowUtils.SuperordinateDomainInPendingDeleteException;
import google.registry.model.ForeignKeyUtils;
@@ -286,7 +287,7 @@ class HostCreateFlowTest extends ResourceFlowTestCase<HostCreateFlow, Host> {
@Test
void testFailure_badCharacter() {
doFailingHostNameTest("foo bar", InvalidHostNameException.class);
doFailingHostNameTest("foo bar", BadHostNameCharacterException.class);
}
@Test
@@ -322,6 +323,26 @@ class HostCreateFlowTest extends ResourceFlowTestCase<HostCreateFlow, Host> {
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
void testFailure_localhostInetAddress_ipv4() {
createTld("tld");
persistActiveDomain("example.tld");
setEppHostCreateInput("ns1.example.tld", "<host:addr ip=\"v4\">127.0.0.1</host:addr>");
assertAboutEppExceptions()
.that(assertThrows(LoopbackIpNotValidForHostException.class, this::runFlow))
.marshalsToXml();
}
@Test
void testFailure_localhostInetAddress_ipv6() {
createTld("tld");
persistActiveDomain("example.tld");
setEppHostCreateInput("ns1.example.tld", "<host:addr ip=\"v6\">::1</host:addr>");
assertAboutEppExceptions()
.that(assertThrows(LoopbackIpNotValidForHostException.class, this::runFlow))
.marshalsToXml();
}
@Test
void testIcannActivityReportField_getsLogged() throws Exception {
runFlow();
@@ -54,13 +54,14 @@ import google.registry.flows.ResourceFlowUtils.ResourceNotOwnedException;
import google.registry.flows.ResourceFlowUtils.StatusNotClientSettableException;
import google.registry.flows.exceptions.ResourceHasClientUpdateProhibitedException;
import google.registry.flows.exceptions.ResourceStatusProhibitsOperationException;
import google.registry.flows.host.HostFlowUtils.BadHostNameCharacterException;
import google.registry.flows.host.HostFlowUtils.HostDomainNotOwnedException;
import google.registry.flows.host.HostFlowUtils.HostNameNotLowerCaseException;
import google.registry.flows.host.HostFlowUtils.HostNameNotNormalizedException;
import google.registry.flows.host.HostFlowUtils.HostNameNotPunyCodedException;
import google.registry.flows.host.HostFlowUtils.HostNameTooLongException;
import google.registry.flows.host.HostFlowUtils.HostNameTooShallowException;
import google.registry.flows.host.HostFlowUtils.InvalidHostNameException;
import google.registry.flows.host.HostFlowUtils.LoopbackIpNotValidForHostException;
import google.registry.flows.host.HostFlowUtils.SuperordinateDomainDoesNotExistException;
import google.registry.flows.host.HostFlowUtils.SuperordinateDomainInPendingDeleteException;
import google.registry.flows.host.HostUpdateFlow.CannotAddIpToExternalHostException;
@@ -1259,7 +1260,7 @@ class HostUpdateFlowTest extends ResourceFlowTestCase<HostUpdateFlow, Host> {
@Test
void testFailure_renameToBadCharacter() throws Exception {
doFailingHostNameTest("foo bar", InvalidHostNameException.class);
doFailingHostNameTest("foo bar", BadHostNameCharacterException.class);
}
@Test
@@ -1306,6 +1307,28 @@ class HostUpdateFlowTest extends ResourceFlowTestCase<HostUpdateFlow, Host> {
doFailingHostNameTest("foo.co.uk", HostNameTooShallowException.class);
}
@Test
void testFailure_localhostInetAddress_ipv4() throws Exception {
createTld("tld");
persistActiveSubordinateHost(oldHostName(), persistActiveDomain("example.tld"));
setEppHostUpdateInput(
"ns1.example.tld", "ns2.example.tld", "<host:addr ip=\"v4\">127.0.0.1</host:addr>", null);
assertAboutEppExceptions()
.that(assertThrows(LoopbackIpNotValidForHostException.class, this::runFlow))
.marshalsToXml();
}
@Test
void testFailure_localhostInetAddress_ipv6() throws Exception {
createTld("tld");
persistActiveSubordinateHost(oldHostName(), persistActiveDomain("example.tld"));
setEppHostUpdateInput(
"ns1.example.tld", "ns2.example.tld", "<host:addr ip=\"v6\">::1</host:addr>", null);
assertAboutEppExceptions()
.that(assertThrows(LoopbackIpNotValidForHostException.class, this::runFlow))
.marshalsToXml();
}
@Test
void testSuccess_metadata() throws Exception {
createTld("tld");
@@ -751,7 +751,9 @@ public final class TldTest extends EntityTestCase {
assertThrows(
IllegalArgumentException.class,
() -> Tld.get("tld").asBuilder().setRoidSuffix("123456789"));
assertThat(e).hasMessageThat().isEqualTo("ROID suffix must be in format ^[A-Z\\d_]{1,8}$");
assertThat(e)
.hasMessageThat()
.isEqualTo("ROID suffix 123456789 must be in format ^[A-Z\\d]{1,8}$");
}
@Test
@@ -766,6 +768,12 @@ public final class TldTest extends EntityTestCase {
IllegalArgumentException.class, () -> Tld.get("tld").asBuilder().setRoidSuffix("ABC-DEF"));
}
@Test
void testFailure_roidSuffixContainsUnderscores() {
assertThrows(
IllegalArgumentException.class, () -> Tld.get("tld").asBuilder().setRoidSuffix("ABC_DEF"));
}
@Test
void testSuccess_setDefaultPromoTokens() {
Tld registry = Tld.get("tld");
@@ -28,6 +28,7 @@ import google.registry.flows.TlsCredentials.EppTlsModule;
import google.registry.flows.custom.CustomLogicModule;
import google.registry.loadtest.LoadTestModule;
import google.registry.monitoring.whitebox.WhiteboxModule;
import google.registry.mosapi.module.MosApiRequestModule;
import google.registry.rdap.RdapModule;
import google.registry.rde.RdeModule;
import google.registry.reporting.ReportingModule;
@@ -60,6 +61,7 @@ import google.registry.ui.server.console.ConsoleModule;
EppToolModule.class,
IcannReportingModule.class,
LoadTestModule.class,
MosApiRequestModule.class,
RdapModule.class,
RdeModule.class,
ReportingModule.class,
@@ -0,0 +1,98 @@
// 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.mosapi;
import static com.google.common.truth.Truth.assertThat;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import com.google.common.collect.ImmutableList;
import com.google.common.net.MediaType;
import com.google.gson.Gson;
import google.registry.mosapi.MosApiModels.AllServicesStateResponse;
import google.registry.mosapi.MosApiModels.ServiceStateSummary;
import google.registry.request.HttpException.ServiceUnavailableException;
import google.registry.testing.FakeResponse;
import java.util.Optional;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
/** Unit tests for {@link GetServiceStateAction}. */
@ExtendWith(MockitoExtension.class)
public class GetServiceStateActionTest {
@Mock private MosApiStateService stateService;
private final FakeResponse response = new FakeResponse();
private final Gson gson = new Gson();
@Test
void testRun_singleTld_returnsStateForTld() throws Exception {
GetServiceStateAction action =
new GetServiceStateAction(stateService, response, gson, Optional.of("example"));
ServiceStateSummary summary = new ServiceStateSummary("example", "Up", null);
when(stateService.getServiceStateSummary("example")).thenReturn(summary);
action.run();
assertThat(response.getContentType()).isEqualTo(MediaType.JSON_UTF_8);
assertThat(response.getPayload())
.contains(
"""
"overallStatus":"Up"
"""
.trim());
verify(stateService).getServiceStateSummary("example");
}
@Test
void testRun_noTld_returnsStateForAll() {
GetServiceStateAction action =
new GetServiceStateAction(stateService, response, gson, Optional.empty());
AllServicesStateResponse allStates = new AllServicesStateResponse(ImmutableList.of());
when(stateService.getAllServiceStateSummaries()).thenReturn(allStates);
action.run();
assertThat(response.getContentType()).isEqualTo(MediaType.JSON_UTF_8);
assertThat(response.getPayload())
.contains(
"""
"serviceStates":[]
"""
.trim());
verify(stateService).getAllServiceStateSummaries();
}
@Test
void testRun_serviceThrowsException_throwsServiceUnavailable() throws Exception {
GetServiceStateAction action =
new GetServiceStateAction(stateService, response, gson, Optional.of("example"));
doThrow(new MosApiException("Backend error", null))
.when(stateService)
.getServiceStateSummary("example");
ServiceUnavailableException thrown =
assertThrows(ServiceUnavailableException.class, action::run);
assertThat(thrown).hasMessageThat().isEqualTo("Error fetching MoSAPI service state.");
}
}
@@ -11,7 +11,7 @@
// 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.mosapi.model;
package google.registry.mosapi;
import static com.google.common.truth.Truth.assertThat;
@@ -20,7 +20,6 @@ import google.registry.mosapi.MosApiException.DateOrderInvalidException;
import google.registry.mosapi.MosApiException.EndDateSyntaxInvalidException;
import google.registry.mosapi.MosApiException.MosApiAuthorizationException;
import google.registry.mosapi.MosApiException.StartDateSyntaxInvalidException;
import google.registry.mosapi.model.MosApiErrorResponse;
import org.junit.jupiter.api.Test;
/** Unit tests for {@link MosApiException}. */
@@ -0,0 +1,172 @@
// 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.mosapi;
import static com.google.common.truth.Truth.assertThat;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import google.registry.mosapi.MosApiModels.AllServicesStateResponse;
import google.registry.mosapi.MosApiModels.IncidentSummary;
import google.registry.mosapi.MosApiModels.ServiceStateSummary;
import google.registry.mosapi.MosApiModels.ServiceStatus;
import google.registry.mosapi.MosApiModels.TldServiceState;
import org.junit.Test;
/** Tests for {@link MosApiModels}. */
public final class MosApiModelsTest {
private static final Gson gson =
new GsonBuilder().excludeFieldsWithoutExposeAnnotation().create();
@Test
public void testAllServicesStateResponse_nullCollection_initializedToEmpty() {
AllServicesStateResponse response = new AllServicesStateResponse(null);
assertThat(response.serviceStates()).isEmpty();
assertThat(response.serviceStates()).isNotNull();
}
@Test
public void testServiceStateSummary_nullCollection_initializedToEmpty() {
ServiceStateSummary summary = new ServiceStateSummary("example", "Up", null);
assertThat(summary.activeIncidents()).isEmpty();
assertThat(summary.activeIncidents()).isNotNull();
}
@Test
public void testServiceStatus_nullCollection_initializedToEmpty() {
ServiceStatus status = new ServiceStatus("Up", 0.0, null);
assertThat(status.incidents()).isEmpty();
assertThat(status.incidents()).isNotNull();
}
@Test
public void testTldServiceState_nullCollection_initializedToEmpty() {
TldServiceState state = new TldServiceState("example", 123456L, "Up", null);
assertThat(state.serviceStatuses()).isEmpty();
assertThat(state.serviceStatuses()).isNotNull();
}
@Test
public void testIncidentSummary_jsonSerialization() {
IncidentSummary incident = new IncidentSummary("inc-123", 1000L, false, "Active", 2000L);
String json = gson.toJson(incident);
// Using Text Blocks to avoid escaping quotes
assertThat(json)
.contains(
"""
"incidentID":"inc-123"
"""
.trim());
assertThat(json)
.contains(
"""
"startTime":1000
"""
.trim());
assertThat(json)
.contains(
"""
"falsePositive":false
"""
.trim());
assertThat(json)
.contains(
"""
"state":"Active"
"""
.trim());
assertThat(json)
.contains(
"""
"endTime":2000
"""
.trim());
}
@Test
public void testServiceStatus_jsonSerialization() {
IncidentSummary incident = new IncidentSummary("inc-1", 1000L, false, "Resolved", null);
ServiceStatus status = new ServiceStatus("Down", 75.5, ImmutableList.of(incident));
String json = gson.toJson(status);
assertThat(json)
.contains(
"""
"status":"Down"
"""
.trim());
assertThat(json)
.contains(
"""
"emergencyThreshold":75.5
"""
.trim());
assertThat(json)
.contains(
"""
"incidents":[
"""
.trim());
}
@Test
public void testTldServiceState_jsonSerialization() {
ServiceStatus dnsStatus = new ServiceStatus("Up", 0.0, ImmutableList.of());
TldServiceState state =
new TldServiceState("app", 1700000000L, "Up", ImmutableMap.of("DNS", dnsStatus));
String json = gson.toJson(state);
assertThat(json)
.contains(
"""
"tld":"app"
"""
.trim());
assertThat(json)
.contains(
"""
"status":"Up"
"""
.trim());
assertThat(json)
.contains(
"""
"testedServices":{"DNS":{
"""
.trim());
}
@Test
public void testAllServicesStateResponse_jsonSerialization() {
ServiceStateSummary summary = new ServiceStateSummary("dev", "Up", ImmutableList.of());
AllServicesStateResponse response = new AllServicesStateResponse(ImmutableList.of(summary));
String json = gson.toJson(response);
assertThat(json)
.contains(
"""
"serviceStates":[
"""
.trim());
assertThat(json)
.contains(
"""
"tld":"dev"
"""
.trim());
}
}
@@ -0,0 +1,130 @@
// 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.mosapi;
import static com.google.common.truth.Truth.assertThat;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.Mockito.when;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.util.concurrent.MoreExecutors;
import google.registry.mosapi.MosApiModels.AllServicesStateResponse;
import google.registry.mosapi.MosApiModels.IncidentSummary;
import google.registry.mosapi.MosApiModels.ServiceStateSummary;
import google.registry.mosapi.MosApiModels.ServiceStatus;
import google.registry.mosapi.MosApiModels.TldServiceState;
import java.util.concurrent.ExecutorService;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
/** Unit tests for {@link MosApiStateService}. */
@ExtendWith(MockitoExtension.class)
class MosApiStateServiceTest {
@Mock private ServiceMonitoringClient client;
private final ExecutorService executor = MoreExecutors.newDirectExecutorService();
private MosApiStateService service;
@BeforeEach
void setUp() {
service = new MosApiStateService(client, ImmutableSet.of("tld1", "tld2"), executor);
}
@Test
void testGetServiceStateSummary_upStatus_returnsEmptyIncidents() throws Exception {
TldServiceState rawState = new TldServiceState("tld1", 12345L, "Up", ImmutableMap.of());
when(client.getTldServiceState("tld1")).thenReturn(rawState);
ServiceStateSummary result = service.getServiceStateSummary("tld1");
assertThat(result.tld()).isEqualTo("tld1");
assertThat(result.overallStatus()).isEqualTo("Up");
assertThat(result.activeIncidents()).isEmpty();
}
@Test
void testGetServiceStateSummary_downStatus_filtersActiveIncidents() throws Exception {
IncidentSummary dnsIncident = new IncidentSummary("inc-1", 100L, false, "Open", null);
ServiceStatus dnsService = new ServiceStatus("Down", 50.0, ImmutableList.of(dnsIncident));
ServiceStatus rdapService = new ServiceStatus("Up", 0.0, ImmutableList.of());
TldServiceState rawState =
new TldServiceState(
"tld1", 12345L, "Down", ImmutableMap.of("DNS", dnsService, "RDAP", rdapService));
when(client.getTldServiceState("tld1")).thenReturn(rawState);
ServiceStateSummary result = service.getServiceStateSummary("tld1");
assertThat(result.overallStatus()).isEqualTo("Down");
assertThat(result.activeIncidents()).hasSize(1);
ServiceStatus incidentSummary = result.activeIncidents().get(0);
assertThat(incidentSummary.status()).isEqualTo("DNS");
assertThat(incidentSummary.incidents()).containsExactly(dnsIncident);
}
@Test
void testGetServiceStateSummary_throwsException_whenClientFails() throws Exception {
when(client.getTldServiceState("tld1")).thenThrow(new MosApiException("Network error", null));
assertThrows(MosApiException.class, () -> service.getServiceStateSummary("tld1"));
}
@Test
void testGetAllServiceStateSummaries_success() throws Exception {
TldServiceState state1 = new TldServiceState("tld1", 1L, "Up", ImmutableMap.of());
TldServiceState state2 = new TldServiceState("tld2", 2L, "Up", ImmutableMap.of());
when(client.getTldServiceState("tld1")).thenReturn(state1);
when(client.getTldServiceState("tld2")).thenReturn(state2);
AllServicesStateResponse response = service.getAllServiceStateSummaries();
assertThat(response.serviceStates()).hasSize(2);
assertThat(response.serviceStates().stream().map(ServiceStateSummary::tld))
.containsExactly("tld1", "tld2");
}
@Test
void testGetAllServiceStateSummaries_partialFailure_returnsErrorState() throws Exception {
TldServiceState state1 = new TldServiceState("tld1", 1L, "Up", ImmutableMap.of());
when(client.getTldServiceState("tld1")).thenReturn(state1);
when(client.getTldServiceState("tld2")).thenThrow(new MosApiException("Failure", null));
AllServicesStateResponse response = service.getAllServiceStateSummaries();
assertThat(response.serviceStates()).hasSize(2);
ServiceStateSummary summary1 =
response.serviceStates().stream().filter(s -> s.tld().equals("tld1")).findFirst().get();
assertThat(summary1.overallStatus()).isEqualTo("Up");
ServiceStateSummary summary2 =
response.serviceStates().stream().filter(s -> s.tld().equals("tld2")).findFirst().get();
assertThat(summary2.overallStatus()).isEqualTo("ERROR");
assertThat(summary2.activeIncidents()).isEmpty();
}
}
@@ -0,0 +1,140 @@
// 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.mosapi;
import static com.google.common.truth.Truth.assertThat;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.ArgumentMatchers.anyMap;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import com.google.gson.Gson;
import google.registry.mosapi.MosApiModels.TldServiceState;
import google.registry.tools.GsonUtils;
import okhttp3.MediaType;
import okhttp3.Protocol;
import okhttp3.Request;
import okhttp3.Response;
import okhttp3.ResponseBody;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
public class ServiceMonitoringClientTest {
private static final String TLD = "example";
private static final String ENDPOINT = "v2/monitoring/state";
private final MosApiClient mosApiClient = mock(MosApiClient.class);
private final Gson gson = GsonUtils.provideGson();
private ServiceMonitoringClient client;
@BeforeEach
void beforeEach() {
client = new ServiceMonitoringClient(mosApiClient, gson);
}
@Test
void testGetTldServiceState_success() throws Exception {
String jsonResponse =
"""
{
"tld": "example",
"services": [
{
"service": "DNS",
"status": "OPERATIONAL"
}
]
}
""";
try (Response response = createMockResponse(200, jsonResponse)) {
when(mosApiClient.sendGetRequest(eq(TLD), eq(ENDPOINT), anyMap(), anyMap()))
.thenReturn(response);
TldServiceState result = client.getTldServiceState(TLD);
assertThat(gson.toJson(result)).contains("example");
}
}
@Test
void testGetTldServiceState_apiError_throwsMosApiException() throws Exception {
String errorJson =
"""
{
"resultCode": "2011",
"message": "Invalid duration"
}
""";
try (Response response = createMockResponse(400, errorJson)) {
when(mosApiClient.sendGetRequest(eq(TLD), eq(ENDPOINT), anyMap(), anyMap()))
.thenReturn(response);
MosApiException thrown =
assertThrows(MosApiException.class, () -> client.getTldServiceState(TLD));
assertThat(thrown.getMessage()).contains("2011");
assertThat(thrown.getMessage()).contains("Invalid duration");
}
}
@Test
void testGetTldServiceState_nonJsonError_throwsMosApiException() throws Exception {
String htmlError =
"""
<html>
<body>502 Bad Gateway</body>
</html>
""";
try (Response response = createMockResponse(502, htmlError)) {
when(mosApiClient.sendGetRequest(eq(TLD), eq(ENDPOINT), anyMap(), anyMap()))
.thenReturn(response);
MosApiException thrown =
assertThrows(MosApiException.class, () -> client.getTldServiceState(TLD));
assertThat(thrown.getMessage()).contains("MoSAPI json parsing error (502)");
assertThat(thrown.getMessage()).contains("502 Bad Gateway");
}
}
@Test
void testGetTldServiceState_emptyBody_throwsMosApiException() throws Exception {
Response response =
new Response.Builder()
.request(new Request.Builder().url("http://localhost").build())
.protocol(Protocol.HTTP_1_1)
.code(204)
.message("No Content")
.build();
when(mosApiClient.sendGetRequest(eq(TLD), eq(ENDPOINT), anyMap(), anyMap()))
.thenReturn(response);
MosApiException thrown =
assertThrows(MosApiException.class, () -> client.getTldServiceState(TLD));
assertThat(thrown.getMessage()).contains("returned an empty body");
}
private Response createMockResponse(int code, String body) {
return new Response.Builder()
.request(new Request.Builder().url("http://localhost").build())
.protocol(Protocol.HTTP_1_1)
.code(code)
.message(code == 200 ? "OK" : "Error")
.body(ResponseBody.create(body, MediaType.parse("application/json")))
.build();
}
}
@@ -0,0 +1,57 @@
// 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.mosapi.module;
import static com.google.common.truth.Truth.assertThat;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import jakarta.servlet.http.HttpServletRequest;
import java.util.Optional;
import org.junit.jupiter.api.Test;
/** Unit tests for {@link MosApiRequestModule}. */
public class MosApiRequestModuleTest {
@Test
void testProvideTld_paramPresent() {
HttpServletRequest req = mock(HttpServletRequest.class);
when(req.getParameter("tld")).thenReturn("example.tld");
Optional<String> result = MosApiRequestModule.provideTld(req);
assertThat(result).hasValue("example.tld");
}
@Test
void testProvideTld_paramMissing() {
HttpServletRequest req = mock(HttpServletRequest.class);
when(req.getParameter("tld")).thenReturn(null);
Optional<String> result = MosApiRequestModule.provideTld(req);
assertThat(result).isEmpty();
}
@Test
void testProvideTld_paramEmptyString() {
HttpServletRequest req = mock(HttpServletRequest.class);
when(req.getParameter("tld")).thenReturn("");
Optional<String> result = MosApiRequestModule.provideTld(req);
assertThat(result).isEmpty();
}
}
@@ -246,7 +246,7 @@ class RdapDomainActionTest extends RdapActionBaseTestCase<RdapDomainAction> {
.isEqualTo(
addDomainBoilerplateNotices(
jsonFileBuilder()
.addDomain("cat.1.tld", "D-1_TLD")
.addDomain("cat.1.tld", "D-1TLD")
.addNameserver("ns1.cat.lol", "2-ROID")
.addNameserver("ns2.cat.lol", "4-ROID")
.addRegistrar("Multilevel Registrar")
@@ -732,7 +732,7 @@ class RdapDomainSearchActionTest extends RdapSearchActionTestCase<RdapDomainSear
RequestType.NAME,
"cat.1.test",
jsonFileBuilder()
.addDomain("cat.1.test", "1B-1_TEST")
.addDomain("cat.1.test", "1B-1TEST")
.addRegistrar("1.test")
.addNameserver("ns1.cat.1.test", "17-ROID")
.addNameserver("ns2.cat.2.test", "19-ROID")
@@ -746,7 +746,7 @@ class RdapDomainSearchActionTest extends RdapSearchActionTestCase<RdapDomainSear
RequestType.NAME,
"ca*.1.test",
jsonFileBuilder()
.addDomain("cat.1.test", "1B-1_TEST")
.addDomain("cat.1.test", "1B-1TEST")
.addRegistrar("1.test")
.addNameserver("ns1.cat.1.test", "17-ROID")
.addNameserver("ns2.cat.2.test", "19-ROID")
@@ -822,7 +822,7 @@ class RdapDomainSearchActionTest extends RdapSearchActionTestCase<RdapDomainSear
.that(generateActualJson(RequestType.NAME, "cat.*"))
.isEqualTo(
jsonFileBuilder()
.addDomain("cat.1.test", "1B-1_TEST")
.addDomain("cat.1.test", "1B-1TEST")
.addDomain("cat.example", "F-EXAMPLE")
.addDomain("cat.lol", "6-LOL")
.addDomain("cat.みんな", "15-Q9JYB4C")
@@ -860,7 +860,7 @@ class RdapDomainSearchActionTest extends RdapSearchActionTestCase<RdapDomainSear
.that(generateActualJson(RequestType.NAME, "cat*"))
.isEqualTo(
jsonFileBuilder()
.addDomain("cat.1.test", "1B-1_TEST")
.addDomain("cat.1.test", "1B-1TEST")
.addDomain("cat.example", "F-EXAMPLE")
.addDomain("cat.lol", "6-LOL")
.addDomain("cat.みんな", "15-Q9JYB4C")
@@ -1284,7 +1284,7 @@ class RdapDomainSearchActionTest extends RdapSearchActionTestCase<RdapDomainSear
RequestType.NS_LDH_NAME,
"ns1.cat.1.test",
jsonFileBuilder()
.addDomain("cat.1.test", "1B-1_TEST")
.addDomain("cat.1.test", "1B-1TEST")
.addRegistrar("1.test")
.addNameserver("ns1.cat.1.test", "17-ROID")
.addNameserver("ns2.cat.2.test", "19-ROID")
@@ -1298,7 +1298,7 @@ class RdapDomainSearchActionTest extends RdapSearchActionTestCase<RdapDomainSear
RequestType.NS_LDH_NAME,
"ns*.cat.1.test",
jsonFileBuilder()
.addDomain("cat.1.test", "1B-1_TEST")
.addDomain("cat.1.test", "1B-1TEST")
.addRegistrar("1.test")
.addNameserver("ns1.cat.1.test", "17-ROID")
.addNameserver("ns2.cat.2.test", "19-ROID")
@@ -74,7 +74,9 @@ class RdapNameserverActionTest extends RdapActionBaseTestCase<RdapNameserverActi
.that(generateActualJson("invalid/host/name"))
.isEqualTo(
generateExpectedJsonError(
"invalid/host/name is not a valid nameserver: Invalid host name", 400));
"invalid/host/name is not a valid nameserver: Host names can only contain a-z, 0-9,"
+ " '.', '_', and '-'",
400));
assertThat(response.getStatus()).isEqualTo(400);
}
@@ -420,8 +420,7 @@ public final class DatabaseHelper {
public static Tld createTld(String tld, ImmutableSortedMap<DateTime, TldState> tldStates) {
// Coerce the TLD string into a valid ROID suffix.
String roidSuffix =
Ascii.toUpperCase(tld.replaceFirst(ACE_PREFIX_REGEX, "").replace('.', '_'))
.replace('-', '_');
Ascii.toUpperCase(tld.replaceFirst(ACE_PREFIX_REGEX, "").replace(".", "")).replace("-", "");
return createTld(
tld, roidSuffix.length() > 8 ? roidSuffix.substring(0, 8) : roidSuffix, tldStates);
}
@@ -0,0 +1,161 @@
// 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.tmch;
import static com.google.common.truth.Truth.assertThat;
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
import static google.registry.util.RegistryEnvironment.PRODUCTION;
import static google.registry.util.RegistryEnvironment.SANDBOX;
import static org.joda.time.DateTime.now;
import static org.joda.time.DateTimeZone.UTC;
import com.google.common.base.Splitter;
import google.registry.model.smd.SignedMarkRevocationList;
import google.registry.model.smd.SignedMarkRevocationListDao;
import google.registry.model.tmch.ClaimsListDao;
import google.registry.persistence.transaction.JpaTestExtensions;
import google.registry.testing.FakeClock;
import google.registry.util.RegistryEnvironment;
import java.util.stream.Stream;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.extension.RegisterExtension;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
public class RstTmchUtilsIntTest {
private final FakeClock clock = new FakeClock();
@RegisterExtension
final JpaTestExtensions.JpaIntegrationTestExtension jpa =
new JpaTestExtensions.Builder().withClock(clock).buildIntegrationTestExtension();
private static final String TMCH_CLAIM_LABEL = "tmch";
// RST label found in *.rst.dnl.csv resources. Currently both files are identical
private static final String RST_CLAIM_LABEL = "test--validate";
private static final String TMCH_SMD_ID = "tmch";
// RST label found in *.rst.smdrl.csv resources. Currently both files are identical
private static final String RST_SMD_ID = "0000001761385117375880-65535";
private static final String TMCH_DNL =
"""
1,2024-09-13T02:21:12.0Z
DNL,lookup-key,insertion-datetime
LABEL,2024091300/6/a/b/arJyPPf2CK7f21bVGne0qMgW0000000001,2024-09-13T02:21:12.0Z
"""
.replace("LABEL", TMCH_CLAIM_LABEL);
private static final String TMCH_SMDRL =
"""
1,2022-11-22T01:49:36.9Z
smd-id,insertion-datetime
ID,2013-07-15T00:00:00.0Z
"""
.replace("ID", TMCH_SMD_ID);
@BeforeEach
void setup() throws Exception {
Splitter lineSplitter = Splitter.on("\n").omitEmptyStrings().trimResults();
tm().transact(
() -> ClaimsListDao.save(ClaimsListParser.parse(lineSplitter.splitToList(TMCH_DNL))));
tm().transact(
() ->
SignedMarkRevocationListDao.save(
SmdrlCsvParser.parse(lineSplitter.splitToList(TMCH_SMDRL))));
}
@ParameterizedTest
@MethodSource("provideTestCases")
@SuppressWarnings("unused") // testCaseName
void getClaimsList_production(String testCaseName, String tld) {
var currEnv = RegistryEnvironment.get();
try {
PRODUCTION.setup();
var claimsList = ClaimsListDao.get(tld);
assertThat(claimsList.getClaimKey(TMCH_CLAIM_LABEL)).isPresent();
assertThat(claimsList.getClaimKey(RST_CLAIM_LABEL)).isEmpty();
} finally {
currEnv.setup();
}
}
@ParameterizedTest
@MethodSource("provideTestCases")
@SuppressWarnings("unused") // testCaseName
void getSmdrList_production(String testCaseName, String tld) {
var currEnv = RegistryEnvironment.get();
try {
PRODUCTION.setup();
var smdrl = SignedMarkRevocationList.get(tld);
assertThat(smdrl.isSmdRevoked(TMCH_SMD_ID, now(UTC))).isTrue();
assertThat(smdrl.isSmdRevoked(RST_SMD_ID, now(UTC))).isFalse();
assertThat(smdrl.size()).isEqualTo(1);
} finally {
currEnv.setup();
}
}
@ParameterizedTest
@MethodSource("provideTestCases")
@SuppressWarnings("unused") // testCaseName
void getClaimsList_sandbox(String testCaseName, String tld) {
var currEnv = RegistryEnvironment.get();
try {
SANDBOX.setup();
var claimsList = ClaimsListDao.get(tld);
if (tld.equals("app")) {
assertThat(claimsList.getClaimKey(TMCH_CLAIM_LABEL)).isPresent();
assertThat(claimsList.getClaimKey(RST_CLAIM_LABEL)).isEmpty();
} else {
assertThat(claimsList.getClaimKey(TMCH_CLAIM_LABEL)).isEmpty();
// Currently ote and prod have the same data.
assertThat(claimsList.getClaimKey(RST_CLAIM_LABEL)).isPresent();
}
} finally {
currEnv.setup();
}
}
@ParameterizedTest
@MethodSource("provideTestCases")
@SuppressWarnings("unused") // testCaseName
void getSmdrList_sandbox(String testCaseName, String tld) {
var currEnv = RegistryEnvironment.get();
try {
SANDBOX.setup();
var smdrList = SignedMarkRevocationList.get(tld);
if (tld.equals("app")) {
assertThat(smdrList.size()).isEqualTo(1);
assertThat(smdrList.isSmdRevoked(TMCH_SMD_ID, now(UTC))).isTrue();
assertThat(smdrList.isSmdRevoked(RST_SMD_ID, now(UTC))).isFalse();
} else {
// Currently ote and prod have the same data.
assertThat(smdrList.size()).isEqualTo(5);
assertThat(smdrList.isSmdRevoked(TMCH_SMD_ID, now())).isFalse();
assertThat(smdrList.isSmdRevoked(RST_SMD_ID, now())).isTrue();
}
} finally {
currEnv.setup();
}
}
private static Stream<Arguments> provideTestCases() {
return Stream.of(
Arguments.of("NotRST", "app"),
Arguments.of("OTE", "cc-rst-test-tld-1"),
Arguments.of("PROD", "zz--idn-123"));
}
}
@@ -0,0 +1,117 @@
// 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.tmch;
import static com.google.common.truth.Truth.assertThat;
import static google.registry.tmch.RstTmchUtils.getClaimsList;
import static google.registry.tmch.RstTmchUtils.getSmdrList;
import static google.registry.util.RegistryEnvironment.PRODUCTION;
import static google.registry.util.RegistryEnvironment.SANDBOX;
import google.registry.util.RegistryEnvironment;
import java.util.stream.Stream;
import org.joda.time.DateTime;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
public class RstTmchUtilsTest {
@ParameterizedTest
@MethodSource("provideTestCases")
@SuppressWarnings("unused") // testCaseName
void getClaimsList_production(String testCaseName, String tld) {
var currEnv = RegistryEnvironment.get();
try {
PRODUCTION.setup();
assertThat(getClaimsList(tld)).isEmpty();
} finally {
currEnv.setup();
}
}
@ParameterizedTest
@MethodSource("provideTestCases")
@SuppressWarnings("unused") // testCaseName
void getSmdrList_production(String testCaseName, String tld) {
var currEnv = RegistryEnvironment.get();
try {
PRODUCTION.setup();
assertThat(getSmdrList(tld)).isEmpty();
} finally {
currEnv.setup();
}
}
@ParameterizedTest
@MethodSource("provideTestCases")
@SuppressWarnings("unused") // testCaseName
void getClaimsList_sandbox(String testCaseName, String tld) {
var currEnv = RegistryEnvironment.get();
try {
SANDBOX.setup();
var claimsListOptional = getClaimsList(tld);
if (tld.equals("app")) {
assertThat(claimsListOptional).isEmpty();
} else {
// Currently ote and prod have the same data.
var claimsList = claimsListOptional.get();
assertThat(claimsList.getClaimKey("test-and-validate")).isPresent();
var labelsToKeys = claimsList.getLabelsToKeys();
assertThat(labelsToKeys).hasSize(8);
assertThat(labelsToKeys)
.containsEntry(
"test---validate", "2024091300/6/a/b/arJyPPf2CK7f21bVGne0qMgW0000000001");
}
} finally {
currEnv.setup();
}
}
@ParameterizedTest
@MethodSource("provideTestCases")
@SuppressWarnings("unused") // testCaseName
void getSmdrList_sandbox(String testCaseName, String tld) {
var currEnv = RegistryEnvironment.get();
try {
SANDBOX.setup();
var smdrListOptional = getSmdrList(tld);
if (tld.equals("app")) {
assertThat(smdrListOptional).isEmpty();
} else {
// Currently ote and prod have the same data.
var smdrList = smdrListOptional.get();
assertThat(smdrList.size()).isEqualTo(5);
assertThat(
smdrList.isSmdRevoked(
"000000541526299609231-65535", DateTime.parse("2018-05-14T17:52:23.6Z")))
.isFalse();
assertThat(
smdrList.isSmdRevoked(
"000000541526299609231-65535", DateTime.parse("2018-05-14T17:52:23.7Z")))
.isTrue();
}
} finally {
currEnv.setup();
}
}
private static Stream<Arguments> provideTestCases() {
return Stream.of(
Arguments.of("NotRST", "app"),
Arguments.of("OTE", "cc-rst-test-tld-1"),
Arguments.of("PROD", "zz--idn-123"));
}
}
@@ -57,7 +57,7 @@ class TmchTestDataExpirationTest {
String tmchData = loadFile(TmchTestDataExpirationTest.class, filePath);
EncodedSignedMark smd = TmchData.readEncodedSignedMark(tmchData);
try {
tmchUtils.verifyEncodedSignedMark(smd, DateTime.now(UTC));
tmchUtils.verifyEncodedSignedMark("", smd, DateTime.now(UTC));
} catch (EppException e) {
throw new AssertionError("Error verifying signed mark " + filePath, e);
}
@@ -368,7 +368,7 @@ public class ConfigureTldCommandTest extends CommandTestCase<ConfigureTldCommand
"TLDSTR", name, "TLDUNICODE", name, "ROIDSUFFIX", "TLLLLLLLLLLLLLLLLLLLLLLD")));
IllegalArgumentException thrown =
assertThrows(IllegalArgumentException.class, () -> runCommandForced("--input=" + tldFile));
assertThat(thrown.getMessage()).isEqualTo("ROID suffix must be in format ^[A-Z\\d_]{1,8}$");
assertThat(thrown.getMessage()).isEqualTo("ROID suffix must be in format ^[A-Z\\d]{1,8}$");
}
@Test
@@ -124,6 +124,24 @@ class UpdateRegistrarCommandTest extends CommandTestCase<UpdateRegistrarCommand>
.containsExactly("xn--q9jyb4c", "foobar");
}
@Test
void testSuccess_allowedTlds_tldNameWithHyphens() throws Exception {
persistRdapAbuseContact();
createTlds("zz--main-1611", "foobar");
persistResource(
loadRegistrar("NewRegistrar")
.asBuilder()
.setAllowedTlds(ImmutableSet.of("foobar"))
.build());
runCommandInEnvironment(
RegistryToolEnvironment.PRODUCTION,
"--allowed_tlds=zz--main-1611,foobar",
"--force",
"NewRegistrar");
assertThat(loadRegistrar("NewRegistrar").getAllowedTlds())
.containsExactly("zz--main-1611", "foobar");
}
@Test
void testSuccess_addAllowedTlds() throws Exception {
persistRdapAbuseContact();
@@ -142,6 +160,19 @@ class UpdateRegistrarCommandTest extends CommandTestCase<UpdateRegistrarCommand>
.containsExactly("xn--q9jyb4c", "foo", "bar");
}
@Test
void testSuccess_addAllowedTlds_tldNameWithHyphens() throws Exception {
persistRdapAbuseContact();
createTlds("foo", "bar", "zz--main-1611");
runCommandInEnvironment(
RegistryToolEnvironment.PRODUCTION,
"--add_allowed_tlds=foo,bar",
"--force",
"NewRegistrar");
assertThat(loadRegistrar("NewRegistrar").getAllowedTlds())
.containsExactly("foo", "bar", "zz--main-1611");
}
@Test
void testSuccess_addAllowedTldsWithDupes() throws Exception {
persistRdapAbuseContact();
@@ -26,6 +26,11 @@
<rdeIDN:urlPolicy>https://www.registry.google/about/policies/domainabuse/</rdeIDN:urlPolicy>
</rdeIDN:idnTableRef>
<rdeIDN:idnTableRef id="augmented_latin">
<rdeIDN:url>https://www.iana.org/domains/idn-tables/tables/google_latn_3.0.txt</rdeIDN:url>
<rdeIDN:urlPolicy>https://www.registry.google/about/policies/domainabuse/</rdeIDN:urlPolicy>
</rdeIDN:idnTableRef>
<rdeIDN:idnTableRef id="ja">
<rdeIDN:url>https://www.iana.org/domains/idn-tables/tables/google_ja_1.0.txt</rdeIDN:url>
<rdeIDN:urlPolicy>https://www.registry.google/about/policies/domainabuse/</rdeIDN:urlPolicy>
@@ -37,7 +42,7 @@
<rdeHeader:count uri="urn:ietf:params:xml:ns:rdeDomain-1.0">1</rdeHeader:count>
<rdeHeader:count uri="urn:ietf:params:xml:ns:rdeHost-1.0">1</rdeHeader:count>
<rdeHeader:count uri="urn:ietf:params:xml:ns:rdeRegistrar-1.0">1</rdeHeader:count>
<rdeHeader:count uri="urn:ietf:params:xml:ns:rdeIDN-1.0">3</rdeHeader:count>
<rdeHeader:count uri="urn:ietf:params:xml:ns:rdeIDN-1.0">4</rdeHeader:count>
</rdeHeader:header>
</rde:contents>
@@ -14,6 +14,6 @@
<rdeHeader:count uri="urn:ietf:params:xml:ns:rdeDomain-1.0">1</rdeHeader:count>
<rdeHeader:count uri="urn:ietf:params:xml:ns:rdeHost-1.0">1</rdeHeader:count>
<rdeHeader:count uri="urn:ietf:params:xml:ns:rdeRegistrar-1.0">1</rdeHeader:count>
<rdeHeader:count uri="urn:ietf:params:xml:ns:rdeIDN-1.0">3</rdeHeader:count>
<rdeHeader:count uri="urn:ietf:params:xml:ns:rdeIDN-1.0">4</rdeHeader:count>
</rdeHeader:header>
</rdeReport:report>
@@ -1,33 +0,0 @@
<epp xmlns="urn:ietf:params:xml:ns:epp-1.0">
<command>
<create>
<contact:create
xmlns:contact="urn:ietf:params:xml:ns:contact-1.0">
<contact:id>jd1234</contact:id>
<contact:postalInfo type="int">
<contact:name>John Doe</contact:name>
<contact:org>Example Inc.</contact:org>
<contact:addr>
<contact:street>123 Example Dr.</contact:street>
<contact:street>Suite 100</contact:street>
<contact:city>Dulles</contact:city>
<contact:sp>VA</contact:sp>
<contact:pc>20166-6503</contact:pc>
<contact:cc>US</contact:cc>
</contact:addr>
</contact:postalInfo>
<contact:voice x="1234">+1.7035555555</contact:voice>
<contact:fax>+1.7035555556</contact:fax>
<contact:email>jdoe@example.com</contact:email>
<contact:authInfo>
<contact:pw>2fooBAR</contact:pw>
</contact:authInfo>
<contact:disclose flag="1">
<contact:voice/>
<contact:email/>
</contact:disclose>
</contact:create>
</create>
<clTRID>ABC-12345</clTRID>
</command>
</epp>
@@ -1,18 +0,0 @@
<epp xmlns="urn:ietf:params:xml:ns:epp-1.0">
<response>
<result code="1000">
<msg>Command completed successfully</msg>
</result>
<resData>
<contact:creData
xmlns:contact="urn:ietf:params:xml:ns:contact-1.0">
<contact:id>jd1234</contact:id>
<contact:crDate>2000-06-01T00:01:00.0Z</contact:crDate>
</contact:creData>
</resData>
<trID>
<clTRID>ABC-12345</clTRID>
<svTRID>server-trid</svTRID>
</trID>
</response>
</epp>
@@ -1,18 +0,0 @@
<epp xmlns="urn:ietf:params:xml:ns:epp-1.0">
<response>
<result code="1000">
<msg>Command completed successfully</msg>
</result>
<resData>
<contact:creData
xmlns:contact="urn:ietf:params:xml:ns:contact-1.0">
<contact:id>sh8013</contact:id>
<contact:crDate>%CRDATE%</contact:crDate>
</contact:creData>
</resData>
<trID>
<clTRID>ABC-12345</clTRID>
<svTRID>server-trid</svTRID>
</trID>
</response>
</epp>
@@ -1,33 +0,0 @@
<epp xmlns="urn:ietf:params:xml:ns:epp-1.0">
<command>
<create>
<contact:create
xmlns:contact="urn:ietf:params:xml:ns:contact-1.0">
<contact:id>sh8013</contact:id>
<contact:postalInfo type="int">
<contact:name>John Doe</contact:name>
<contact:org>Example Inc.</contact:org>
<contact:addr>
<contact:street>123 Example Dr.</contact:street>
<contact:street>Suite 100</contact:street>
<contact:city>Dulles</contact:city>
<contact:sp>VA</contact:sp>
<contact:pc>20166-6503</contact:pc>
<contact:cc>US</contact:cc>
</contact:addr>
</contact:postalInfo>
<contact:voice x="1234">+1.7035555555</contact:voice>
<contact:fax>+1.7035555556</contact:fax>
<contact:email>jdoe@example.com</contact:email>
<contact:authInfo>
<contact:pw>2fooBAR</contact:pw>
</contact:authInfo>
<contact:disclose flag="1">
<contact:voice/>
<contact:email/>
</contact:disclose>
</contact:create>
</create>
<clTRID>ABC-12345</clTRID>
</command>
</epp>
@@ -1,11 +0,0 @@
<epp xmlns="urn:ietf:params:xml:ns:epp-1.0">
<response>
<result code="1000">
<msg>Command completed successfully</msg>
</result>
<trID>
<clTRID>ABC-12345</clTRID>
<svTRID>server-trid</svTRID>
</trID>
</response>
</epp>
@@ -1,11 +0,0 @@
<epp xmlns="urn:ietf:params:xml:ns:epp-1.0">
<command>
<delete>
<contact:delete
xmlns:contact="urn:ietf:params:xml:ns:contact-1.0">
<contact:id>sh8013</contact:id>
</contact:delete>
</delete>
<clTRID>ABC-12345</clTRID>
</command>
</epp>
@@ -1,14 +0,0 @@
<epp xmlns="urn:ietf:params:xml:ns:epp-1.0">
<command>
<transfer op="request">
<contact:transfer
xmlns:contact="urn:ietf:params:xml:ns:contact-1.0">
<contact:id>sh8013</contact:id>
<contact:authInfo>
<contact:pw>2fooBAR</contact:pw>
</contact:authInfo>
</contact:transfer>
</transfer>
<clTRID>ABC-12345</clTRID>
</command>
</epp>
@@ -1,22 +0,0 @@
<epp xmlns="urn:ietf:params:xml:ns:epp-1.0">
<response>
<result code="1001">
<msg>Command completed successfully; action pending</msg>
</result>
<resData>
<contact:trnData
xmlns:contact="urn:ietf:params:xml:ns:contact-1.0">
<contact:id>sh8013</contact:id>
<contact:trStatus>pending</contact:trStatus>
<contact:reID>TheRegistrar</contact:reID>
<contact:reDate>2000-06-08T22:00:00.0Z</contact:reDate>
<contact:acID>NewRegistrar</contact:acID>
<contact:acDate>2000-06-13T22:00:00.0Z</contact:acDate>
</contact:trnData>
</resData>
<trID>
<clTRID>ABC-12345</clTRID>
<svTRID>server-trid</svTRID>
</trID>
</response>
</epp>
@@ -3,56 +3,56 @@
<check>
<host:check
xmlns:host="urn:ietf:params:xml:ns:host-1.0">
<host:name>www1.tld</host:name>
<host:name>www2.tld</host:name>
<host:name>www3.tld</host:name>
<host:name>www4.tld</host:name>
<host:name>www5.tld</host:name>
<host:name>www6.tld</host:name>
<host:name>www7.tld</host:name>
<host:name>www8.tld</host:name>
<host:name>www9.tld</host:name>
<host:name>www10.tld</host:name>
<host:name>www11.tld</host:name>
<host:name>www12.tld</host:name>
<host:name>www13.tld</host:name>
<host:name>www14.tld</host:name>
<host:name>www15.tld</host:name>
<host:name>www16.tld</host:name>
<host:name>www17.tld</host:name>
<host:name>www18.tld</host:name>
<host:name>www19.tld</host:name>
<host:name>www20.tld</host:name>
<host:name>www21.tld</host:name>
<host:name>www22.tld</host:name>
<host:name>www23.tld</host:name>
<host:name>www24.tld</host:name>
<host:name>www25.tld</host:name>
<host:name>www26.tld</host:name>
<host:name>www27.tld</host:name>
<host:name>www28.tld</host:name>
<host:name>www29.tld</host:name>
<host:name>www30.tld</host:name>
<host:name>www31.tld</host:name>
<host:name>www32.tld</host:name>
<host:name>www33.tld</host:name>
<host:name>www34.tld</host:name>
<host:name>www35.tld</host:name>
<host:name>www36.tld</host:name>
<host:name>www37.tld</host:name>
<host:name>www38.tld</host:name>
<host:name>www39.tld</host:name>
<host:name>www40.tld</host:name>
<host:name>www41.tld</host:name>
<host:name>www42.tld</host:name>
<host:name>www43.tld</host:name>
<host:name>www44.tld</host:name>
<host:name>www45.tld</host:name>
<host:name>www46.tld</host:name>
<host:name>www47.tld</host:name>
<host:name>www48.tld</host:name>
<host:name>www49.tld</host:name>
<host:name>www50.tld</host:name>
<host:name>ns1.www1.tld</host:name>
<host:name>ns1.www2.tld</host:name>
<host:name>ns1.www3.tld</host:name>
<host:name>ns1.www4.tld</host:name>
<host:name>ns1.www5.tld</host:name>
<host:name>ns1.www6.tld</host:name>
<host:name>ns1.www7.tld</host:name>
<host:name>ns1.www8.tld</host:name>
<host:name>ns1.www9.tld</host:name>
<host:name>ns1.www10.tld</host:name>
<host:name>ns1.www11.tld</host:name>
<host:name>ns1.www12.tld</host:name>
<host:name>ns1.www13.tld</host:name>
<host:name>ns1.www14.tld</host:name>
<host:name>ns1.www15.tld</host:name>
<host:name>ns1.www16.tld</host:name>
<host:name>ns1.www17.tld</host:name>
<host:name>ns1.www18.tld</host:name>
<host:name>ns1.www19.tld</host:name>
<host:name>ns1.www20.tld</host:name>
<host:name>ns1.www21.tld</host:name>
<host:name>ns1.www22.tld</host:name>
<host:name>ns1.www23.tld</host:name>
<host:name>ns1.www24.tld</host:name>
<host:name>ns1.www25.tld</host:name>
<host:name>ns1.www26.tld</host:name>
<host:name>ns1.www27.tld</host:name>
<host:name>ns1.www28.tld</host:name>
<host:name>ns1.www29.tld</host:name>
<host:name>ns1.www30.tld</host:name>
<host:name>ns1.www31.tld</host:name>
<host:name>ns1.www32.tld</host:name>
<host:name>ns1.www33.tld</host:name>
<host:name>ns1.www34.tld</host:name>
<host:name>ns1.www35.tld</host:name>
<host:name>ns1.www36.tld</host:name>
<host:name>ns1.www37.tld</host:name>
<host:name>ns1.www38.tld</host:name>
<host:name>ns1.www39.tld</host:name>
<host:name>ns1.www40.tld</host:name>
<host:name>ns1.www41.tld</host:name>
<host:name>ns1.www42.tld</host:name>
<host:name>ns1.www43.tld</host:name>
<host:name>ns1.www44.tld</host:name>
<host:name>ns1.www45.tld</host:name>
<host:name>ns1.www46.tld</host:name>
<host:name>ns1.www47.tld</host:name>
<host:name>ns1.www48.tld</host:name>
<host:name>ns1.www49.tld</host:name>
<host:name>ns1.www50.tld</host:name>
</host:check>
</check>
<clTRID>ABC-12345</clTRID>
@@ -0,0 +1,11 @@
<epp xmlns="urn:ietf:params:xml:ns:epp-1.0">
<command>
<check>
<host:check
xmlns:host="urn:ietf:params:xml:ns:host-1.0">
<host:name>%HOSTNAME%</host:name>
</host:check>
</check>
<clTRID>ABC-12345</clTRID>
</command>
</epp>
@@ -20,7 +20,7 @@
</resData>
<trID>
<clTRID>ABC-12345</clTRID>
<svTRID>BYiJH5vHRRW6EuYXo/Kdsg==-23</svTRID>
<svTRID>server-trid</svTRID>
</trID>
</response>
</epp>
@@ -13,6 +13,7 @@ BACKEND /_dr/admin/verifyOte VerifyOteAction
BACKEND /_dr/cron/fanout TldFanoutAction GET y APP ADMIN
BACKEND /_dr/epptool EppToolAction POST n APP ADMIN
BACKEND /_dr/loadtest LoadTestAction POST y APP ADMIN
BACKEND /_dr/mosapi/getServiceState GetServiceStateAction GET n APP ADMIN
BACKEND /_dr/task/brdaCopy BrdaCopyAction POST y APP ADMIN
BACKEND /_dr/task/bsaDownload BsaDownloadAction GET,POST n APP ADMIN
BACKEND /_dr/task/bsaRefresh BsaRefreshAction GET,POST n APP ADMIN
@@ -261,7 +261,7 @@
<rdeHeader:count uri="urn:ietf:params:xml:ns:rdeDomain-1.0">1</rdeHeader:count>
<rdeHeader:count uri="urn:ietf:params:xml:ns:rdeHost-1.0">2</rdeHeader:count>
<rdeHeader:count uri="urn:ietf:params:xml:ns:rdeRegistrar-1.0">2</rdeHeader:count>
<rdeHeader:count uri="urn:ietf:params:xml:ns:rdeIDN-1.0">3</rdeHeader:count>
<rdeHeader:count uri="urn:ietf:params:xml:ns:rdeIDN-1.0">4</rdeHeader:count>
</rdeHeader:header>
</rde:contents>
@@ -37,6 +37,6 @@
<rdeHeader:count uri="urn:ietf:params:xml:ns:rdeDomain-1.0">1</rdeHeader:count>
<rdeHeader:count uri="urn:ietf:params:xml:ns:rdeHost-1.0">2</rdeHeader:count>
<rdeHeader:count uri="urn:ietf:params:xml:ns:rdeRegistrar-1.0">2</rdeHeader:count>
<rdeHeader:count uri="urn:ietf:params:xml:ns:rdeIDN-1.0">3</rdeHeader:count>
<rdeHeader:count uri="urn:ietf:params:xml:ns:rdeIDN-1.0">4</rdeHeader:count>
</rdeHeader:header>
</rdeReport:report>

Some files were not shown because too many files have changed in this diff Show More