Flatten the domain transfer flows

-------------
Created by MOE: https://github.com/google/moe
MOE_MIGRATED_REVID=133906420
This commit is contained in:
cgoldfeder 2016-09-21 19:17:07 -07:00 committed by Ben McIlwain
parent 2d46c7c27c
commit 1b34f1e326
19 changed files with 848 additions and 351 deletions

View file

@ -32,6 +32,8 @@ import com.googlecode.objectify.Work;
import google.registry.flows.EppException.AuthorizationErrorException; import google.registry.flows.EppException.AuthorizationErrorException;
import google.registry.flows.EppException.InvalidAuthorizationInformationErrorException; import google.registry.flows.EppException.InvalidAuthorizationInformationErrorException;
import google.registry.flows.exceptions.MissingTransferRequestAuthInfoException; import google.registry.flows.exceptions.MissingTransferRequestAuthInfoException;
import google.registry.flows.exceptions.NotPendingTransferException;
import google.registry.flows.exceptions.NotTransferInitiatorException;
import google.registry.flows.exceptions.ResourceAlreadyExistsException; import google.registry.flows.exceptions.ResourceAlreadyExistsException;
import google.registry.flows.exceptions.ResourceStatusProhibitsOperationException; import google.registry.flows.exceptions.ResourceStatusProhibitsOperationException;
import google.registry.flows.exceptions.ResourceToDeleteIsReferencedException; import google.registry.flows.exceptions.ResourceToDeleteIsReferencedException;
@ -140,14 +142,14 @@ public class ResourceFlowUtils {
*/ */
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
public static <R extends EppResource> Builder<R, ? extends Builder<R, ?>> public static <R extends EppResource> Builder<R, ? extends Builder<R, ?>>
prepareDeletedResourceAsBuilder(R existingResource, DateTime now) { prepareDeletedResourceAsBuilder(R resource, DateTime now) {
Builder<R, ? extends Builder<R, ?>> builder = Builder<R, ? extends Builder<R, ?>> builder =
(Builder<R, ? extends Builder<R, ?>>) existingResource.asBuilder() (Builder<R, ? extends Builder<R, ?>>) resource.asBuilder()
.setDeletionTime(now) .setDeletionTime(now)
.setStatusValues(null) .setStatusValues(null)
.setTransferData( .setTransferData(
existingResource.getStatusValues().contains(StatusValue.PENDING_TRANSFER) resource.getStatusValues().contains(StatusValue.PENDING_TRANSFER)
? existingResource.getTransferData().asBuilder() ? resource.getTransferData().asBuilder()
.setTransferStatus(TransferStatus.SERVER_CANCELLED) .setTransferStatus(TransferStatus.SERVER_CANCELLED)
.setServerApproveEntities(null) .setServerApproveEntities(null)
.setServerApproveBillingEvent(null) .setServerApproveBillingEvent(null)
@ -155,7 +157,7 @@ public class ResourceFlowUtils {
.setServerApproveAutorenewPollMessage(null) .setServerApproveAutorenewPollMessage(null)
.setPendingTransferExpirationTime(null) .setPendingTransferExpirationTime(null)
.build() .build()
: existingResource.getTransferData()) : resource.getTransferData())
.wipeOut(); .wipeOut();
return builder; return builder;
} }
@ -169,9 +171,9 @@ public class ResourceFlowUtils {
/** If there is a transfer out, delete the server-approve entities and enqueue a poll message. */ /** If there is a transfer out, delete the server-approve entities and enqueue a poll message. */
public static <R extends EppResource> void handlePendingTransferOnDelete( public static <R extends EppResource> void handlePendingTransferOnDelete(
R existingResource, R newResource, DateTime now, HistoryEntry historyEntry) { R resource, R newResource, DateTime now, HistoryEntry historyEntry) {
if (existingResource.getStatusValues().contains(StatusValue.PENDING_TRANSFER)) { if (resource.getStatusValues().contains(StatusValue.PENDING_TRANSFER)) {
TransferData oldTransferData = existingResource.getTransferData(); TransferData oldTransferData = resource.getTransferData();
ofy().delete().keys(oldTransferData.getServerApproveEntities()); ofy().delete().keys(oldTransferData.getServerApproveEntities());
ofy().save().entity(new PollMessage.OneTime.Builder() ofy().save().entity(new PollMessage.OneTime.Builder()
.setClientId(oldTransferData.getGainingClientId()) .setClientId(oldTransferData.getGainingClientId())
@ -180,7 +182,7 @@ public class ResourceFlowUtils {
.setResponseData(ImmutableList.of( .setResponseData(ImmutableList.of(
createTransferResponse(newResource, newResource.getTransferData(), now), createTransferResponse(newResource, newResource.getTransferData(), now),
createPendingTransferNotificationResponse( createPendingTransferNotificationResponse(
existingResource, oldTransferData.getTransferRequestTrid(), false, now))) resource, oldTransferData.getTransferRequestTrid(), false, now)))
.setParent(historyEntry) .setParent(historyEntry)
.build()); .build());
} }
@ -275,22 +277,29 @@ public class ResourceFlowUtils {
return resolvePendingTransfer(resource, transferStatus, now).build(); return resolvePendingTransfer(resource, transferStatus, now).build();
} }
public static void verifyHasPendingTransfer(EppResource resource)
throws NotPendingTransferException {
if (resource.getTransferData().getTransferStatus() != TransferStatus.PENDING) {
throw new NotPendingTransferException(resource.getForeignKey());
}
}
public static <R extends EppResource> R loadResourceForQuery( public static <R extends EppResource> R loadResourceForQuery(
Class<R> clazz, String targetId, DateTime now) throws ResourceToQueryDoesNotExistException { Class<R> clazz, String targetId, DateTime now) throws ResourceToQueryDoesNotExistException {
R existingResource = loadByUniqueId(clazz, targetId, now); R resource = loadByUniqueId(clazz, targetId, now);
if (existingResource == null) { if (resource == null) {
throw new ResourceToQueryDoesNotExistException(clazz, targetId); throw new ResourceToQueryDoesNotExistException(clazz, targetId);
} }
return existingResource; return resource;
} }
public static <R extends EppResource> R loadResourceToMutate( public static <R extends EppResource> R loadResourceToMutate(
Class<R> clazz, String targetId, DateTime now) throws ResourceToMutateDoesNotExistException { Class<R> clazz, String targetId, DateTime now) throws ResourceToMutateDoesNotExistException {
R existingResource = loadByUniqueId(clazz, targetId, now); R resource = loadByUniqueId(clazz, targetId, now);
if (existingResource == null) { if (resource == null) {
throw new ResourceToMutateDoesNotExistException(clazz, targetId); throw new ResourceToMutateDoesNotExistException(clazz, targetId);
} }
return existingResource; return resource;
} }
public static <R extends EppResource> void verifyResourceDoesNotExist( public static <R extends EppResource> void verifyResourceDoesNotExist(
@ -300,6 +309,13 @@ public class ResourceFlowUtils {
} }
} }
public static void verifyIsGainingRegistrar(EppResource resource, String clientId)
throws NotTransferInitiatorException {
if (!clientId.equals(resource.getTransferData().getGainingClientId())) {
throw new NotTransferInitiatorException();
}
}
/** The specified resource belongs to another client. */ /** The specified resource belongs to another client. */
public static class ResourceNotOwnedException extends AuthorizationErrorException { public static class ResourceNotOwnedException extends AuthorizationErrorException {
public ResourceNotOwnedException() { public ResourceNotOwnedException() {
@ -317,11 +333,11 @@ public class ResourceFlowUtils {
/** Check that the given AuthInfo is present and valid for a resource being transferred. */ /** Check that the given AuthInfo is present and valid for a resource being transferred. */
public static void verifyRequiredAuthInfoForResourceTransfer( public static void verifyRequiredAuthInfoForResourceTransfer(
Optional<AuthInfo> authInfo, ContactResource existingContact) throws EppException { Optional<AuthInfo> authInfo, EppResource existingResource) throws EppException {
if (!authInfo.isPresent()) { if (!authInfo.isPresent()) {
throw new MissingTransferRequestAuthInfoException(); throw new MissingTransferRequestAuthInfoException();
} }
verifyOptionalAuthInfoForResource(authInfo, existingContact); verifyOptionalAuthInfoForResource(authInfo, existingResource);
} }
/** Check that the given AuthInfo is valid for the given resource. */ /** Check that the given AuthInfo is valid for the given resource. */

View file

@ -78,6 +78,7 @@ import google.registry.model.host.HostResource;
import google.registry.model.mark.Mark; import google.registry.model.mark.Mark;
import google.registry.model.mark.ProtectedMark; import google.registry.model.mark.ProtectedMark;
import google.registry.model.mark.Trademark; import google.registry.model.mark.Trademark;
import google.registry.model.poll.PendingActionNotificationResponse.DomainPendingActionNotificationResponse;
import google.registry.model.poll.PollMessage; import google.registry.model.poll.PollMessage;
import google.registry.model.registrar.Registrar; import google.registry.model.registrar.Registrar;
import google.registry.model.registry.Registry; import google.registry.model.registry.Registry;
@ -88,6 +89,8 @@ import google.registry.model.smd.AbstractSignedMark;
import google.registry.model.smd.EncodedSignedMark; import google.registry.model.smd.EncodedSignedMark;
import google.registry.model.smd.SignedMark; import google.registry.model.smd.SignedMark;
import google.registry.model.smd.SignedMarkRevocationList; import google.registry.model.smd.SignedMarkRevocationList;
import google.registry.model.transfer.TransferData;
import google.registry.model.transfer.TransferResponse.DomainTransferResponse;
import google.registry.tmch.TmchXmlSignature; import google.registry.tmch.TmchXmlSignature;
import google.registry.tmch.TmchXmlSignature.CertificateSignatureException; import google.registry.tmch.TmchXmlSignature.CertificateSignatureException;
import google.registry.util.Idn; import google.registry.util.Idn;
@ -283,12 +286,10 @@ public class DomainFlowUtils {
if (!whitelist.isEmpty() && count == 0) { if (!whitelist.isEmpty() && count == 0) {
throw new NameserversNotSpecifiedException(); throw new NameserversNotSpecifiedException();
} }
if (count > MAX_NAMESERVERS_PER_DOMAIN) { if (count > MAX_NAMESERVERS_PER_DOMAIN) {
throw new TooManyNameserversException(String.format( throw new TooManyNameserversException(String.format(
"Only %d nameservers are allowed per domain", MAX_NAMESERVERS_PER_DOMAIN)); "Only %d nameservers are allowed per domain", MAX_NAMESERVERS_PER_DOMAIN));
} }
} }
static void validateNoDuplicateContacts(Set<DesignatedContact> contacts) static void validateNoDuplicateContacts(Set<DesignatedContact> contacts)
@ -684,6 +685,59 @@ public class DomainFlowUtils {
} }
} }
/** Create a poll message for the gaining client in a transfer. */
static PollMessage createGainingTransferPollMessage(
String targetId,
TransferData transferData,
@Nullable DateTime extendedRegistrationExpirationTime,
HistoryEntry historyEntry) {
return new PollMessage.OneTime.Builder()
.setClientId(transferData.getGainingClientId())
.setEventTime(transferData.getPendingTransferExpirationTime())
.setMsg(transferData.getTransferStatus().getMessage())
.setResponseData(ImmutableList.of(
createTransferResponse(targetId, transferData, extendedRegistrationExpirationTime),
DomainPendingActionNotificationResponse.create(
targetId,
transferData.getTransferStatus().isApproved(),
transferData.getTransferRequestTrid(),
historyEntry.getModificationTime())))
.setParent(historyEntry)
.build();
}
/** Create a poll message for the losing client in a transfer. */
static PollMessage createLosingTransferPollMessage(
String targetId,
TransferData transferData,
@Nullable DateTime extendedRegistrationExpirationTime,
HistoryEntry historyEntry) {
return new PollMessage.OneTime.Builder()
.setClientId(transferData.getLosingClientId())
.setEventTime(transferData.getPendingTransferExpirationTime())
.setMsg(transferData.getTransferStatus().getMessage())
.setResponseData(ImmutableList.of(
createTransferResponse(targetId, transferData, extendedRegistrationExpirationTime)))
.setParent(historyEntry)
.build();
}
/** Create a {@link DomainTransferResponse} off of the info in a {@link TransferData}. */
static DomainTransferResponse createTransferResponse(
String targetId,
TransferData transferData,
@Nullable DateTime extendedRegistrationExpirationTime) {
return new DomainTransferResponse.Builder()
.setFullyQualifiedDomainNameName(targetId)
.setGainingClientId(transferData.getGainingClientId())
.setLosingClientId(transferData.getLosingClientId())
.setPendingTransferExpirationTime(transferData.getPendingTransferExpirationTime())
.setTransferRequestTime(transferData.getTransferRequestTime())
.setTransferStatus(transferData.getTransferStatus())
.setExtendedRegistrationExpirationTime(extendedRegistrationExpirationTime)
.build();
}
/** Encoded signed marks must use base64 encoding. */ /** Encoded signed marks must use base64 encoding. */
static class Base64RequiredForEncodedSignedMarksException static class Base64RequiredForEncodedSignedMarksException
extends ParameterValuePolicyErrorException { extends ParameterValuePolicyErrorException {

View file

@ -14,86 +14,122 @@
package google.registry.flows.domain; package google.registry.flows.domain;
import static com.google.common.collect.Iterables.filter;
import static com.google.common.collect.Iterables.getOnlyElement;
import static google.registry.flows.ResourceFlowUtils.approvePendingTransfer;
import static google.registry.flows.ResourceFlowUtils.loadResourceToMutate;
import static google.registry.flows.ResourceFlowUtils.verifyHasPendingTransfer;
import static google.registry.flows.ResourceFlowUtils.verifyOptionalAuthInfoForResource;
import static google.registry.flows.ResourceFlowUtils.verifyResourceOwnership;
import static google.registry.flows.domain.DomainFlowUtils.checkAllowedAccessToTld; import static google.registry.flows.domain.DomainFlowUtils.checkAllowedAccessToTld;
import static google.registry.flows.domain.DomainFlowUtils.createGainingTransferPollMessage;
import static google.registry.flows.domain.DomainFlowUtils.createTransferResponse;
import static google.registry.flows.domain.DomainFlowUtils.updateAutorenewRecurrenceEndTime; import static google.registry.flows.domain.DomainFlowUtils.updateAutorenewRecurrenceEndTime;
import static google.registry.model.domain.DomainResource.extendRegistrationWithCap; import static google.registry.model.domain.DomainResource.extendRegistrationWithCap;
import static google.registry.model.eppoutput.Result.Code.SUCCESS;
import static google.registry.model.ofy.ObjectifyService.ofy; import static google.registry.model.ofy.ObjectifyService.ofy;
import static google.registry.pricing.PricingEngineProxy.getDomainRenewCost; import static google.registry.pricing.PricingEngineProxy.getDomainRenewCost;
import static google.registry.util.DateTimeUtils.END_OF_TIME; import static google.registry.util.DateTimeUtils.END_OF_TIME;
import com.google.common.base.Optional;
import com.google.common.base.Predicate; import com.google.common.base.Predicate;
import com.google.common.collect.FluentIterable;
import com.google.common.collect.ImmutableSet; import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
import com.googlecode.objectify.Key; import com.googlecode.objectify.Key;
import google.registry.flows.EppException; import google.registry.flows.EppException;
import google.registry.flows.ResourceTransferApproveFlow; import google.registry.flows.FlowModule.ClientId;
import google.registry.flows.FlowModule.TargetId;
import google.registry.flows.LoggedInFlow;
import google.registry.flows.TransactionalFlow;
import google.registry.model.ImmutableObject;
import google.registry.model.billing.BillingEvent; import google.registry.model.billing.BillingEvent;
import google.registry.model.billing.BillingEvent.Flag; import google.registry.model.billing.BillingEvent.Flag;
import google.registry.model.billing.BillingEvent.Reason; import google.registry.model.billing.BillingEvent.Reason;
import google.registry.model.domain.DomainCommand.Transfer;
import google.registry.model.domain.DomainResource; import google.registry.model.domain.DomainResource;
import google.registry.model.domain.DomainResource.Builder;
import google.registry.model.domain.GracePeriod; import google.registry.model.domain.GracePeriod;
import google.registry.model.domain.metadata.MetadataExtension;
import google.registry.model.domain.rgp.GracePeriodStatus; import google.registry.model.domain.rgp.GracePeriodStatus;
import google.registry.model.eppcommon.AuthInfo;
import google.registry.model.eppoutput.EppOutput;
import google.registry.model.poll.PollMessage; import google.registry.model.poll.PollMessage;
import google.registry.model.registry.Registry; import google.registry.model.registry.Registry;
import google.registry.model.reporting.HistoryEntry; import google.registry.model.reporting.HistoryEntry;
import google.registry.model.transfer.TransferData; import google.registry.model.transfer.TransferData;
import google.registry.model.transfer.TransferStatus;
import javax.inject.Inject; import javax.inject.Inject;
import org.joda.time.DateTime; import org.joda.time.DateTime;
/** /**
* An EPP flow that approves a pending transfer on a {@link DomainResource}. * An EPP flow that approves a pending transfer on a {@link DomainResource}.
* *
* <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.
*
* <p>When the transfer was requested, poll messages and billing events were saved to Datastore with
* timestamps such that they only would become active when the transfer period passed. In this flow,
* those speculative objects are deleted and replaced with new ones with the correct approval time.
*
* <p>The logic in this flow, which handles client approvals, very closely parallels the logic in * <p>The logic in this flow, which handles client approvals, very closely parallels the logic in
* {@link DomainResource#cloneProjectedAtTime} which handles implicit server approvals. * {@link DomainResource#cloneProjectedAtTime} which handles implicit server approvals.
* *
* @error {@link google.registry.flows.domain.DomainFlowUtils.NotAuthorizedForTldException}
* @error {@link google.registry.flows.ResourceFlowUtils.BadAuthInfoForResourceException} * @error {@link google.registry.flows.ResourceFlowUtils.BadAuthInfoForResourceException}
* @error {@link google.registry.flows.ResourceFlowUtils.ResourceNotOwnedException} * @error {@link google.registry.flows.ResourceFlowUtils.ResourceNotOwnedException}
* @error {@link google.registry.flows.ResourceMutateFlow.ResourceToMutateDoesNotExistException} * @error {@link google.registry.flows.exceptions.NotPendingTransferException}
* @error {@link google.registry.flows.ResourceMutatePendingTransferFlow.NotPendingTransferException} * @error {@link google.registry.flows.exceptions.ResourceToMutateDoesNotExistException}
* @error {@link DomainFlowUtils.NotAuthorizedForTldException}
*/ */
public class DomainTransferApproveFlow extends public final class DomainTransferApproveFlow extends LoggedInFlow implements TransactionalFlow {
ResourceTransferApproveFlow<DomainResource, Builder, Transfer> {
@Inject Optional<AuthInfo> authInfo;
@Inject @ClientId String clientId;
@Inject @TargetId String targetId;
@Inject HistoryEntry.Builder historyBuilder;
@Inject DomainTransferApproveFlow() {} @Inject DomainTransferApproveFlow() {}
@Override @Override
protected void verifyOwnedResourcePendingTransferMutationAllowed() throws EppException { protected final void initLoggedInFlow() throws EppException {
checkAllowedAccessToTld(getAllowedTlds(), existingResource.getTld()); registerExtensions(MetadataExtension.class);
} }
@Override @Override
protected final void setTransferApproveProperties(Builder builder) { public final EppOutput run() throws EppException {
TransferData transferData = existingResource.getTransferData(); DomainResource existingDomain = loadResourceToMutate(DomainResource.class, targetId, now);
verifyOptionalAuthInfoForResource(authInfo, existingDomain);
verifyHasPendingTransfer(existingDomain);
verifyResourceOwnership(clientId, existingDomain);
String tld = existingDomain.getTld();
checkAllowedAccessToTld(getAllowedTlds(), tld);
HistoryEntry historyEntry = historyBuilder
.setType(HistoryEntry.Type.DOMAIN_TRANSFER_APPROVE)
.setModificationTime(now)
.setParent(Key.create(existingDomain))
.build();
TransferData transferData = existingDomain.getTransferData();
String gainingClientId = transferData.getGainingClientId(); String gainingClientId = transferData.getGainingClientId();
String tld = existingResource.getTld();
int extraYears = transferData.getExtendedRegistrationYears(); int extraYears = transferData.getExtendedRegistrationYears();
// Bill for the transfer. // Bill for the transfer.
BillingEvent.OneTime billingEvent = BillingEvent.OneTime billingEvent = new BillingEvent.OneTime.Builder()
new BillingEvent.OneTime.Builder() .setReason(Reason.TRANSFER)
.setReason(Reason.TRANSFER) .setTargetId(targetId)
.setTargetId(targetId) .setClientId(gainingClientId)
.setClientId(gainingClientId) .setPeriodYears(extraYears)
.setPeriodYears(extraYears) .setCost(getDomainRenewCost(targetId, transferData.getTransferRequestTime(), extraYears))
.setCost( .setEventTime(now)
getDomainRenewCost(targetId, transferData.getTransferRequestTime(), extraYears)) .setBillingTime(now.plus(Registry.get(tld).getTransferGracePeriodLength()))
.setEventTime(now) .setParent(historyEntry)
.setBillingTime(now.plus(Registry.get(tld).getTransferGracePeriodLength())) .build();
.setParent(historyEntry)
.build();
ofy().save().entity(billingEvent);
// If we are within an autorenew grace period, cancel the autorenew billing event and reduce // If we are within an autorenew grace period, cancel the autorenew billing event and reduce
// the number of years to extend the registration by one. // the number of years to extend the registration by one.
GracePeriod autorenewGrace = Iterables.getOnlyElement(FluentIterable GracePeriod autorenewGrace = getOnlyElement(
.from(existingResource.getGracePeriods()) filter(
.filter(new Predicate<GracePeriod>(){ existingDomain.getGracePeriods(),
@Override new Predicate<GracePeriod>() {
public boolean apply(GracePeriod gracePeriod) { @Override
return GracePeriodStatus.AUTO_RENEW.equals(gracePeriod.getType()); public boolean apply(GracePeriod gracePeriod) {
}}), null); return GracePeriodStatus.AUTO_RENEW.equals(gracePeriod.getType());
}}),
null);
if (autorenewGrace != null) { if (autorenewGrace != null) {
extraYears--; extraYears--;
ofy().save().entity( ofy().save().entity(
@ -101,9 +137,9 @@ public class DomainTransferApproveFlow extends
} }
// Close the old autorenew event and poll message at the transfer time (aka now). This may end // Close the old autorenew event and poll message at the transfer time (aka now). This may end
// up deleting the poll message. // up deleting the poll message.
updateAutorenewRecurrenceEndTime(existingResource, now); updateAutorenewRecurrenceEndTime(existingDomain, now);
DateTime newExpirationTime = extendRegistrationWithCap( DateTime newExpirationTime = extendRegistrationWithCap(
now, existingResource.getRegistrationExpirationTime(), extraYears); now, existingDomain.getRegistrationExpirationTime(), extraYears);
// Create a new autorenew event starting at the expiration time. // Create a new autorenew event starting at the expiration time.
BillingEvent.Recurring autorenewEvent = new BillingEvent.Recurring.Builder() BillingEvent.Recurring autorenewEvent = new BillingEvent.Recurring.Builder()
.setReason(Reason.RENEW) .setReason(Reason.RENEW)
@ -114,7 +150,6 @@ public class DomainTransferApproveFlow extends
.setRecurrenceEndTime(END_OF_TIME) .setRecurrenceEndTime(END_OF_TIME)
.setParent(historyEntry) .setParent(historyEntry)
.build(); .build();
ofy().save().entity(autorenewEvent);
// Create a new autorenew poll message. // Create a new autorenew poll message.
PollMessage.Autorenew gainingClientAutorenewPollMessage = new PollMessage.Autorenew.Builder() PollMessage.Autorenew gainingClientAutorenewPollMessage = new PollMessage.Autorenew.Builder()
.setTargetId(targetId) .setTargetId(targetId)
@ -124,18 +159,35 @@ public class DomainTransferApproveFlow extends
.setMsg("Domain was auto-renewed.") .setMsg("Domain was auto-renewed.")
.setParent(historyEntry) .setParent(historyEntry)
.build(); .build();
ofy().save().entity(gainingClientAutorenewPollMessage); DomainResource newDomain =
builder approvePendingTransfer(existingDomain, TransferStatus.CLIENT_APPROVED, now)
.setRegistrationExpirationTime(newExpirationTime) .asBuilder()
.setAutorenewBillingEvent(Key.create(autorenewEvent)) .setRegistrationExpirationTime(newExpirationTime)
.setAutorenewPollMessage(Key.create(gainingClientAutorenewPollMessage)) .setAutorenewBillingEvent(Key.create(autorenewEvent))
// Remove all the old grace periods and add a new one for the transfer. .setAutorenewPollMessage(Key.create(gainingClientAutorenewPollMessage))
.setGracePeriods(ImmutableSet.of( // Remove all the old grace periods and add a new one for the transfer.
GracePeriod.forBillingEvent(GracePeriodStatus.TRANSFER, billingEvent))); .setGracePeriods(ImmutableSet.of(
} GracePeriod.forBillingEvent(GracePeriodStatus.TRANSFER, billingEvent)))
.build();
@Override // Create a poll message for the gaining client.
protected final HistoryEntry.Type getHistoryEntryType() { PollMessage gainingClientPollMessage = createGainingTransferPollMessage(
return HistoryEntry.Type.DOMAIN_TRANSFER_APPROVE; targetId,
newDomain.getTransferData(),
newExpirationTime,
historyEntry);
ofy().save().<ImmutableObject>entities(
newDomain,
historyEntry,
billingEvent,
autorenewEvent,
gainingClientPollMessage,
gainingClientAutorenewPollMessage);
// Delete the billing event and poll messages that were written in case the transfer would have
// been implicitly server approved.
ofy().delete().keys(existingDomain.getTransferData().getServerApproveEntities());
return createOutput(
SUCCESS,
createTransferResponse(
targetId, newDomain.getTransferData(), newDomain.getRegistrationExpirationTime()));
} }
} }

View file

@ -14,49 +14,93 @@
package google.registry.flows.domain; package google.registry.flows.domain;
import static google.registry.flows.ResourceFlowUtils.denyPendingTransfer;
import static google.registry.flows.ResourceFlowUtils.loadResourceToMutate;
import static google.registry.flows.ResourceFlowUtils.verifyHasPendingTransfer;
import static google.registry.flows.ResourceFlowUtils.verifyIsGainingRegistrar;
import static google.registry.flows.ResourceFlowUtils.verifyOptionalAuthInfoForResource;
import static google.registry.flows.domain.DomainFlowUtils.checkAllowedAccessToTld; import static google.registry.flows.domain.DomainFlowUtils.checkAllowedAccessToTld;
import static google.registry.flows.domain.DomainFlowUtils.createLosingTransferPollMessage;
import static google.registry.flows.domain.DomainFlowUtils.createTransferResponse;
import static google.registry.flows.domain.DomainFlowUtils.updateAutorenewRecurrenceEndTime; import static google.registry.flows.domain.DomainFlowUtils.updateAutorenewRecurrenceEndTime;
import static google.registry.model.eppoutput.Result.Code.SUCCESS;
import static google.registry.model.ofy.ObjectifyService.ofy;
import static google.registry.util.DateTimeUtils.END_OF_TIME; import static google.registry.util.DateTimeUtils.END_OF_TIME;
import com.google.common.base.Optional;
import com.googlecode.objectify.Key;
import google.registry.flows.EppException; import google.registry.flows.EppException;
import google.registry.flows.ResourceTransferCancelFlow; import google.registry.flows.FlowModule.ClientId;
import google.registry.model.domain.DomainCommand.Transfer; import google.registry.flows.FlowModule.TargetId;
import google.registry.flows.LoggedInFlow;
import google.registry.flows.TransactionalFlow;
import google.registry.model.ImmutableObject;
import google.registry.model.domain.DomainResource; import google.registry.model.domain.DomainResource;
import google.registry.model.domain.DomainResource.Builder; import google.registry.model.domain.metadata.MetadataExtension;
import google.registry.model.eppcommon.AuthInfo;
import google.registry.model.eppoutput.EppOutput;
import google.registry.model.reporting.HistoryEntry; import google.registry.model.reporting.HistoryEntry;
import google.registry.model.transfer.TransferStatus;
import javax.inject.Inject; import javax.inject.Inject;
/** /**
* An EPP flow that cancels a pending transfer on a {@link DomainResource}. * An EPP flow that cancels a pending transfer on a {@link DomainResource}.
* *
* @error {@link google.registry.flows.domain.DomainFlowUtils.NotAuthorizedForTldException} * <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.
*
* <p>When the transfer was requested, poll messages and billing events were saved to Datastore with
* timestamps such that they only would become active when the transfer period passed. In this flow,
* those speculative objects are deleted.
*
* @error {@link google.registry.flows.ResourceFlowUtils.BadAuthInfoForResourceException} * @error {@link google.registry.flows.ResourceFlowUtils.BadAuthInfoForResourceException}
* @error {@link google.registry.flows.ResourceMutateFlow.ResourceToMutateDoesNotExistException} * @error {@link google.registry.flows.exceptions.NotPendingTransferException}
* @error {@link google.registry.flows.ResourceMutatePendingTransferFlow.NotPendingTransferException} * @error {@link google.registry.flows.exceptions.NotTransferInitiatorException}
* @error {@link google.registry.flows.ResourceTransferCancelFlow.NotTransferInitiatorException} * @error {@link google.registry.flows.exceptions.ResourceToMutateDoesNotExistException}
* @error {@link DomainFlowUtils.NotAuthorizedForTldException}
*/ */
public class DomainTransferCancelFlow public final class DomainTransferCancelFlow extends LoggedInFlow implements TransactionalFlow {
extends ResourceTransferCancelFlow<DomainResource, Builder, Transfer> {
@Inject Optional<AuthInfo> authInfo;
@Inject @ClientId String clientId;
@Inject @TargetId String targetId;
@Inject HistoryEntry.Builder historyBuilder;
@Inject DomainTransferCancelFlow() {} @Inject DomainTransferCancelFlow() {}
/**
* Reopen the autorenew event and poll message that we closed for the implicit transfer.
* This may end up recreating the autorenew poll message if it was deleted when the transfer
* request was made.
*/
@Override @Override
protected final void modifyRelatedResourcesForTransferCancel() { protected final void initLoggedInFlow() throws EppException {
updateAutorenewRecurrenceEndTime(existingResource, END_OF_TIME); registerExtensions(MetadataExtension.class);
} }
@Override @Override
protected void verifyTransferCancelMutationAllowed() throws EppException { public final EppOutput run() throws EppException {
checkAllowedAccessToTld(getAllowedTlds(), existingResource.getTld()); DomainResource existingDomain = loadResourceToMutate(DomainResource.class, targetId, now);
} verifyOptionalAuthInfoForResource(authInfo, existingDomain);
verifyHasPendingTransfer(existingDomain);
@Override verifyIsGainingRegistrar(existingDomain, clientId);
protected final HistoryEntry.Type getHistoryEntryType() { checkAllowedAccessToTld(getAllowedTlds(), existingDomain.getTld());
return HistoryEntry.Type.DOMAIN_TRANSFER_CANCEL; HistoryEntry historyEntry = historyBuilder
.setType(HistoryEntry.Type.DOMAIN_TRANSFER_CANCEL)
.setModificationTime(now)
.setParent(Key.create(existingDomain))
.build();
DomainResource newDomain =
denyPendingTransfer(existingDomain, TransferStatus.CLIENT_CANCELLED, now);
ofy().save().<ImmutableObject>entities(
newDomain,
historyEntry,
createLosingTransferPollMessage(
targetId, newDomain.getTransferData(), null, historyEntry));
// Reopen the autorenew event and poll message that we closed for the implicit transfer. This
// may recreate the autorenew poll message if it was deleted when the transfer request was made.
updateAutorenewRecurrenceEndTime(existingDomain, END_OF_TIME);
// Delete the billing event and poll messages that were written in case the transfer would have
// been implicitly server approved.
ofy().delete().keys(existingDomain.getTransferData().getServerApproveEntities());
return createOutput(
SUCCESS,
createTransferResponse(targetId, newDomain.getTransferData(), null));
} }
} }

View file

@ -14,19 +14,75 @@
package google.registry.flows.domain; package google.registry.flows.domain;
import google.registry.flows.ResourceTransferQueryFlow; import static google.registry.flows.ResourceFlowUtils.loadResourceForQuery;
import google.registry.model.domain.DomainCommand.Transfer; import static google.registry.flows.ResourceFlowUtils.verifyOptionalAuthInfoForResource;
import static google.registry.flows.domain.DomainFlowUtils.createTransferResponse;
import static google.registry.model.domain.DomainResource.extendRegistrationWithCap;
import static google.registry.model.eppoutput.Result.Code.SUCCESS;
import com.google.common.base.Optional;
import google.registry.flows.EppException;
import google.registry.flows.FlowModule.ClientId;
import google.registry.flows.FlowModule.TargetId;
import google.registry.flows.LoggedInFlow;
import google.registry.flows.exceptions.NoTransferHistoryToQueryException;
import google.registry.flows.exceptions.NotAuthorizedToViewTransferException;
import google.registry.model.domain.DomainResource; import google.registry.model.domain.DomainResource;
import google.registry.model.eppcommon.AuthInfo;
import google.registry.model.eppoutput.EppOutput;
import google.registry.model.transfer.TransferData;
import google.registry.model.transfer.TransferStatus;
import javax.inject.Inject; import javax.inject.Inject;
import org.joda.time.DateTime;
/** /**
* An EPP flow that queries a pending transfer on a {@link DomainResource}. * An EPP flow that queries a pending transfer on a {@link DomainResource}.
* *
* <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.ResourceFlowUtils.BadAuthInfoForResourceException} * @error {@link google.registry.flows.ResourceFlowUtils.BadAuthInfoForResourceException}
* @error {@link google.registry.flows.ResourceQueryFlow.ResourceToQueryDoesNotExistException} * @error {@link google.registry.flows.exceptions.NoTransferHistoryToQueryException}
* @error {@link google.registry.flows.ResourceTransferQueryFlow.NoTransferHistoryToQueryException} * @error {@link google.registry.flows.exceptions.NotAuthorizedToViewTransferException}
* @error {@link google.registry.flows.ResourceTransferQueryFlow.NotAuthorizedToViewTransferException} * @error {@link google.registry.flows.exceptions.ResourceToQueryDoesNotExistException}
*/ */
public class DomainTransferQueryFlow extends ResourceTransferQueryFlow<DomainResource, Transfer> { public final class DomainTransferQueryFlow extends LoggedInFlow {
@Inject Optional<AuthInfo> authInfo;
@Inject @ClientId String clientId;
@Inject @TargetId String targetId;
@Inject DomainTransferQueryFlow() {} @Inject DomainTransferQueryFlow() {}
@Override
public final EppOutput run() throws EppException {
DomainResource domain = loadResourceForQuery(DomainResource.class, targetId, now);
verifyOptionalAuthInfoForResource(authInfo, domain);
// 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).
TransferData transferData = domain.getTransferData();
if (transferData.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.isPresent()
&& !clientId.equals(transferData.getGainingClientId())
&& !clientId.equals(transferData.getLosingClientId())) {
throw new NotAuthorizedToViewTransferException();
}
DateTime newExpirationTime = null;
if (transferData.getTransferStatus().isApproved()
|| transferData.getTransferStatus().equals(TransferStatus.PENDING)) {
// TODO(b/25084229): This is not quite right.
newExpirationTime = extendRegistrationWithCap(
now,
domain.getRegistrationExpirationTime(),
transferData.getExtendedRegistrationYears());
}
return createOutput(SUCCESS, createTransferResponse(targetId, transferData, newExpirationTime));
}
} }

View file

@ -14,48 +14,93 @@
package google.registry.flows.domain; package google.registry.flows.domain;
import static google.registry.flows.ResourceFlowUtils.denyPendingTransfer;
import static google.registry.flows.ResourceFlowUtils.loadResourceToMutate;
import static google.registry.flows.ResourceFlowUtils.verifyHasPendingTransfer;
import static google.registry.flows.ResourceFlowUtils.verifyOptionalAuthInfoForResource;
import static google.registry.flows.ResourceFlowUtils.verifyResourceOwnership;
import static google.registry.flows.domain.DomainFlowUtils.checkAllowedAccessToTld; import static google.registry.flows.domain.DomainFlowUtils.checkAllowedAccessToTld;
import static google.registry.flows.domain.DomainFlowUtils.createGainingTransferPollMessage;
import static google.registry.flows.domain.DomainFlowUtils.createTransferResponse;
import static google.registry.flows.domain.DomainFlowUtils.updateAutorenewRecurrenceEndTime; import static google.registry.flows.domain.DomainFlowUtils.updateAutorenewRecurrenceEndTime;
import static google.registry.model.eppoutput.Result.Code.SUCCESS;
import static google.registry.model.ofy.ObjectifyService.ofy;
import static google.registry.util.DateTimeUtils.END_OF_TIME; import static google.registry.util.DateTimeUtils.END_OF_TIME;
import com.google.common.base.Optional;
import com.googlecode.objectify.Key;
import google.registry.flows.EppException; import google.registry.flows.EppException;
import google.registry.flows.ResourceTransferRejectFlow; import google.registry.flows.FlowModule.ClientId;
import google.registry.model.domain.DomainCommand.Transfer; import google.registry.flows.FlowModule.TargetId;
import google.registry.flows.LoggedInFlow;
import google.registry.flows.TransactionalFlow;
import google.registry.model.ImmutableObject;
import google.registry.model.domain.DomainResource; import google.registry.model.domain.DomainResource;
import google.registry.model.domain.DomainResource.Builder; import google.registry.model.domain.metadata.MetadataExtension;
import google.registry.model.eppcommon.AuthInfo;
import google.registry.model.eppoutput.EppOutput;
import google.registry.model.reporting.HistoryEntry; import google.registry.model.reporting.HistoryEntry;
import google.registry.model.transfer.TransferStatus;
import javax.inject.Inject; import javax.inject.Inject;
/** /**
* An EPP flow that rejects a pending transfer on a {@link DomainResource}. * An EPP flow that rejects a pending transfer on a {@link DomainResource}.
* *
* @error {@link google.registry.flows.domain.DomainFlowUtils.NotAuthorizedForTldException} * <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.
*
* <p>When the transfer was requested, poll messages and billing events were saved to Datastore with
* timestamps such that they only would become active when the transfer period passed. In this flow,
* those speculative objects are deleted.
*
* @error {@link google.registry.flows.ResourceFlowUtils.BadAuthInfoForResourceException} * @error {@link google.registry.flows.ResourceFlowUtils.BadAuthInfoForResourceException}
* @error {@link google.registry.flows.ResourceFlowUtils.ResourceNotOwnedException} * @error {@link google.registry.flows.ResourceFlowUtils.ResourceNotOwnedException}
* @error {@link google.registry.flows.ResourceMutateFlow.ResourceToMutateDoesNotExistException} * @error {@link google.registry.flows.exceptions.NotPendingTransferException}
* @error {@link google.registry.flows.ResourceMutatePendingTransferFlow.NotPendingTransferException} * @error {@link google.registry.flows.exceptions.ResourceToMutateDoesNotExistException}
* @error {@link DomainFlowUtils.NotAuthorizedForTldException}
*/ */
public class DomainTransferRejectFlow public final class DomainTransferRejectFlow extends LoggedInFlow implements TransactionalFlow {
extends ResourceTransferRejectFlow<DomainResource, Builder, Transfer> {
@Inject Optional<AuthInfo> authInfo;
@Inject @ClientId String clientId;
@Inject @TargetId String targetId;
@Inject HistoryEntry.Builder historyBuilder;
@Inject DomainTransferRejectFlow() {} @Inject DomainTransferRejectFlow() {}
@Override @Override
protected void verifyOwnedResourcePendingTransferMutationAllowed() throws EppException { protected final void initLoggedInFlow() throws EppException {
checkAllowedAccessToTld(getAllowedTlds(), existingResource.getTld()); registerExtensions(MetadataExtension.class);
}
/**
* Reopen the autorenew event and poll message that we closed for the implicit transfer.
* This may end up recreating the poll message if it was deleted upon the transfer request.
*/
@Override
protected final void modifyRelatedResourcesForTransferReject() {
updateAutorenewRecurrenceEndTime(existingResource, END_OF_TIME);
} }
@Override @Override
protected final HistoryEntry.Type getHistoryEntryType() { public final EppOutput run() throws EppException {
return HistoryEntry.Type.DOMAIN_TRANSFER_REJECT; DomainResource existingDomain = loadResourceToMutate(DomainResource.class, targetId, now);
HistoryEntry historyEntry = historyBuilder
.setType(HistoryEntry.Type.DOMAIN_TRANSFER_REJECT)
.setModificationTime(now)
.setParent(Key.create(existingDomain))
.build();
verifyOptionalAuthInfoForResource(authInfo, existingDomain);
verifyHasPendingTransfer(existingDomain);
verifyResourceOwnership(clientId, existingDomain);
checkAllowedAccessToTld(getAllowedTlds(), existingDomain.getTld());
DomainResource newDomain =
denyPendingTransfer(existingDomain, TransferStatus.CLIENT_REJECTED, now);
ofy().save().<ImmutableObject>entities(
newDomain,
historyEntry,
createGainingTransferPollMessage(
targetId, newDomain.getTransferData(), null, historyEntry));
// Reopen the autorenew event and poll message that we closed for the implicit transfer. This
// may end up recreating the poll message if it was deleted upon the transfer request.
updateAutorenewRecurrenceEndTime(existingDomain, END_OF_TIME);
// Delete the billing event and poll messages that were written in case the transfer would have
// been implicitly server approved.
ofy().delete().keys(existingDomain.getTransferData().getServerApproveEntities());
return createOutput(
SUCCESS,
createTransferResponse(targetId, newDomain.getTransferData(), null));
} }
} }

View file

@ -14,13 +14,23 @@
package google.registry.flows.domain; package google.registry.flows.domain;
import static com.google.common.collect.Iterables.filter;
import static com.google.common.collect.Iterables.getOnlyElement;
import static com.google.common.collect.Sets.union;
import static google.registry.flows.ResourceFlowUtils.loadResourceToMutate;
import static google.registry.flows.ResourceFlowUtils.verifyNoDisallowedStatuses;
import static google.registry.flows.ResourceFlowUtils.verifyRequiredAuthInfoForResourceTransfer;
import static google.registry.flows.domain.DomainFlowUtils.checkAllowedAccessToTld; import static google.registry.flows.domain.DomainFlowUtils.checkAllowedAccessToTld;
import static google.registry.flows.domain.DomainFlowUtils.createGainingTransferPollMessage;
import static google.registry.flows.domain.DomainFlowUtils.createLosingTransferPollMessage;
import static google.registry.flows.domain.DomainFlowUtils.createTransferResponse;
import static google.registry.flows.domain.DomainFlowUtils.updateAutorenewRecurrenceEndTime; import static google.registry.flows.domain.DomainFlowUtils.updateAutorenewRecurrenceEndTime;
import static google.registry.flows.domain.DomainFlowUtils.validateFeeChallenge; import static google.registry.flows.domain.DomainFlowUtils.validateFeeChallenge;
import static google.registry.flows.domain.DomainFlowUtils.verifyPremiumNameIsNotBlocked; import static google.registry.flows.domain.DomainFlowUtils.verifyPremiumNameIsNotBlocked;
import static google.registry.flows.domain.DomainFlowUtils.verifyUnitIsYears; import static google.registry.flows.domain.DomainFlowUtils.verifyUnitIsYears;
import static google.registry.model.domain.DomainResource.extendRegistrationWithCap; import static google.registry.model.domain.DomainResource.extendRegistrationWithCap;
import static google.registry.model.domain.fee.Fee.FEE_TRANSFER_COMMAND_EXTENSIONS_IN_PREFERENCE_ORDER; import static google.registry.model.domain.fee.Fee.FEE_TRANSFER_COMMAND_EXTENSIONS_IN_PREFERENCE_ORDER;
import static google.registry.model.eppoutput.Result.Code.SUCCESS_WITH_ACTION_PENDING;
import static google.registry.model.ofy.ObjectifyService.ofy; import static google.registry.model.ofy.ObjectifyService.ofy;
import static google.registry.pricing.PricingEngineProxy.getDomainRenewCost; import static google.registry.pricing.PricingEngineProxy.getDomainRenewCost;
import static google.registry.util.DateTimeUtils.END_OF_TIME; import static google.registry.util.DateTimeUtils.END_OF_TIME;
@ -30,7 +40,13 @@ import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet; import com.google.common.collect.ImmutableSet;
import com.googlecode.objectify.Key; import com.googlecode.objectify.Key;
import google.registry.flows.EppException; import google.registry.flows.EppException;
import google.registry.flows.ResourceTransferRequestFlow; import google.registry.flows.FlowModule.ClientId;
import google.registry.flows.FlowModule.TargetId;
import google.registry.flows.LoggedInFlow;
import google.registry.flows.TransactionalFlow;
import google.registry.flows.exceptions.AlreadyPendingTransferException;
import google.registry.flows.exceptions.ObjectAlreadySponsoredException;
import google.registry.model.ImmutableObject;
import google.registry.model.billing.BillingEvent; import google.registry.model.billing.BillingEvent;
import google.registry.model.billing.BillingEvent.Flag; import google.registry.model.billing.BillingEvent.Flag;
import google.registry.model.billing.BillingEvent.Reason; import google.registry.model.billing.BillingEvent.Reason;
@ -40,230 +56,371 @@ import google.registry.model.domain.Period;
import google.registry.model.domain.fee.BaseFee.FeeType; import google.registry.model.domain.fee.BaseFee.FeeType;
import google.registry.model.domain.fee.Fee; import google.registry.model.domain.fee.Fee;
import google.registry.model.domain.fee.FeeTransformCommandExtension; import google.registry.model.domain.fee.FeeTransformCommandExtension;
import google.registry.model.domain.fee.FeeTransformResponseExtension;
import google.registry.model.domain.flags.FlagsTransferCommandExtension; import google.registry.model.domain.flags.FlagsTransferCommandExtension;
import google.registry.model.eppoutput.EppResponse.ResponseExtension; 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.EppOutput;
import google.registry.model.poll.PollMessage; import google.registry.model.poll.PollMessage;
import google.registry.model.registry.Registry; import google.registry.model.registry.Registry;
import google.registry.model.reporting.HistoryEntry; import google.registry.model.reporting.HistoryEntry;
import google.registry.model.transfer.TransferData; import google.registry.model.transfer.TransferData;
import google.registry.model.transfer.TransferData.Builder;
import google.registry.model.transfer.TransferData.TransferServerApproveEntity; import google.registry.model.transfer.TransferData.TransferServerApproveEntity;
import java.util.HashSet; import google.registry.model.transfer.TransferResponse.DomainTransferResponse;
import java.util.Set; import google.registry.model.transfer.TransferStatus;
import javax.inject.Inject; import javax.inject.Inject;
import org.joda.money.Money; import org.joda.money.Money;
import org.joda.time.DateTime; import org.joda.time.DateTime;
import org.joda.time.Duration;
/** /**
* An EPP flow that requests a transfer on a {@link DomainResource}. * An EPP flow that requests a transfer on a {@link DomainResource}.
* *
* @error {@link google.registry.flows.domain.DomainFlowUtils.NotAuthorizedForTldException} * <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.
*
* <p>When a transfer is requested, poll messages and billing events are saved to Datastore with
* timestamps such that they only become active when the server-approval period passes. Keys to
* these speculative objects are saved in the domain's transfer data, and on explicit approval,
* rejection or cancellation of the request, they will be deleted (and in the approval case,
* replaced with new ones with the correct approval time).
*
* @error {@link google.registry.flows.ResourceFlowUtils.BadAuthInfoForResourceException} * @error {@link google.registry.flows.ResourceFlowUtils.BadAuthInfoForResourceException}
* @error {@link google.registry.flows.ResourceMutateFlow.ResourceToMutateDoesNotExistException} * @error {@link google.registry.flows.exceptions.AlreadyPendingTransferException}
* @error {@link google.registry.flows.ResourceTransferRequestFlow.AlreadyPendingTransferException} * @error {@link google.registry.flows.exceptions.MissingTransferRequestAuthInfoException}
* @error {@link google.registry.flows.ResourceTransferRequestFlow.MissingTransferRequestAuthInfoException} * @error {@link google.registry.flows.exceptions.ObjectAlreadySponsoredException}
* @error {@link google.registry.flows.ResourceTransferRequestFlow.ObjectAlreadySponsoredException} * @error {@link google.registry.flows.exceptions.ResourceStatusProhibitsOperationException}
* @error {@link google.registry.flows.SingleResourceFlow.ResourceStatusProhibitsOperationException} * @error {@link google.registry.flows.exceptions.ResourceToMutateDoesNotExistException}
* @error {@link DomainFlowUtils.BadPeriodUnitException} * @error {@link DomainFlowUtils.BadPeriodUnitException}
* @error {@link DomainFlowUtils.CurrencyUnitMismatchException} * @error {@link DomainFlowUtils.CurrencyUnitMismatchException}
* @error {@link DomainFlowUtils.CurrencyValueScaleException} * @error {@link DomainFlowUtils.CurrencyValueScaleException}
* @error {@link DomainFlowUtils.FeesMismatchException} * @error {@link DomainFlowUtils.FeesMismatchException}
* @error {@link DomainFlowUtils.FeesRequiredForPremiumNameException} * @error {@link DomainFlowUtils.FeesRequiredForPremiumNameException}
* @error {@link DomainFlowUtils.NotAuthorizedForTldException}
* @error {@link DomainFlowUtils.PremiumNameBlockedException} * @error {@link DomainFlowUtils.PremiumNameBlockedException}
* @error {@link DomainFlowUtils.UnsupportedFeeAttributeException} * @error {@link DomainFlowUtils.UnsupportedFeeAttributeException}
*/ */
public class DomainTransferRequestFlow public final class DomainTransferRequestFlow extends LoggedInFlow implements TransactionalFlow {
extends ResourceTransferRequestFlow<DomainResource, Transfer> {
/** The time when the transfer will be server approved if no other action happens first. */ private static final ImmutableSet<StatusValue> DISALLOWED_STATUSES = ImmutableSet.of(
private DateTime automaticTransferTime; StatusValue.CLIENT_TRANSFER_PROHIBITED,
StatusValue.PENDING_DELETE,
/** A new one-time billing event for the renewal packaged as part of this transfer. */ StatusValue.SERVER_TRANSFER_PROHIBITED);
private BillingEvent.OneTime transferBillingEvent;
/** A new autorenew billing event starting at the transfer time. */
private BillingEvent.Recurring gainingClientAutorenewEvent;
/** A new autorenew poll message starting at the transfer time. */
private PollMessage.Autorenew gainingClientAutorenewPollMessage;
/** The amount that this transfer will cost due to the implied renew. */
private Money renewCost;
/** Extra flow logic instance. */
protected Optional<RegistryExtraFlowLogic> extraFlowLogic;
/**
* An optional extension from the client specifying how much they think the transfer should cost.
*/
private FeeTransformCommandExtension feeTransfer;
@Inject ResourceCommand resourceCommand;
@Inject Optional<AuthInfo> authInfo;
@Inject @ClientId String gainingClientId;
@Inject @TargetId String targetId;
@Inject HistoryEntry.Builder historyBuilder;
@Inject DomainTransferRequestFlow() {} @Inject DomainTransferRequestFlow() {}
@Override @Override
protected Duration getAutomaticTransferLength() { protected final void initLoggedInFlow() throws EppException {
return Registry.get(existingResource.getTld()).getAutomaticTransferLength(); registerExtensions(MetadataExtension.class, FlagsTransferCommandExtension.class);
}
@Override
protected final void initResourceTransferRequestFlow() throws EppException {
registerExtensions(FlagsTransferCommandExtension.class);
registerExtensions(FEE_TRANSFER_COMMAND_EXTENSIONS_IN_PREFERENCE_ORDER); registerExtensions(FEE_TRANSFER_COMMAND_EXTENSIONS_IN_PREFERENCE_ORDER);
feeTransfer = eppInput.getFirstExtensionOfClasses( }
@Override
public final EppOutput run() throws EppException {
Period period = ((Transfer) resourceCommand).getPeriod();
int years = period.getValue();
DomainResource existingDomain = loadResourceToMutate(DomainResource.class, targetId, now);
verifyTransferAllowed(existingDomain, period);
String tld = existingDomain.getTld();
Registry registry = Registry.get(tld);
// The cost of the renewal implied by a transfer.
Money renewCost = getDomainRenewCost(targetId, now, years);
// An optional extension from the client specifying what they think the transfer should cost.
FeeTransformCommandExtension feeTransfer = eppInput.getFirstExtensionOfClasses(
FEE_TRANSFER_COMMAND_EXTENSIONS_IN_PREFERENCE_ORDER); FEE_TRANSFER_COMMAND_EXTENSIONS_IN_PREFERENCE_ORDER);
// The "existingResource" field is loaded before this function is called, but it may be null if validateFeeChallenge(targetId, tld, now, feeTransfer, renewCost);
// the domain name specified is invalid or doesn't exist. If that's the case, simply exit ImmutableSet.Builder<ImmutableObject> entitiesToSave = new ImmutableSet.Builder<>();
// early, and ResourceMutateFlow will later throw ResourceToMutateDoesNotExistException. HistoryEntry historyEntry = buildHistory(period, existingDomain);
if (existingResource == null) { entitiesToSave.add(historyEntry);
return; DateTime automaticTransferTime = now.plus(registry.getAutomaticTransferLength());
} // The new expiration time if there is a server approval.
Registry registry = Registry.get(existingResource.getTld()); DateTime serverApproveNewExpirationTime = extendRegistrationWithCap(
automaticTransferTime = now.plus(registry.getAutomaticTransferLength()); automaticTransferTime, existingDomain.getRegistrationExpirationTime(), years);
// Note that the gaining registrar is used to calculate the cost of the renewal. ImmutableSet<BillingEvent> billingEvents = createBillingEvents(
renewCost = getDomainRenewCost(targetId, now, command.getPeriod().getValue()); renewCost,
transferBillingEvent = new BillingEvent.OneTime.Builder() registry,
.setReason(Reason.TRANSFER) existingDomain,
.setTargetId(targetId) historyEntry,
.setClientId(getClientId())
.setCost(renewCost)
.setPeriodYears(command.getPeriod().getValue())
.setEventTime(automaticTransferTime)
.setBillingTime(automaticTransferTime.plus(registry.getTransferGracePeriodLength()))
.setParent(historyEntry)
.build();
DateTime newExpirationTime = extendRegistrationWithCap(
automaticTransferTime, automaticTransferTime,
existingResource.getRegistrationExpirationTime(), serverApproveNewExpirationTime,
command.getPeriod().getValue()); years);
gainingClientAutorenewEvent = new BillingEvent.Recurring.Builder() entitiesToSave.addAll(billingEvents);
.setReason(Reason.RENEW) ImmutableSet<PollMessage> pollMessages = createPollMessages(
.setFlags(ImmutableSet.of(Flag.AUTO_RENEW)) existingDomain,
.setTargetId(targetId) historyEntry,
.setClientId(gainingClient.getId()) automaticTransferTime,
.setEventTime(newExpirationTime) serverApproveNewExpirationTime,
.setRecurrenceEndTime(END_OF_TIME) years);
.setParent(historyEntry) entitiesToSave.addAll(pollMessages);
ImmutableSet.Builder<Key<? extends TransferServerApproveEntity>> serverApproveEntities =
new ImmutableSet.Builder<>();
for (TransferServerApproveEntity entity : union(billingEvents, pollMessages)) {
serverApproveEntities.add(Key.create(entity));
}
// Create the transfer data that represents the pending transfer.
TransferData pendingTransferData = createTransferDataBuilder()
.setTransferStatus(TransferStatus.PENDING)
.setLosingClientId(existingDomain.getCurrentSponsorClientId())
.setPendingTransferExpirationTime(automaticTransferTime)
.setExtendedRegistrationYears(years)
.setServerApproveBillingEvent(Key.create(
getOnlyElement(filter(billingEvents, BillingEvent.OneTime.class))))
.setServerApproveAutorenewEvent(Key.create(
getOnlyElement(filter(billingEvents, BillingEvent.Recurring.class))))
.setServerApproveAutorenewPollMessage(Key.create(
getOnlyElement(filter(pollMessages, PollMessage.Autorenew.class))))
.setServerApproveEntities(serverApproveEntities.build())
.build(); .build();
gainingClientAutorenewPollMessage = new PollMessage.Autorenew.Builder() // When a transfer is requested, a poll message is created to notify the losing registrar.
.setTargetId(targetId) PollMessage requestPollMessage = createLosingTransferPollMessage(
.setClientId(gainingClient.getId()) targetId, pendingTransferData, serverApproveNewExpirationTime, historyEntry)
.setEventTime(newExpirationTime) .asBuilder().setEventTime(now).build();
.setAutorenewEndTime(END_OF_TIME) entitiesToSave.add(requestPollMessage);
.setMsg("Domain was auto-renewed.") // End the old autorenew event and poll message at the implicit transfer time. This may delete
.setParent(historyEntry) // the poll message if it has no events left. Note that this is still left on the domain as the
// autorenewBillingEvent because it is still the current autorenew event until the transfer
// happens. If you read the domain after the transfer occurs, then cloneProjectedAtTime() will
// move the serverApproveAutoRenewEvent into the autoRenewEvent field.
updateAutorenewRecurrenceEndTime(existingDomain, automaticTransferTime);
handleExtraFlowLogic(years, existingDomain, historyEntry);
DomainResource newDomain = existingDomain.asBuilder()
.setTransferData(pendingTransferData)
.addStatusValue(StatusValue.PENDING_TRANSFER)
.build(); .build();
extraFlowLogic = RegistryExtraFlowLogicProxy.newInstanceForDomain(existingResource); ofy().save().entities(entitiesToSave.add(newDomain).build());
return createOutput(
SUCCESS_WITH_ACTION_PENDING,
createResponse(period, existingDomain, newDomain),
createResponseExtensions(renewCost, feeTransfer));
} }
@Override private void verifyTransferAllowed(DomainResource existingDomain, Period period)
protected final void verifyTransferRequestIsAllowed() throws EppException { throws EppException {
verifyUnitIsYears(command.getPeriod()); verifyNoDisallowedStatuses(existingDomain, DISALLOWED_STATUSES);
verifyRequiredAuthInfoForResourceTransfer(authInfo, existingDomain);
// Verify that the resource does not already have a pending transfer.
if (TransferStatus.PENDING.equals(existingDomain.getTransferData().getTransferStatus())) {
throw new AlreadyPendingTransferException(targetId);
}
// Verify that this client doesn't already sponsor this resource.
if (gainingClientId.equals(existingDomain.getCurrentSponsorClientId())) {
throw new ObjectAlreadySponsoredException();
}
checkAllowedAccessToTld(getAllowedTlds(), existingDomain.getTld());
verifyUnitIsYears(period);
if (!isSuperuser) { if (!isSuperuser) {
verifyPremiumNameIsNotBlocked(targetId, now, getClientId()); verifyPremiumNameIsNotBlocked(targetId, now, gainingClientId);
}
validateFeeChallenge(
targetId, existingResource.getTld(), now, feeTransfer, renewCost);
checkAllowedAccessToTld(getAllowedTlds(), existingResource.getTld());
}
@Override
protected ImmutableList<? extends ResponseExtension> getTransferResponseExtensions() {
if (feeTransfer != null) {
return ImmutableList.of(
feeTransfer
.createResponseBuilder()
.setCurrency(renewCost.getCurrencyUnit())
.setFees(ImmutableList.of(Fee.create(renewCost.getAmount(), FeeType.RENEW)))
.build());
} else {
return null;
} }
} }
@Override private HistoryEntry buildHistory(Period period, DomainResource existingResource) {
protected void setTransferDataProperties(TransferData.Builder builder) throws EppException { return historyBuilder
builder .setType(HistoryEntry.Type.DOMAIN_TRANSFER_REQUEST)
.setServerApproveBillingEvent(Key.create(transferBillingEvent)) .setPeriod(period)
.setServerApproveAutorenewEvent(Key.create(gainingClientAutorenewEvent)) .setModificationTime(now)
.setServerApproveAutorenewPollMessage(Key.create(gainingClientAutorenewPollMessage)) .setParent(Key.create(existingResource))
.setExtendedRegistrationYears(command.getPeriod().getValue()); .build();
// Handle extra flow logic, if any.
if (extraFlowLogic.isPresent()) {
extraFlowLogic.get().performAdditionalDomainTransferLogic(
existingResource,
getClientId(),
now,
command.getPeriod().getValue(),
eppInput,
historyEntry);
}
} }
/** private ImmutableSet<BillingEvent> createBillingEvents(
* When a transfer is requested, schedule a billing event and poll message for the automatic Money renewCost,
* approval case. Registry registry,
* DomainResource existingDomain,
* <p>Note that the action time is AUTOMATIC_TRANSFER_DAYS in the future, matching the server HistoryEntry historyEntry,
* policy on automated approval of transfers. There is no equivalent grace period added; if the DateTime automaticTransferTime,
* transfer is implicitly approved, the resource will project a grace period on itself. DateTime serverApproveNewExpirationTime,
*/ int years) {
@Override ImmutableSet.Builder<BillingEvent> billingEvents = new ImmutableSet.Builder<>();
protected Set<Key<? extends TransferServerApproveEntity>> getTransferServerApproveEntities() { BillingEvent.OneTime transferBillingEvent =
ofy().save().<Object>entities( createTransferBillingEvent(years, renewCost, registry, historyEntry);
transferBillingEvent, gainingClientAutorenewEvent, gainingClientAutorenewPollMessage); BillingEvent.Recurring gainingClientAutorenewEvent = createGainingClientAutorenewEvent(
historyEntry, serverApproveNewExpirationTime);
billingEvents.add(transferBillingEvent, gainingClientAutorenewEvent);
// If there will be an autorenew between now and the automatic transfer time, and if the // If there will be an autorenew between now and the automatic transfer time, and if the
// autorenew grace period length is long enough that the domain will still be within it at the // autorenew grace period length is long enough that the domain will still be within it at the
// automatic transfer time, then the transfer will subsume the autorenew so we need to write out // automatic transfer time, then the transfer will subsume the autorenew so we need to write out
// a cancellation for it. // a cancellation for it.
Set<Key<? extends TransferServerApproveEntity>> serverApproveEntities = new HashSet<>(); DateTime oldExpirationTime = existingDomain.getRegistrationExpirationTime();
DateTime expirationTime = existingResource.getRegistrationExpirationTime(); if (automaticTransferTime.isAfter(oldExpirationTime) && automaticTransferTime.isBefore(
Registry registry = Registry.get(existingResource.getTld()); oldExpirationTime.plus(registry.getAutoRenewGracePeriodLength()))) {
if (automaticTransferTime.isAfter(expirationTime) && automaticTransferTime.isBefore( BillingEvent.Cancellation autorenewCancellation =
expirationTime.plus(registry.getAutoRenewGracePeriodLength()))) { createAutorenewCancellation(
BillingEvent.Cancellation autorenewCancellation = new BillingEvent.Cancellation.Builder() existingDomain, historyEntry, automaticTransferTime, registry);
.setReason(Reason.RENEW) billingEvents.add(autorenewCancellation);
.setFlags(ImmutableSet.of(Flag.AUTO_RENEW))
.setTargetId(targetId)
.setClientId(existingResource.getCurrentSponsorClientId())
.setEventTime(automaticTransferTime)
.setBillingTime(expirationTime.plus(registry.getAutoRenewGracePeriodLength()))
.setRecurringEventKey(existingResource.getAutorenewBillingEvent())
.setParent(historyEntry)
.build();
ofy().save().entity(autorenewCancellation);
serverApproveEntities.add(Key.create(autorenewCancellation));
} }
serverApproveEntities.add(Key.create(transferBillingEvent)); return billingEvents.build();
serverApproveEntities.add(Key.create(gainingClientAutorenewEvent));
serverApproveEntities.add(Key.create(gainingClientAutorenewPollMessage));
return serverApproveEntities;
} }
/** Close the old autorenew billing event and save a new one. */ private BillingEvent.OneTime createTransferBillingEvent(
@Override int years, Money renewCost, Registry registry, HistoryEntry historyEntry) {
protected final void modifyRelatedResources() throws EppException { DateTime automaticTransferTime = now.plus(registry.getAutomaticTransferLength());
// End the old autorenew event and poll message at the implicit transfer time. This may delete return new BillingEvent.OneTime.Builder()
// the poll message if it has no events left. .setReason(Reason.TRANSFER)
// .setTargetId(targetId)
// Note that this is still left on the domain as the autorenewBillingEvent because it is still .setClientId(gainingClientId)
// the current autorenew event until the transfer happens. If you read the domain after the .setCost(renewCost)
// transfer occurs, then the logic in cloneProjectedAtTime() will move the .setPeriodYears(years)
// serverApproveAutoRenewEvent into the autoRenewEvent field. .setEventTime(automaticTransferTime)
updateAutorenewRecurrenceEndTime(existingResource, automaticTransferTime); .setBillingTime(automaticTransferTime.plus(registry.getTransferGracePeriodLength()))
.setParent(historyEntry)
.build();
}
private BillingEvent.Recurring createGainingClientAutorenewEvent(
HistoryEntry historyEntry, DateTime serverApproveNewExpirationTime) {
return new BillingEvent.Recurring.Builder()
.setReason(Reason.RENEW)
.setFlags(ImmutableSet.of(Flag.AUTO_RENEW))
.setTargetId(targetId)
.setClientId(gainingClientId)
.setEventTime(serverApproveNewExpirationTime)
.setRecurrenceEndTime(END_OF_TIME)
.setParent(historyEntry)
.build();
}
/**
* Creates an autorenew cancellation.
*
* <p>If there will be an autorenew between now and the automatic transfer time, and if the
* autorenew grace period length is long enough that the domain will still be within it at the
* automatic transfer time, then the transfer will subsume the autorenew and we need to write out
* a cancellation for it.
*/
private BillingEvent.Cancellation createAutorenewCancellation(
DomainResource existingDomain,
HistoryEntry historyEntry,
DateTime automaticTransferTime,
Registry registry) {
return new BillingEvent.Cancellation.Builder()
.setReason(Reason.RENEW)
.setFlags(ImmutableSet.of(Flag.AUTO_RENEW))
.setTargetId(targetId)
.setClientId(existingDomain.getCurrentSponsorClientId())
.setEventTime(automaticTransferTime)
.setBillingTime(existingDomain.getRegistrationExpirationTime()
.plus(registry.getAutoRenewGracePeriodLength()))
.setRecurringEventKey(existingDomain.getAutorenewBillingEvent())
.setParent(historyEntry)
.build();
}
/** Create the message that will be sent to the gaining registrar on server approval. */
private PollMessage createServerApproveGainingPollMessage(
DomainResource existingDomain,
HistoryEntry historyEntry,
DateTime automaticTransferTime,
DateTime serverApproveNewExpirationTime,
int years) {
return createGainingTransferPollMessage(
targetId,
createTransferDataBuilder()
.setTransferStatus(TransferStatus.SERVER_APPROVED)
.setLosingClientId(existingDomain.getCurrentSponsorClientId())
.setPendingTransferExpirationTime(automaticTransferTime)
.setExtendedRegistrationYears(years)
.build(),
serverApproveNewExpirationTime,
historyEntry);
}
private ImmutableSet<PollMessage> createPollMessages(
DomainResource existingDomain,
HistoryEntry historyEntry,
DateTime automaticTransferTime,
DateTime serverApproveNewExpirationTime,
int years) {
PollMessage.Autorenew gainingClientAutorenewPollMessage =
createGainingClientAutorenewPollMessage(historyEntry, serverApproveNewExpirationTime);
PollMessage serverApproveGainingPollMessage = createServerApproveGainingPollMessage(
existingDomain, historyEntry, automaticTransferTime, serverApproveNewExpirationTime, years);
PollMessage serverApproveLosingPollMessage = createServerApproveLosingPollMessage(
existingDomain, historyEntry, automaticTransferTime, serverApproveNewExpirationTime, years);
return ImmutableSet.of(
gainingClientAutorenewPollMessage,
serverApproveGainingPollMessage,
serverApproveLosingPollMessage);
}
private PollMessage.Autorenew createGainingClientAutorenewPollMessage(
HistoryEntry historyEntry, DateTime serverApproveNewExpirationTime) {
return new PollMessage.Autorenew.Builder()
.setTargetId(targetId)
.setClientId(gainingClientId)
.setEventTime(serverApproveNewExpirationTime)
.setAutorenewEndTime(END_OF_TIME)
.setMsg("Domain was auto-renewed.")
.setParent(historyEntry)
.build();
}
/** Create the message that will be sent to the losing registrar on server approval. */
private PollMessage createServerApproveLosingPollMessage(
DomainResource existingDomain,
HistoryEntry historyEntry,
DateTime automaticTransferTime,
DateTime serverApproveNewExpirationTime,
int years) {
return createLosingTransferPollMessage(
targetId,
createTransferDataBuilder()
.setTransferStatus(TransferStatus.SERVER_APPROVED)
.setLosingClientId(existingDomain.getCurrentSponsorClientId())
.setPendingTransferExpirationTime(automaticTransferTime)
.setExtendedRegistrationYears(years)
.build(),
serverApproveNewExpirationTime,
historyEntry);
}
private Builder createTransferDataBuilder() {
return new TransferData.Builder()
.setTransferRequestTime(now)
.setGainingClientId(gainingClientId)
.setTransferRequestTrid(trid);
}
private void handleExtraFlowLogic(
int years, DomainResource existingDomain, HistoryEntry historyEntry) throws EppException {
Optional<RegistryExtraFlowLogic> extraFlowLogic =
RegistryExtraFlowLogicProxy.newInstanceForDomain(existingDomain);
if (extraFlowLogic.isPresent()) { if (extraFlowLogic.isPresent()) {
extraFlowLogic.get().performAdditionalDomainTransferLogic(
existingDomain, gainingClientId, now, years, eppInput, historyEntry);
extraFlowLogic.get().commitAdditionalLogicChanges(); extraFlowLogic.get().commitAdditionalLogicChanges();
} }
} }
@Override private DomainTransferResponse createResponse(
protected final HistoryEntry.Type getHistoryEntryType() { Period period, DomainResource existingDomain, DomainResource newDomain) {
return HistoryEntry.Type.DOMAIN_TRANSFER_REQUEST; // If the registration were approved this instant, this is what the new expiration would be,
// because we cap at 10 years from the moment of approval. This is different than the server
// approval new expiration time, which is capped at 10 years from the server approve time.
DateTime approveNowExtendedRegistrationTime = extendRegistrationWithCap(
now,
existingDomain.getRegistrationExpirationTime(),
period.getValue());
return createTransferResponse(
targetId, newDomain.getTransferData(), approveNowExtendedRegistrationTime);
} }
@Override private ImmutableList<FeeTransformResponseExtension> createResponseExtensions(Money renewCost,
protected final Period getCommandPeriod() { FeeTransformCommandExtension feeTransfer) {
return command.getPeriod(); return feeTransfer == null
? null
: ImmutableList.of(feeTransfer.createResponseBuilder()
.setCurrency(renewCost.getCurrencyUnit())
.setFees(ImmutableList.of(Fee.create(renewCost.getAmount(), FeeType.RENEW)))
.build());
} }
} }

View file

@ -58,6 +58,10 @@ public abstract class TransferResponse extends BaseTransferObject implements Res
@XmlElement(name = "exDate") @XmlElement(name = "exDate")
DateTime extendedRegistrationExpirationTime; DateTime extendedRegistrationExpirationTime;
public DateTime getExtendedRegistrationExpirationTime() {
return extendedRegistrationExpirationTime;
}
/** Builder for {@link DomainTransferResponse}. */ /** Builder for {@link DomainTransferResponse}. */
public static class Builder public static class Builder
extends BaseTransferObject.Builder<DomainTransferResponse, Builder> { extends BaseTransferObject.Builder<DomainTransferResponse, Builder> {

View file

@ -337,7 +337,7 @@ public class EppLifecycleDomainTest extends EppTestCase {
DateTime.parse("2001-01-01T00:01:00Z")); DateTime.parse("2001-01-01T00:01:00Z"));
assertCommandAndResponse( assertCommandAndResponse(
"poll_ack.xml", "poll_ack.xml",
ImmutableMap.of("ID", "1-B-EXAMPLE-17-21"), ImmutableMap.of("ID", "1-B-EXAMPLE-17-23"),
"poll_ack_response_empty.xml", "poll_ack_response_empty.xml",
null, null,
DateTime.parse("2001-01-01T00:01:00Z")); DateTime.parse("2001-01-01T00:01:00Z"));
@ -349,7 +349,7 @@ public class EppLifecycleDomainTest extends EppTestCase {
DateTime.parse("2001-01-06T00:01:00Z")); DateTime.parse("2001-01-06T00:01:00Z"));
assertCommandAndResponse( assertCommandAndResponse(
"poll_ack.xml", "poll_ack.xml",
ImmutableMap.of("ID", "1-B-EXAMPLE-17-23"), ImmutableMap.of("ID", "1-B-EXAMPLE-17-22"),
"poll_ack_response_empty.xml", "poll_ack_response_empty.xml",
null, null,
DateTime.parse("2001-01-06T00:01:00Z")); DateTime.parse("2001-01-06T00:01:00Z"));
@ -365,7 +365,7 @@ public class EppLifecycleDomainTest extends EppTestCase {
DateTime.parse("2001-01-06T00:02:00Z")); DateTime.parse("2001-01-06T00:02:00Z"));
assertCommandAndResponse( assertCommandAndResponse(
"poll_ack.xml", "poll_ack.xml",
ImmutableMap.of("ID", "1-B-EXAMPLE-17-22"), ImmutableMap.of("ID", "1-B-EXAMPLE-17-21"),
"poll_ack_response_empty.xml", "poll_ack_response_empty.xml",
null, null,
DateTime.parse("2001-01-06T00:02:00Z")); DateTime.parse("2001-01-06T00:02:00Z"));

View file

@ -304,7 +304,7 @@ public abstract class FlowTestCase<F extends Flow> extends ShardableTestCase {
} }
/** Run a flow, marshal the result to EPP, and assert that the output is as expected. */ /** Run a flow, marshal the result to EPP, and assert that the output is as expected. */
public void runFlowAssertResponse( public EppOutput runFlowAssertResponse(
CommitMode commitMode, UserPrivileges userPrivileges, String xml, String... ignoredPaths) CommitMode commitMode, UserPrivileges userPrivileges, String xml, String... ignoredPaths)
throws Exception { throws Exception {
// Always ignore the server trid, since it's generated and meaningless to flow correctness. // Always ignore the server trid, since it's generated and meaningless to flow correctness.
@ -332,15 +332,18 @@ public abstract class FlowTestCase<F extends Flow> extends ShardableTestCase {
} }
// Clear the cache so that we don't see stale results in tests. // Clear the cache so that we don't see stale results in tests.
ofy().clearSessionCache(); ofy().clearSessionCache();
return output;
} }
public void dryRunFlowAssertResponse(String xml, String... ignoredPaths) throws Exception { public EppOutput dryRunFlowAssertResponse(String xml, String... ignoredPaths) throws Exception {
List<Object> beforeEntities = ofy().load().list(); List<Object> beforeEntities = ofy().load().list();
runFlowAssertResponse(CommitMode.DRY_RUN, UserPrivileges.NORMAL, xml, ignoredPaths); EppOutput output =
runFlowAssertResponse(CommitMode.DRY_RUN, UserPrivileges.NORMAL, xml, ignoredPaths);
assertThat(ofy().load()).containsExactlyElementsIn(beforeEntities); assertThat(ofy().load()).containsExactlyElementsIn(beforeEntities);
return output;
} }
public void runFlowAssertResponse(String xml, String... ignoredPaths) throws Exception { public EppOutput runFlowAssertResponse(String xml, String... ignoredPaths) throws Exception {
runFlowAssertResponse(CommitMode.LIVE, UserPrivileges.NORMAL, xml, ignoredPaths); return runFlowAssertResponse(CommitMode.LIVE, UserPrivileges.NORMAL, xml, ignoredPaths);
} }
} }

View file

@ -14,6 +14,7 @@
package google.registry.flows.domain; package google.registry.flows.domain;
import static com.google.common.collect.Iterables.getOnlyElement;
import static com.google.common.truth.Truth.assertThat; import static com.google.common.truth.Truth.assertThat;
import static google.registry.model.EppResourceUtils.loadByUniqueId; import static google.registry.model.EppResourceUtils.loadByUniqueId;
import static google.registry.model.ofy.ObjectifyService.ofy; import static google.registry.model.ofy.ObjectifyService.ofy;
@ -39,9 +40,9 @@ import com.google.common.collect.Iterables;
import com.google.common.collect.Ordering; import com.google.common.collect.Ordering;
import google.registry.flows.ResourceFlowUtils.BadAuthInfoForResourceException; import google.registry.flows.ResourceFlowUtils.BadAuthInfoForResourceException;
import google.registry.flows.ResourceFlowUtils.ResourceNotOwnedException; import google.registry.flows.ResourceFlowUtils.ResourceNotOwnedException;
import google.registry.flows.ResourceMutateFlow.ResourceToMutateDoesNotExistException;
import google.registry.flows.ResourceMutatePendingTransferFlow.NotPendingTransferException;
import google.registry.flows.domain.DomainFlowUtils.NotAuthorizedForTldException; import google.registry.flows.domain.DomainFlowUtils.NotAuthorizedForTldException;
import google.registry.flows.exceptions.NotPendingTransferException;
import google.registry.flows.exceptions.ResourceToMutateDoesNotExistException;
import google.registry.model.EppResource; import google.registry.model.EppResource;
import google.registry.model.billing.BillingEvent; import google.registry.model.billing.BillingEvent;
import google.registry.model.billing.BillingEvent.Cancellation; import google.registry.model.billing.BillingEvent.Cancellation;
@ -61,7 +62,7 @@ import google.registry.model.poll.PollMessage;
import google.registry.model.registrar.Registrar; import google.registry.model.registrar.Registrar;
import google.registry.model.registry.Registry; import google.registry.model.registry.Registry;
import google.registry.model.reporting.HistoryEntry; import google.registry.model.reporting.HistoryEntry;
import google.registry.model.transfer.TransferResponse; import google.registry.model.transfer.TransferResponse.DomainTransferResponse;
import google.registry.model.transfer.TransferStatus; import google.registry.model.transfer.TransferStatus;
import org.joda.money.Money; import org.joda.money.Money;
import org.joda.time.DateTime; import org.joda.time.DateTime;
@ -212,12 +213,12 @@ public class DomainTransferApproveFlowTest
assertThat(gainingTransferPollMessage.getEventTime()).isEqualTo(clock.nowUtc()); assertThat(gainingTransferPollMessage.getEventTime()).isEqualTo(clock.nowUtc());
assertThat(gainingAutorenewPollMessage.getEventTime()) assertThat(gainingAutorenewPollMessage.getEventTime())
.isEqualTo(domain.getRegistrationExpirationTime()); .isEqualTo(domain.getRegistrationExpirationTime());
assertThat( DomainTransferResponse transferResponse = getOnlyElement(FluentIterable
Iterables.getOnlyElement(FluentIterable .from(gainingTransferPollMessage.getResponseData())
.from(gainingTransferPollMessage.getResponseData()) .filter(DomainTransferResponse.class));
.filter(TransferResponse.class)) assertThat(transferResponse.getTransferStatus()).isEqualTo(TransferStatus.CLIENT_APPROVED);
.getTransferStatus()) assertThat(transferResponse.getExtendedRegistrationExpirationTime())
.isEqualTo(TransferStatus.CLIENT_APPROVED); .isEqualTo(domain.getRegistrationExpirationTime());
PendingActionNotificationResponse panData = Iterables.getOnlyElement(FluentIterable PendingActionNotificationResponse panData = Iterables.getOnlyElement(FluentIterable
.from(gainingTransferPollMessage.getResponseData()) .from(gainingTransferPollMessage.getResponseData())
.filter(PendingActionNotificationResponse.class)); .filter(PendingActionNotificationResponse.class));

View file

@ -27,10 +27,10 @@ import static google.registry.util.DateTimeUtils.END_OF_TIME;
import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet; import com.google.common.collect.ImmutableSet;
import google.registry.flows.ResourceFlowUtils.BadAuthInfoForResourceException; import google.registry.flows.ResourceFlowUtils.BadAuthInfoForResourceException;
import google.registry.flows.ResourceMutateFlow.ResourceToMutateDoesNotExistException;
import google.registry.flows.ResourceMutatePendingTransferFlow.NotPendingTransferException;
import google.registry.flows.ResourceTransferCancelFlow.NotTransferInitiatorException;
import google.registry.flows.domain.DomainFlowUtils.NotAuthorizedForTldException; import google.registry.flows.domain.DomainFlowUtils.NotAuthorizedForTldException;
import google.registry.flows.exceptions.NotPendingTransferException;
import google.registry.flows.exceptions.NotTransferInitiatorException;
import google.registry.flows.exceptions.ResourceToMutateDoesNotExistException;
import google.registry.model.contact.ContactAuthInfo; import google.registry.model.contact.ContactAuthInfo;
import google.registry.model.domain.DomainAuthInfo; import google.registry.model.domain.DomainAuthInfo;
import google.registry.model.domain.DomainResource; import google.registry.model.domain.DomainResource;

View file

@ -15,6 +15,7 @@
package google.registry.flows.domain; package google.registry.flows.domain;
import static com.google.common.truth.Truth.assertThat; import static com.google.common.truth.Truth.assertThat;
import static google.registry.model.domain.DomainResource.extendRegistrationWithCap;
import static google.registry.testing.DatastoreHelper.assertBillingEvents; import static google.registry.testing.DatastoreHelper.assertBillingEvents;
import static google.registry.testing.DatastoreHelper.deleteResource; import static google.registry.testing.DatastoreHelper.deleteResource;
import static google.registry.testing.DatastoreHelper.getPollMessages; import static google.registry.testing.DatastoreHelper.getPollMessages;
@ -22,15 +23,18 @@ import static google.registry.testing.DatastoreHelper.persistResource;
import static google.registry.testing.DomainResourceSubject.assertAboutDomains; import static google.registry.testing.DomainResourceSubject.assertAboutDomains;
import google.registry.flows.ResourceFlowUtils.BadAuthInfoForResourceException; import google.registry.flows.ResourceFlowUtils.BadAuthInfoForResourceException;
import google.registry.flows.ResourceQueryFlow.ResourceToQueryDoesNotExistException; import google.registry.flows.exceptions.NoTransferHistoryToQueryException;
import google.registry.flows.ResourceTransferQueryFlow.NoTransferHistoryToQueryException; import google.registry.flows.exceptions.NotAuthorizedToViewTransferException;
import google.registry.flows.ResourceTransferQueryFlow.NotAuthorizedToViewTransferException; import google.registry.flows.exceptions.ResourceToQueryDoesNotExistException;
import google.registry.model.contact.ContactAuthInfo; import google.registry.model.contact.ContactAuthInfo;
import google.registry.model.domain.DomainAuthInfo; import google.registry.model.domain.DomainAuthInfo;
import google.registry.model.domain.DomainResource; import google.registry.model.domain.DomainResource;
import google.registry.model.eppcommon.AuthInfo.PasswordAuth; import google.registry.model.eppcommon.AuthInfo.PasswordAuth;
import google.registry.model.eppoutput.EppOutput;
import google.registry.model.reporting.HistoryEntry; import google.registry.model.reporting.HistoryEntry;
import google.registry.model.transfer.TransferResponse.DomainTransferResponse;
import google.registry.model.transfer.TransferStatus; import google.registry.model.transfer.TransferStatus;
import org.joda.time.DateTime;
import org.junit.Before; import org.junit.Before;
import org.junit.Test; import org.junit.Test;
@ -45,14 +49,16 @@ public class DomainTransferQueryFlowTest
setupDomainWithPendingTransfer(); setupDomainWithPendingTransfer();
} }
private void doSuccessfulTest(String commandFilename, String expectedXmlFilename) private void doSuccessfulTest(
throws Exception { String commandFilename,
String expectedXmlFilename,
DateTime newExpirationTime) throws Exception {
setEppInput(commandFilename); setEppInput(commandFilename);
// Replace the ROID in the xml file with the one generated in our test. // Replace the ROID in the xml file with the one generated in our test.
eppLoader.replaceAll("JD1234-REP", contact.getRepoId()); eppLoader.replaceAll("JD1234-REP", contact.getRepoId());
// Setup done; run the test. // Setup done; run the test.
assertTransactionalFlow(false); assertTransactionalFlow(false);
runFlowAssertResponse(readFile(expectedXmlFilename)); EppOutput output = runFlowAssertResponse(readFile(expectedXmlFilename));
assertAboutDomains().that(domain).hasOneHistoryEntryEachOfTypes( assertAboutDomains().that(domain).hasOneHistoryEntryEachOfTypes(
HistoryEntry.Type.DOMAIN_CREATE, HistoryEntry.Type.DOMAIN_CREATE,
HistoryEntry.Type.DOMAIN_TRANSFER_REQUEST); HistoryEntry.Type.DOMAIN_TRANSFER_REQUEST);
@ -61,10 +67,11 @@ public class DomainTransferQueryFlowTest
getGainingClientAutorenewEvent(), getGainingClientAutorenewEvent(),
getLosingClientAutorenewEvent()); getLosingClientAutorenewEvent());
// Look in the future and make sure the poll messages for implicit ack are there. // Look in the future and make sure the poll messages for implicit ack are there.
assertThat(getPollMessages("NewRegistrar", clock.nowUtc().plusYears(1))) assertThat(getPollMessages("NewRegistrar", clock.nowUtc().plusYears(1))).hasSize(1);
.hasSize(1); assertThat(getPollMessages("TheRegistrar", clock.nowUtc().plusYears(1))).hasSize(1);
assertThat(getPollMessages("TheRegistrar", clock.nowUtc().plusYears(1))) DomainTransferResponse response =
.hasSize(1); ((DomainTransferResponse) output.getResponse().getResponseData().get(0));
assertThat(response.getExtendedRegistrationExpirationTime()).isEqualTo(newExpirationTime);
} }
private void doFailingTest(String commandFilename) throws Exception { private void doFailingTest(String commandFilename) throws Exception {
@ -78,61 +85,95 @@ public class DomainTransferQueryFlowTest
@Test @Test
public void testSuccess() throws Exception { public void testSuccess() throws Exception {
doSuccessfulTest("domain_transfer_query.xml", "domain_transfer_query_response.xml"); doSuccessfulTest(
"domain_transfer_query.xml",
"domain_transfer_query_response.xml",
domain.getRegistrationExpirationTime().plusYears(1));
} }
@Test @Test
public void testSuccess_sponsoringClient() throws Exception { public void testSuccess_sponsoringClient() throws Exception {
setClientIdForFlow("TheRegistrar"); setClientIdForFlow("TheRegistrar");
doSuccessfulTest("domain_transfer_query.xml", "domain_transfer_query_response.xml"); doSuccessfulTest(
"domain_transfer_query.xml",
"domain_transfer_query_response.xml",
domain.getRegistrationExpirationTime().plusYears(1));
} }
@Test @Test
public void testSuccess_domainAuthInfo() throws Exception { public void testSuccess_domainAuthInfo() throws Exception {
setClientIdForFlow("ClientZ"); setClientIdForFlow("ClientZ");
doSuccessfulTest("domain_transfer_query_domain_authinfo.xml", doSuccessfulTest(
"domain_transfer_query_response.xml"); "domain_transfer_query_domain_authinfo.xml",
"domain_transfer_query_response.xml",
domain.getRegistrationExpirationTime().plusYears(1));
} }
@Test @Test
public void testSuccess_contactAuthInfo() throws Exception { public void testSuccess_contactAuthInfo() throws Exception {
setClientIdForFlow("ClientZ"); setClientIdForFlow("ClientZ");
doSuccessfulTest("domain_transfer_query_contact_authinfo.xml", doSuccessfulTest(
"domain_transfer_query_response.xml"); "domain_transfer_query_contact_authinfo.xml",
"domain_transfer_query_response.xml",
domain.getRegistrationExpirationTime().plusYears(1));
} }
@Test @Test
public void testSuccess_clientApproved() throws Exception { public void testSuccess_clientApproved() throws Exception {
changeTransferStatus(TransferStatus.CLIENT_APPROVED); changeTransferStatus(TransferStatus.CLIENT_APPROVED);
doSuccessfulTest("domain_transfer_query.xml", doSuccessfulTest(
"domain_transfer_query_response_client_approved.xml"); "domain_transfer_query.xml",
"domain_transfer_query_response_client_approved.xml",
domain.getRegistrationExpirationTime().plusYears(1));
} }
@Test @Test
public void testSuccess_clientRejected() throws Exception { public void testSuccess_clientRejected() throws Exception {
changeTransferStatus(TransferStatus.CLIENT_REJECTED); changeTransferStatus(TransferStatus.CLIENT_REJECTED);
doSuccessfulTest("domain_transfer_query.xml", doSuccessfulTest(
"domain_transfer_query_response_client_rejected.xml"); "domain_transfer_query.xml",
"domain_transfer_query_response_client_rejected.xml",
null);
} }
@Test @Test
public void testSuccess_clientCancelled() throws Exception { public void testSuccess_clientCancelled() throws Exception {
changeTransferStatus(TransferStatus.CLIENT_CANCELLED); changeTransferStatus(TransferStatus.CLIENT_CANCELLED);
doSuccessfulTest("domain_transfer_query.xml", doSuccessfulTest(
"domain_transfer_query_response_client_cancelled.xml"); "domain_transfer_query.xml",
"domain_transfer_query_response_client_cancelled.xml",
null);
} }
@Test @Test
public void testSuccess_serverApproved() throws Exception { public void testSuccess_serverApproved() throws Exception {
changeTransferStatus(TransferStatus.SERVER_APPROVED); changeTransferStatus(TransferStatus.SERVER_APPROVED);
doSuccessfulTest("domain_transfer_query.xml", doSuccessfulTest(
"domain_transfer_query_response_server_approved.xml"); "domain_transfer_query.xml",
"domain_transfer_query_response_server_approved.xml",
domain.getRegistrationExpirationTime().plusYears(1));
} }
@Test @Test
public void testSuccess_serverCancelled() throws Exception { public void testSuccess_serverCancelled() throws Exception {
changeTransferStatus(TransferStatus.SERVER_CANCELLED); changeTransferStatus(TransferStatus.SERVER_CANCELLED);
doSuccessfulTest("domain_transfer_query.xml", doSuccessfulTest(
"domain_transfer_query_response_server_cancelled.xml"); "domain_transfer_query.xml",
"domain_transfer_query_response_server_cancelled.xml",
null);
}
@Test
public void testSuccess_tenYears() throws Exception {
domain = persistResource(domain.asBuilder()
.setTransferData(domain.getTransferData().asBuilder()
.setExtendedRegistrationYears(10)
.build())
.build());
doSuccessfulTest(
"domain_transfer_query.xml",
"domain_transfer_query_response_10_years.xml",
extendRegistrationWithCap(clock.nowUtc(), domain.getRegistrationExpirationTime(), 10));
} }
@Test @Test
@ -140,8 +181,10 @@ public class DomainTransferQueryFlowTest
changeTransferStatus(TransferStatus.SERVER_CANCELLED); changeTransferStatus(TransferStatus.SERVER_CANCELLED);
domain = persistResource( domain = persistResource(
domain.asBuilder().setDeletionTime(clock.nowUtc().plusDays(1)).build()); domain.asBuilder().setDeletionTime(clock.nowUtc().plusDays(1)).build());
doSuccessfulTest("domain_transfer_query.xml", doSuccessfulTest(
"domain_transfer_query_response_server_cancelled.xml"); "domain_transfer_query.xml",
"domain_transfer_query_response_server_cancelled.xml",
null);
} }
@Test @Test

View file

@ -28,9 +28,9 @@ import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables; import com.google.common.collect.Iterables;
import google.registry.flows.ResourceFlowUtils.BadAuthInfoForResourceException; import google.registry.flows.ResourceFlowUtils.BadAuthInfoForResourceException;
import google.registry.flows.ResourceFlowUtils.ResourceNotOwnedException; import google.registry.flows.ResourceFlowUtils.ResourceNotOwnedException;
import google.registry.flows.ResourceMutateFlow.ResourceToMutateDoesNotExistException;
import google.registry.flows.ResourceMutatePendingTransferFlow.NotPendingTransferException;
import google.registry.flows.domain.DomainFlowUtils.NotAuthorizedForTldException; import google.registry.flows.domain.DomainFlowUtils.NotAuthorizedForTldException;
import google.registry.flows.exceptions.NotPendingTransferException;
import google.registry.flows.exceptions.ResourceToMutateDoesNotExistException;
import google.registry.model.contact.ContactAuthInfo; import google.registry.model.contact.ContactAuthInfo;
import google.registry.model.domain.DomainAuthInfo; import google.registry.model.domain.DomainAuthInfo;
import google.registry.model.domain.DomainResource; import google.registry.model.domain.DomainResource;

View file

@ -40,11 +40,6 @@ import com.google.common.collect.ImmutableSet;
import com.google.common.collect.ImmutableSortedMap; import com.google.common.collect.ImmutableSortedMap;
import com.google.common.collect.Iterables; import com.google.common.collect.Iterables;
import google.registry.flows.ResourceFlowUtils.BadAuthInfoForResourceException; import google.registry.flows.ResourceFlowUtils.BadAuthInfoForResourceException;
import google.registry.flows.ResourceMutateFlow.ResourceToMutateDoesNotExistException;
import google.registry.flows.ResourceTransferRequestFlow.AlreadyPendingTransferException;
import google.registry.flows.ResourceTransferRequestFlow.MissingTransferRequestAuthInfoException;
import google.registry.flows.ResourceTransferRequestFlow.ObjectAlreadySponsoredException;
import google.registry.flows.SingleResourceFlow.ResourceStatusProhibitsOperationException;
import google.registry.flows.domain.DomainFlowUtils.BadPeriodUnitException; import google.registry.flows.domain.DomainFlowUtils.BadPeriodUnitException;
import google.registry.flows.domain.DomainFlowUtils.CurrencyUnitMismatchException; import google.registry.flows.domain.DomainFlowUtils.CurrencyUnitMismatchException;
import google.registry.flows.domain.DomainFlowUtils.CurrencyValueScaleException; import google.registry.flows.domain.DomainFlowUtils.CurrencyValueScaleException;
@ -53,6 +48,11 @@ import google.registry.flows.domain.DomainFlowUtils.FeesRequiredForPremiumNameEx
import google.registry.flows.domain.DomainFlowUtils.NotAuthorizedForTldException; import google.registry.flows.domain.DomainFlowUtils.NotAuthorizedForTldException;
import google.registry.flows.domain.DomainFlowUtils.PremiumNameBlockedException; import google.registry.flows.domain.DomainFlowUtils.PremiumNameBlockedException;
import google.registry.flows.domain.DomainFlowUtils.UnsupportedFeeAttributeException; import google.registry.flows.domain.DomainFlowUtils.UnsupportedFeeAttributeException;
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.flows.exceptions.ResourceToMutateDoesNotExistException;
import google.registry.model.EppResource; import google.registry.model.EppResource;
import google.registry.model.billing.BillingEvent; import google.registry.model.billing.BillingEvent;
import google.registry.model.billing.BillingEvent.Cancellation.Builder; import google.registry.model.billing.BillingEvent.Cancellation.Builder;
@ -224,8 +224,7 @@ public class DomainTransferRequestFlowTest
// Two poll messages on the gaining registrar's side at the expected expiration time: a // Two poll messages on the gaining registrar's side at the expected expiration time: a
// (OneTime) transfer approved message, and an Autorenew poll message. // (OneTime) transfer approved message, and an Autorenew poll message.
assertThat(getPollMessages("NewRegistrar", expectedExpirationTime)) assertThat(getPollMessages("NewRegistrar", expectedExpirationTime)).hasSize(2);
.hasSize(2);
PollMessage transferApprovedPollMessage = getOnlyPollMessage( PollMessage transferApprovedPollMessage = getOnlyPollMessage(
"NewRegistrar", implicitTransferTime, PollMessage.OneTime.class); "NewRegistrar", implicitTransferTime, PollMessage.OneTime.class);
PollMessage autorenewPollMessage = getOnlyPollMessage( PollMessage autorenewPollMessage = getOnlyPollMessage(

View file

@ -0,0 +1,23 @@
<epp xmlns="urn:ietf:params:xml:ns:epp-1.0">
<response>
<result code="1000">
<msg>Command completed successfully</msg>
</result>
<resData>
<domain:trnData
xmlns:domain="urn:ietf:params:xml:ns:domain-1.0">
<domain:name>example.tld</domain:name>
<domain:trStatus>pending</domain:trStatus>
<domain:reID>NewRegistrar</domain:reID>
<domain:reDate>2000-06-06T22:00:00.0Z</domain:reDate>
<domain:acID>TheRegistrar</domain:acID>
<domain:acDate>2000-06-11T22:00:00.0Z</domain:acDate>
<domain:exDate>2010-06-09T22:00:00.0Z</domain:exDate>
</domain:trnData>
</resData>
<trID>
<clTRID>ABC-12345</clTRID>
<svTRID>server-trid</svTRID>
</trID>
</response>
</epp>

View file

@ -3,7 +3,7 @@
<result code="1301"> <result code="1301">
<msg>Command completed successfully; ack to dequeue</msg> <msg>Command completed successfully; ack to dequeue</msg>
</result> </result>
<msgQ count="1" id="1-B-EXAMPLE-17-21"> <msgQ count="1" id="1-B-EXAMPLE-17-23">
<qDate>2001-01-01T00:00:00Z</qDate> <qDate>2001-01-01T00:00:00Z</qDate>
<msg>Transfer requested.</msg> <msg>Transfer requested.</msg>
</msgQ> </msgQ>

View file

@ -3,7 +3,7 @@
<result code="1301"> <result code="1301">
<msg>Command completed successfully; ack to dequeue</msg> <msg>Command completed successfully; ack to dequeue</msg>
</result> </result>
<msgQ count="1" id="1-B-EXAMPLE-17-23"> <msgQ count="1" id="1-B-EXAMPLE-17-22">
<qDate>2001-01-06T00:00:00Z</qDate> <qDate>2001-01-06T00:00:00Z</qDate>
<msg>Transfer approved.</msg> <msg>Transfer approved.</msg>
</msgQ> </msgQ>

View file

@ -4,7 +4,7 @@
<result code="1301"> <result code="1301">
<msg>Command completed successfully; ack to dequeue</msg> <msg>Command completed successfully; ack to dequeue</msg>
</result> </result>
<msgQ count="1" id="1-B-EXAMPLE-17-22"> <msgQ count="1" id="1-B-EXAMPLE-17-21">
<qDate>2001-01-06T00:00:00Z</qDate> <qDate>2001-01-06T00:00:00Z</qDate>
<msg>Transfer approved.</msg> <msg>Transfer approved.</msg>
</msgQ> </msgQ>