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.InvalidAuthorizationInformationErrorException;
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.ResourceStatusProhibitsOperationException;
import google.registry.flows.exceptions.ResourceToDeleteIsReferencedException;
@ -140,14 +142,14 @@ public class ResourceFlowUtils {
*/
@SuppressWarnings("unchecked")
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, ?>>) existingResource.asBuilder()
(Builder<R, ? extends Builder<R, ?>>) resource.asBuilder()
.setDeletionTime(now)
.setStatusValues(null)
.setTransferData(
existingResource.getStatusValues().contains(StatusValue.PENDING_TRANSFER)
? existingResource.getTransferData().asBuilder()
resource.getStatusValues().contains(StatusValue.PENDING_TRANSFER)
? resource.getTransferData().asBuilder()
.setTransferStatus(TransferStatus.SERVER_CANCELLED)
.setServerApproveEntities(null)
.setServerApproveBillingEvent(null)
@ -155,7 +157,7 @@ public class ResourceFlowUtils {
.setServerApproveAutorenewPollMessage(null)
.setPendingTransferExpirationTime(null)
.build()
: existingResource.getTransferData())
: resource.getTransferData())
.wipeOut();
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. */
public static <R extends EppResource> void handlePendingTransferOnDelete(
R existingResource, R newResource, DateTime now, HistoryEntry historyEntry) {
if (existingResource.getStatusValues().contains(StatusValue.PENDING_TRANSFER)) {
TransferData oldTransferData = existingResource.getTransferData();
R resource, R newResource, DateTime now, HistoryEntry historyEntry) {
if (resource.getStatusValues().contains(StatusValue.PENDING_TRANSFER)) {
TransferData oldTransferData = resource.getTransferData();
ofy().delete().keys(oldTransferData.getServerApproveEntities());
ofy().save().entity(new PollMessage.OneTime.Builder()
.setClientId(oldTransferData.getGainingClientId())
@ -180,7 +182,7 @@ public class ResourceFlowUtils {
.setResponseData(ImmutableList.of(
createTransferResponse(newResource, newResource.getTransferData(), now),
createPendingTransferNotificationResponse(
existingResource, oldTransferData.getTransferRequestTrid(), false, now)))
resource, oldTransferData.getTransferRequestTrid(), false, now)))
.setParent(historyEntry)
.build());
}
@ -275,22 +277,29 @@ public class ResourceFlowUtils {
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(
Class<R> clazz, String targetId, DateTime now) throws ResourceToQueryDoesNotExistException {
R existingResource = loadByUniqueId(clazz, targetId, now);
if (existingResource == null) {
R resource = loadByUniqueId(clazz, targetId, now);
if (resource == null) {
throw new ResourceToQueryDoesNotExistException(clazz, targetId);
}
return existingResource;
return resource;
}
public static <R extends EppResource> R loadResourceToMutate(
Class<R> clazz, String targetId, DateTime now) throws ResourceToMutateDoesNotExistException {
R existingResource = loadByUniqueId(clazz, targetId, now);
if (existingResource == null) {
R resource = loadByUniqueId(clazz, targetId, now);
if (resource == null) {
throw new ResourceToMutateDoesNotExistException(clazz, targetId);
}
return existingResource;
return resource;
}
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. */
public static class ResourceNotOwnedException extends AuthorizationErrorException {
public ResourceNotOwnedException() {
@ -317,11 +333,11 @@ public class ResourceFlowUtils {
/** Check that the given AuthInfo is present and valid for a resource being transferred. */
public static void verifyRequiredAuthInfoForResourceTransfer(
Optional<AuthInfo> authInfo, ContactResource existingContact) throws EppException {
Optional<AuthInfo> authInfo, EppResource existingResource) throws EppException {
if (!authInfo.isPresent()) {
throw new MissingTransferRequestAuthInfoException();
}
verifyOptionalAuthInfoForResource(authInfo, existingContact);
verifyOptionalAuthInfoForResource(authInfo, existingResource);
}
/** 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.ProtectedMark;
import google.registry.model.mark.Trademark;
import google.registry.model.poll.PendingActionNotificationResponse.DomainPendingActionNotificationResponse;
import google.registry.model.poll.PollMessage;
import google.registry.model.registrar.Registrar;
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.SignedMark;
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.CertificateSignatureException;
import google.registry.util.Idn;
@ -283,12 +286,10 @@ public class DomainFlowUtils {
if (!whitelist.isEmpty() && count == 0) {
throw new NameserversNotSpecifiedException();
}
if (count > MAX_NAMESERVERS_PER_DOMAIN) {
throw new TooManyNameserversException(String.format(
"Only %d nameservers are allowed per domain", MAX_NAMESERVERS_PER_DOMAIN));
}
}
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. */
static class Base64RequiredForEncodedSignedMarksException
extends ParameterValuePolicyErrorException {

View file

@ -14,86 +14,122 @@
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.createGainingTransferPollMessage;
import static google.registry.flows.domain.DomainFlowUtils.createTransferResponse;
import static google.registry.flows.domain.DomainFlowUtils.updateAutorenewRecurrenceEndTime;
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.pricing.PricingEngineProxy.getDomainRenewCost;
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.collect.FluentIterable;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
import com.googlecode.objectify.Key;
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.Flag;
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.Builder;
import google.registry.model.domain.GracePeriod;
import google.registry.model.domain.metadata.MetadataExtension;
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.registry.Registry;
import google.registry.model.reporting.HistoryEntry;
import google.registry.model.transfer.TransferData;
import google.registry.model.transfer.TransferStatus;
import javax.inject.Inject;
import org.joda.time.DateTime;
/**
* 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
* {@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.ResourceNotOwnedException}
* @error {@link google.registry.flows.ResourceMutateFlow.ResourceToMutateDoesNotExistException}
* @error {@link google.registry.flows.ResourceMutatePendingTransferFlow.NotPendingTransferException}
* @error {@link google.registry.flows.exceptions.NotPendingTransferException}
* @error {@link google.registry.flows.exceptions.ResourceToMutateDoesNotExistException}
* @error {@link DomainFlowUtils.NotAuthorizedForTldException}
*/
public class DomainTransferApproveFlow extends
ResourceTransferApproveFlow<DomainResource, Builder, Transfer> {
public final class DomainTransferApproveFlow extends LoggedInFlow implements TransactionalFlow {
@Inject Optional<AuthInfo> authInfo;
@Inject @ClientId String clientId;
@Inject @TargetId String targetId;
@Inject HistoryEntry.Builder historyBuilder;
@Inject DomainTransferApproveFlow() {}
@Override
protected void verifyOwnedResourcePendingTransferMutationAllowed() throws EppException {
checkAllowedAccessToTld(getAllowedTlds(), existingResource.getTld());
protected final void initLoggedInFlow() throws EppException {
registerExtensions(MetadataExtension.class);
}
@Override
protected final void setTransferApproveProperties(Builder builder) {
TransferData transferData = existingResource.getTransferData();
public final EppOutput run() throws EppException {
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 tld = existingResource.getTld();
int extraYears = transferData.getExtendedRegistrationYears();
// Bill for the transfer.
BillingEvent.OneTime billingEvent =
new BillingEvent.OneTime.Builder()
BillingEvent.OneTime billingEvent = new BillingEvent.OneTime.Builder()
.setReason(Reason.TRANSFER)
.setTargetId(targetId)
.setClientId(gainingClientId)
.setPeriodYears(extraYears)
.setCost(
getDomainRenewCost(targetId, transferData.getTransferRequestTime(), extraYears))
.setCost(getDomainRenewCost(targetId, transferData.getTransferRequestTime(), extraYears))
.setEventTime(now)
.setBillingTime(now.plus(Registry.get(tld).getTransferGracePeriodLength()))
.setParent(historyEntry)
.build();
ofy().save().entity(billingEvent);
// 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.
GracePeriod autorenewGrace = Iterables.getOnlyElement(FluentIterable
.from(existingResource.getGracePeriods())
.filter(new Predicate<GracePeriod>(){
GracePeriod autorenewGrace = getOnlyElement(
filter(
existingDomain.getGracePeriods(),
new Predicate<GracePeriod>() {
@Override
public boolean apply(GracePeriod gracePeriod) {
return GracePeriodStatus.AUTO_RENEW.equals(gracePeriod.getType());
}}), null);
}}),
null);
if (autorenewGrace != null) {
extraYears--;
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
// up deleting the poll message.
updateAutorenewRecurrenceEndTime(existingResource, now);
updateAutorenewRecurrenceEndTime(existingDomain, now);
DateTime newExpirationTime = extendRegistrationWithCap(
now, existingResource.getRegistrationExpirationTime(), extraYears);
now, existingDomain.getRegistrationExpirationTime(), extraYears);
// Create a new autorenew event starting at the expiration time.
BillingEvent.Recurring autorenewEvent = new BillingEvent.Recurring.Builder()
.setReason(Reason.RENEW)
@ -114,7 +150,6 @@ public class DomainTransferApproveFlow extends
.setRecurrenceEndTime(END_OF_TIME)
.setParent(historyEntry)
.build();
ofy().save().entity(autorenewEvent);
// Create a new autorenew poll message.
PollMessage.Autorenew gainingClientAutorenewPollMessage = new PollMessage.Autorenew.Builder()
.setTargetId(targetId)
@ -124,18 +159,35 @@ public class DomainTransferApproveFlow extends
.setMsg("Domain was auto-renewed.")
.setParent(historyEntry)
.build();
ofy().save().entity(gainingClientAutorenewPollMessage);
builder
DomainResource newDomain =
approvePendingTransfer(existingDomain, TransferStatus.CLIENT_APPROVED, now)
.asBuilder()
.setRegistrationExpirationTime(newExpirationTime)
.setAutorenewBillingEvent(Key.create(autorenewEvent))
.setAutorenewPollMessage(Key.create(gainingClientAutorenewPollMessage))
// Remove all the old grace periods and add a new one for the transfer.
.setGracePeriods(ImmutableSet.of(
GracePeriod.forBillingEvent(GracePeriodStatus.TRANSFER, billingEvent)));
}
@Override
protected final HistoryEntry.Type getHistoryEntryType() {
return HistoryEntry.Type.DOMAIN_TRANSFER_APPROVE;
GracePeriod.forBillingEvent(GracePeriodStatus.TRANSFER, billingEvent)))
.build();
// Create a poll message for the gaining client.
PollMessage gainingClientPollMessage = createGainingTransferPollMessage(
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;
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.createLosingTransferPollMessage;
import static google.registry.flows.domain.DomainFlowUtils.createTransferResponse;
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 com.google.common.base.Optional;
import com.googlecode.objectify.Key;
import google.registry.flows.EppException;
import google.registry.flows.ResourceTransferCancelFlow;
import google.registry.model.domain.DomainCommand.Transfer;
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.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.transfer.TransferStatus;
import javax.inject.Inject;
/**
* 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.ResourceMutateFlow.ResourceToMutateDoesNotExistException}
* @error {@link google.registry.flows.ResourceMutatePendingTransferFlow.NotPendingTransferException}
* @error {@link google.registry.flows.ResourceTransferCancelFlow.NotTransferInitiatorException}
* @error {@link google.registry.flows.exceptions.NotPendingTransferException}
* @error {@link google.registry.flows.exceptions.NotTransferInitiatorException}
* @error {@link google.registry.flows.exceptions.ResourceToMutateDoesNotExistException}
* @error {@link DomainFlowUtils.NotAuthorizedForTldException}
*/
public class DomainTransferCancelFlow
extends ResourceTransferCancelFlow<DomainResource, Builder, Transfer> {
public final class DomainTransferCancelFlow extends LoggedInFlow implements TransactionalFlow {
@Inject Optional<AuthInfo> authInfo;
@Inject @ClientId String clientId;
@Inject @TargetId String targetId;
@Inject HistoryEntry.Builder historyBuilder;
@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
protected final void modifyRelatedResourcesForTransferCancel() {
updateAutorenewRecurrenceEndTime(existingResource, END_OF_TIME);
protected final void initLoggedInFlow() throws EppException {
registerExtensions(MetadataExtension.class);
}
@Override
protected void verifyTransferCancelMutationAllowed() throws EppException {
checkAllowedAccessToTld(getAllowedTlds(), existingResource.getTld());
}
@Override
protected final HistoryEntry.Type getHistoryEntryType() {
return HistoryEntry.Type.DOMAIN_TRANSFER_CANCEL;
public final EppOutput run() throws EppException {
DomainResource existingDomain = loadResourceToMutate(DomainResource.class, targetId, now);
verifyOptionalAuthInfoForResource(authInfo, existingDomain);
verifyHasPendingTransfer(existingDomain);
verifyIsGainingRegistrar(existingDomain, clientId);
checkAllowedAccessToTld(getAllowedTlds(), existingDomain.getTld());
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;
import google.registry.flows.ResourceTransferQueryFlow;
import google.registry.model.domain.DomainCommand.Transfer;
import static google.registry.flows.ResourceFlowUtils.loadResourceForQuery;
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.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 org.joda.time.DateTime;
/**
* 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.ResourceQueryFlow.ResourceToQueryDoesNotExistException}
* @error {@link google.registry.flows.ResourceTransferQueryFlow.NoTransferHistoryToQueryException}
* @error {@link google.registry.flows.ResourceTransferQueryFlow.NotAuthorizedToViewTransferException}
* @error {@link google.registry.flows.exceptions.NoTransferHistoryToQueryException}
* @error {@link google.registry.flows.exceptions.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() {}
@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;
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.createGainingTransferPollMessage;
import static google.registry.flows.domain.DomainFlowUtils.createTransferResponse;
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 com.google.common.base.Optional;
import com.googlecode.objectify.Key;
import google.registry.flows.EppException;
import google.registry.flows.ResourceTransferRejectFlow;
import google.registry.model.domain.DomainCommand.Transfer;
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.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.transfer.TransferStatus;
import javax.inject.Inject;
/**
* 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.ResourceNotOwnedException}
* @error {@link google.registry.flows.ResourceMutateFlow.ResourceToMutateDoesNotExistException}
* @error {@link google.registry.flows.ResourceMutatePendingTransferFlow.NotPendingTransferException}
* @error {@link google.registry.flows.exceptions.NotPendingTransferException}
* @error {@link google.registry.flows.exceptions.ResourceToMutateDoesNotExistException}
* @error {@link DomainFlowUtils.NotAuthorizedForTldException}
*/
public class DomainTransferRejectFlow
extends ResourceTransferRejectFlow<DomainResource, Builder, Transfer> {
public final class DomainTransferRejectFlow extends LoggedInFlow implements TransactionalFlow {
@Inject Optional<AuthInfo> authInfo;
@Inject @ClientId String clientId;
@Inject @TargetId String targetId;
@Inject HistoryEntry.Builder historyBuilder;
@Inject DomainTransferRejectFlow() {}
@Override
protected void verifyOwnedResourcePendingTransferMutationAllowed() throws EppException {
checkAllowedAccessToTld(getAllowedTlds(), existingResource.getTld());
}
/**
* 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);
protected final void initLoggedInFlow() throws EppException {
registerExtensions(MetadataExtension.class);
}
@Override
protected final HistoryEntry.Type getHistoryEntryType() {
return HistoryEntry.Type.DOMAIN_TRANSFER_REJECT;
public final EppOutput run() throws EppException {
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;
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.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.validateFeeChallenge;
import static google.registry.flows.domain.DomainFlowUtils.verifyPremiumNameIsNotBlocked;
import static google.registry.flows.domain.DomainFlowUtils.verifyUnitIsYears;
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.eppoutput.Result.Code.SUCCESS_WITH_ACTION_PENDING;
import static google.registry.model.ofy.ObjectifyService.ofy;
import static google.registry.pricing.PricingEngineProxy.getDomainRenewCost;
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.googlecode.objectify.Key;
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.Flag;
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.Fee;
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.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.registry.Registry;
import google.registry.model.reporting.HistoryEntry;
import google.registry.model.transfer.TransferData;
import google.registry.model.transfer.TransferData.Builder;
import google.registry.model.transfer.TransferData.TransferServerApproveEntity;
import java.util.HashSet;
import java.util.Set;
import google.registry.model.transfer.TransferResponse.DomainTransferResponse;
import google.registry.model.transfer.TransferStatus;
import javax.inject.Inject;
import org.joda.money.Money;
import org.joda.time.DateTime;
import org.joda.time.Duration;
/**
* 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.ResourceMutateFlow.ResourceToMutateDoesNotExistException}
* @error {@link google.registry.flows.ResourceTransferRequestFlow.AlreadyPendingTransferException}
* @error {@link google.registry.flows.ResourceTransferRequestFlow.MissingTransferRequestAuthInfoException}
* @error {@link google.registry.flows.ResourceTransferRequestFlow.ObjectAlreadySponsoredException}
* @error {@link google.registry.flows.SingleResourceFlow.ResourceStatusProhibitsOperationException}
* @error {@link google.registry.flows.exceptions.AlreadyPendingTransferException}
* @error {@link google.registry.flows.exceptions.MissingTransferRequestAuthInfoException}
* @error {@link google.registry.flows.exceptions.ObjectAlreadySponsoredException}
* @error {@link google.registry.flows.exceptions.ResourceStatusProhibitsOperationException}
* @error {@link google.registry.flows.exceptions.ResourceToMutateDoesNotExistException}
* @error {@link DomainFlowUtils.BadPeriodUnitException}
* @error {@link DomainFlowUtils.CurrencyUnitMismatchException}
* @error {@link DomainFlowUtils.CurrencyValueScaleException}
* @error {@link DomainFlowUtils.FeesMismatchException}
* @error {@link DomainFlowUtils.FeesRequiredForPremiumNameException}
* @error {@link DomainFlowUtils.NotAuthorizedForTldException}
* @error {@link DomainFlowUtils.PremiumNameBlockedException}
* @error {@link DomainFlowUtils.UnsupportedFeeAttributeException}
*/
public class DomainTransferRequestFlow
extends ResourceTransferRequestFlow<DomainResource, Transfer> {
public final class DomainTransferRequestFlow extends LoggedInFlow implements TransactionalFlow {
/** The time when the transfer will be server approved if no other action happens first. */
private DateTime automaticTransferTime;
/** A new one-time billing event for the renewal packaged as part of this transfer. */
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;
private static final ImmutableSet<StatusValue> DISALLOWED_STATUSES = ImmutableSet.of(
StatusValue.CLIENT_TRANSFER_PROHIBITED,
StatusValue.PENDING_DELETE,
StatusValue.SERVER_TRANSFER_PROHIBITED);
@Inject ResourceCommand resourceCommand;
@Inject Optional<AuthInfo> authInfo;
@Inject @ClientId String gainingClientId;
@Inject @TargetId String targetId;
@Inject HistoryEntry.Builder historyBuilder;
@Inject DomainTransferRequestFlow() {}
@Override
protected Duration getAutomaticTransferLength() {
return Registry.get(existingResource.getTld()).getAutomaticTransferLength();
}
@Override
protected final void initResourceTransferRequestFlow() throws EppException {
registerExtensions(FlagsTransferCommandExtension.class);
protected final void initLoggedInFlow() throws EppException {
registerExtensions(MetadataExtension.class, FlagsTransferCommandExtension.class);
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);
// The "existingResource" field is loaded before this function is called, but it may be null if
// the domain name specified is invalid or doesn't exist. If that's the case, simply exit
// early, and ResourceMutateFlow will later throw ResourceToMutateDoesNotExistException.
if (existingResource == null) {
return;
}
Registry registry = Registry.get(existingResource.getTld());
automaticTransferTime = now.plus(registry.getAutomaticTransferLength());
// Note that the gaining registrar is used to calculate the cost of the renewal.
renewCost = getDomainRenewCost(targetId, now, command.getPeriod().getValue());
transferBillingEvent = new BillingEvent.OneTime.Builder()
.setReason(Reason.TRANSFER)
.setTargetId(targetId)
.setClientId(getClientId())
.setCost(renewCost)
.setPeriodYears(command.getPeriod().getValue())
.setEventTime(automaticTransferTime)
.setBillingTime(automaticTransferTime.plus(registry.getTransferGracePeriodLength()))
.setParent(historyEntry)
.build();
DateTime newExpirationTime = extendRegistrationWithCap(
validateFeeChallenge(targetId, tld, now, feeTransfer, renewCost);
ImmutableSet.Builder<ImmutableObject> entitiesToSave = new ImmutableSet.Builder<>();
HistoryEntry historyEntry = buildHistory(period, existingDomain);
entitiesToSave.add(historyEntry);
DateTime automaticTransferTime = now.plus(registry.getAutomaticTransferLength());
// The new expiration time if there is a server approval.
DateTime serverApproveNewExpirationTime = extendRegistrationWithCap(
automaticTransferTime, existingDomain.getRegistrationExpirationTime(), years);
ImmutableSet<BillingEvent> billingEvents = createBillingEvents(
renewCost,
registry,
existingDomain,
historyEntry,
automaticTransferTime,
existingResource.getRegistrationExpirationTime(),
command.getPeriod().getValue());
gainingClientAutorenewEvent = new BillingEvent.Recurring.Builder()
.setReason(Reason.RENEW)
.setFlags(ImmutableSet.of(Flag.AUTO_RENEW))
.setTargetId(targetId)
.setClientId(gainingClient.getId())
.setEventTime(newExpirationTime)
.setRecurrenceEndTime(END_OF_TIME)
.setParent(historyEntry)
serverApproveNewExpirationTime,
years);
entitiesToSave.addAll(billingEvents);
ImmutableSet<PollMessage> pollMessages = createPollMessages(
existingDomain,
historyEntry,
automaticTransferTime,
serverApproveNewExpirationTime,
years);
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();
gainingClientAutorenewPollMessage = new PollMessage.Autorenew.Builder()
.setTargetId(targetId)
.setClientId(gainingClient.getId())
.setEventTime(newExpirationTime)
.setAutorenewEndTime(END_OF_TIME)
.setMsg("Domain was auto-renewed.")
.setParent(historyEntry)
// When a transfer is requested, a poll message is created to notify the losing registrar.
PollMessage requestPollMessage = createLosingTransferPollMessage(
targetId, pendingTransferData, serverApproveNewExpirationTime, historyEntry)
.asBuilder().setEventTime(now).build();
entitiesToSave.add(requestPollMessage);
// End the old autorenew event and poll message at the implicit transfer time. This may delete
// 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();
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
protected final void verifyTransferRequestIsAllowed() throws EppException {
verifyUnitIsYears(command.getPeriod());
private void verifyTransferAllowed(DomainResource existingDomain, Period period)
throws EppException {
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) {
verifyPremiumNameIsNotBlocked(targetId, now, getClientId());
}
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;
verifyPremiumNameIsNotBlocked(targetId, now, gainingClientId);
}
}
@Override
protected void setTransferDataProperties(TransferData.Builder builder) throws EppException {
builder
.setServerApproveBillingEvent(Key.create(transferBillingEvent))
.setServerApproveAutorenewEvent(Key.create(gainingClientAutorenewEvent))
.setServerApproveAutorenewPollMessage(Key.create(gainingClientAutorenewPollMessage))
.setExtendedRegistrationYears(command.getPeriod().getValue());
// Handle extra flow logic, if any.
if (extraFlowLogic.isPresent()) {
extraFlowLogic.get().performAdditionalDomainTransferLogic(
existingResource,
getClientId(),
now,
command.getPeriod().getValue(),
eppInput,
historyEntry);
}
private HistoryEntry buildHistory(Period period, DomainResource existingResource) {
return historyBuilder
.setType(HistoryEntry.Type.DOMAIN_TRANSFER_REQUEST)
.setPeriod(period)
.setModificationTime(now)
.setParent(Key.create(existingResource))
.build();
}
/**
* When a transfer is requested, schedule a billing event and poll message for the automatic
* approval case.
*
* <p>Note that the action time is AUTOMATIC_TRANSFER_DAYS in the future, matching the server
* policy on automated approval of transfers. There is no equivalent grace period added; if the
* transfer is implicitly approved, the resource will project a grace period on itself.
*/
@Override
protected Set<Key<? extends TransferServerApproveEntity>> getTransferServerApproveEntities() {
ofy().save().<Object>entities(
transferBillingEvent, gainingClientAutorenewEvent, gainingClientAutorenewPollMessage);
private ImmutableSet<BillingEvent> createBillingEvents(
Money renewCost,
Registry registry,
DomainResource existingDomain,
HistoryEntry historyEntry,
DateTime automaticTransferTime,
DateTime serverApproveNewExpirationTime,
int years) {
ImmutableSet.Builder<BillingEvent> billingEvents = new ImmutableSet.Builder<>();
BillingEvent.OneTime transferBillingEvent =
createTransferBillingEvent(years, renewCost, registry, historyEntry);
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
// 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
// a cancellation for it.
Set<Key<? extends TransferServerApproveEntity>> serverApproveEntities = new HashSet<>();
DateTime expirationTime = existingResource.getRegistrationExpirationTime();
Registry registry = Registry.get(existingResource.getTld());
if (automaticTransferTime.isAfter(expirationTime) && automaticTransferTime.isBefore(
expirationTime.plus(registry.getAutoRenewGracePeriodLength()))) {
BillingEvent.Cancellation autorenewCancellation = new BillingEvent.Cancellation.Builder()
DateTime oldExpirationTime = existingDomain.getRegistrationExpirationTime();
if (automaticTransferTime.isAfter(oldExpirationTime) && automaticTransferTime.isBefore(
oldExpirationTime.plus(registry.getAutoRenewGracePeriodLength()))) {
BillingEvent.Cancellation autorenewCancellation =
createAutorenewCancellation(
existingDomain, historyEntry, automaticTransferTime, registry);
billingEvents.add(autorenewCancellation);
}
return billingEvents.build();
}
private BillingEvent.OneTime createTransferBillingEvent(
int years, Money renewCost, Registry registry, HistoryEntry historyEntry) {
DateTime automaticTransferTime = now.plus(registry.getAutomaticTransferLength());
return new BillingEvent.OneTime.Builder()
.setReason(Reason.TRANSFER)
.setTargetId(targetId)
.setClientId(gainingClientId)
.setCost(renewCost)
.setPeriodYears(years)
.setEventTime(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(existingResource.getCurrentSponsorClientId())
.setEventTime(automaticTransferTime)
.setBillingTime(expirationTime.plus(registry.getAutoRenewGracePeriodLength()))
.setRecurringEventKey(existingResource.getAutorenewBillingEvent())
.setClientId(gainingClientId)
.setEventTime(serverApproveNewExpirationTime)
.setRecurrenceEndTime(END_OF_TIME)
.setParent(historyEntry)
.build();
ofy().save().entity(autorenewCancellation);
serverApproveEntities.add(Key.create(autorenewCancellation));
}
serverApproveEntities.add(Key.create(transferBillingEvent));
serverApproveEntities.add(Key.create(gainingClientAutorenewEvent));
serverApproveEntities.add(Key.create(gainingClientAutorenewPollMessage));
return serverApproveEntities;
}
/** Close the old autorenew billing event and save a new one. */
@Override
protected final void modifyRelatedResources() throws EppException {
// End the old autorenew event and poll message at the implicit transfer time. This may delete
// 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 the logic in cloneProjectedAtTime() will move the
// serverApproveAutoRenewEvent into the autoRenewEvent field.
updateAutorenewRecurrenceEndTime(existingResource, automaticTransferTime);
/**
* 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()) {
extraFlowLogic.get().performAdditionalDomainTransferLogic(
existingDomain, gainingClientId, now, years, eppInput, historyEntry);
extraFlowLogic.get().commitAdditionalLogicChanges();
}
}
@Override
protected final HistoryEntry.Type getHistoryEntryType() {
return HistoryEntry.Type.DOMAIN_TRANSFER_REQUEST;
private DomainTransferResponse createResponse(
Period period, DomainResource existingDomain, DomainResource newDomain) {
// 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
protected final Period getCommandPeriod() {
return command.getPeriod();
private ImmutableList<FeeTransformResponseExtension> createResponseExtensions(Money renewCost,
FeeTransformCommandExtension feeTransfer) {
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")
DateTime extendedRegistrationExpirationTime;
public DateTime getExtendedRegistrationExpirationTime() {
return extendedRegistrationExpirationTime;
}
/** Builder for {@link DomainTransferResponse}. */
public static class Builder
extends BaseTransferObject.Builder<DomainTransferResponse, Builder> {

View file

@ -337,7 +337,7 @@ public class EppLifecycleDomainTest extends EppTestCase {
DateTime.parse("2001-01-01T00:01:00Z"));
assertCommandAndResponse(
"poll_ack.xml",
ImmutableMap.of("ID", "1-B-EXAMPLE-17-21"),
ImmutableMap.of("ID", "1-B-EXAMPLE-17-23"),
"poll_ack_response_empty.xml",
null,
DateTime.parse("2001-01-01T00:01:00Z"));
@ -349,7 +349,7 @@ public class EppLifecycleDomainTest extends EppTestCase {
DateTime.parse("2001-01-06T00:01:00Z"));
assertCommandAndResponse(
"poll_ack.xml",
ImmutableMap.of("ID", "1-B-EXAMPLE-17-23"),
ImmutableMap.of("ID", "1-B-EXAMPLE-17-22"),
"poll_ack_response_empty.xml",
null,
DateTime.parse("2001-01-06T00:01:00Z"));
@ -365,7 +365,7 @@ public class EppLifecycleDomainTest extends EppTestCase {
DateTime.parse("2001-01-06T00:02:00Z"));
assertCommandAndResponse(
"poll_ack.xml",
ImmutableMap.of("ID", "1-B-EXAMPLE-17-22"),
ImmutableMap.of("ID", "1-B-EXAMPLE-17-21"),
"poll_ack_response_empty.xml",
null,
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. */
public void runFlowAssertResponse(
public EppOutput runFlowAssertResponse(
CommitMode commitMode, UserPrivileges userPrivileges, String xml, String... ignoredPaths)
throws Exception {
// 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.
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();
EppOutput output =
runFlowAssertResponse(CommitMode.DRY_RUN, UserPrivileges.NORMAL, xml, ignoredPaths);
assertThat(ofy().load()).containsExactlyElementsIn(beforeEntities);
return output;
}
public void runFlowAssertResponse(String xml, String... ignoredPaths) throws Exception {
runFlowAssertResponse(CommitMode.LIVE, UserPrivileges.NORMAL, xml, ignoredPaths);
public EppOutput runFlowAssertResponse(String xml, String... ignoredPaths) throws Exception {
return runFlowAssertResponse(CommitMode.LIVE, UserPrivileges.NORMAL, xml, ignoredPaths);
}
}

View file

@ -14,6 +14,7 @@
package google.registry.flows.domain;
import static com.google.common.collect.Iterables.getOnlyElement;
import static com.google.common.truth.Truth.assertThat;
import static google.registry.model.EppResourceUtils.loadByUniqueId;
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 google.registry.flows.ResourceFlowUtils.BadAuthInfoForResourceException;
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.exceptions.NotPendingTransferException;
import google.registry.flows.exceptions.ResourceToMutateDoesNotExistException;
import google.registry.model.EppResource;
import google.registry.model.billing.BillingEvent;
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.registry.Registry;
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 org.joda.money.Money;
import org.joda.time.DateTime;
@ -212,12 +213,12 @@ public class DomainTransferApproveFlowTest
assertThat(gainingTransferPollMessage.getEventTime()).isEqualTo(clock.nowUtc());
assertThat(gainingAutorenewPollMessage.getEventTime())
.isEqualTo(domain.getRegistrationExpirationTime());
assertThat(
Iterables.getOnlyElement(FluentIterable
DomainTransferResponse transferResponse = getOnlyElement(FluentIterable
.from(gainingTransferPollMessage.getResponseData())
.filter(TransferResponse.class))
.getTransferStatus())
.isEqualTo(TransferStatus.CLIENT_APPROVED);
.filter(DomainTransferResponse.class));
assertThat(transferResponse.getTransferStatus()).isEqualTo(TransferStatus.CLIENT_APPROVED);
assertThat(transferResponse.getExtendedRegistrationExpirationTime())
.isEqualTo(domain.getRegistrationExpirationTime());
PendingActionNotificationResponse panData = Iterables.getOnlyElement(FluentIterable
.from(gainingTransferPollMessage.getResponseData())
.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.ImmutableSet;
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.exceptions.NotPendingTransferException;
import google.registry.flows.exceptions.NotTransferInitiatorException;
import google.registry.flows.exceptions.ResourceToMutateDoesNotExistException;
import google.registry.model.contact.ContactAuthInfo;
import google.registry.model.domain.DomainAuthInfo;
import google.registry.model.domain.DomainResource;

View file

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

View file

@ -28,9 +28,9 @@ import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
import google.registry.flows.ResourceFlowUtils.BadAuthInfoForResourceException;
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.exceptions.NotPendingTransferException;
import google.registry.flows.exceptions.ResourceToMutateDoesNotExistException;
import google.registry.model.contact.ContactAuthInfo;
import google.registry.model.domain.DomainAuthInfo;
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.Iterables;
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.CurrencyUnitMismatchException;
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.PremiumNameBlockedException;
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.billing.BillingEvent;
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
// (OneTime) transfer approved message, and an Autorenew poll message.
assertThat(getPollMessages("NewRegistrar", expectedExpirationTime))
.hasSize(2);
assertThat(getPollMessages("NewRegistrar", expectedExpirationTime)).hasSize(2);
PollMessage transferApprovedPollMessage = getOnlyPollMessage(
"NewRegistrar", implicitTransferTime, PollMessage.OneTime.class);
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">
<msg>Command completed successfully; ack to dequeue</msg>
</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>
<msg>Transfer requested.</msg>
</msgQ>

View file

@ -3,7 +3,7 @@
<result code="1301">
<msg>Command completed successfully; ack to dequeue</msg>
</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>
<msg>Transfer approved.</msg>
</msgQ>

View file

@ -4,7 +4,7 @@
<result code="1301">
<msg>Command completed successfully; ack to dequeue</msg>
</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>
<msg>Transfer approved.</msg>
</msgQ>