diff --git a/java/google/registry/flows/ResourceFlowUtils.java b/java/google/registry/flows/ResourceFlowUtils.java index 5700da0fd..1b4e375b9 100644 --- a/java/google/registry/flows/ResourceFlowUtils.java +++ b/java/google/registry/flows/ResourceFlowUtils.java @@ -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 Builder> - prepareDeletedResourceAsBuilder(R existingResource, DateTime now) { + prepareDeletedResourceAsBuilder(R resource, DateTime now) { Builder> builder = - (Builder>) existingResource.asBuilder() + (Builder>) 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 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 loadResourceForQuery( Class 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 loadResourceToMutate( Class 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 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, ContactResource existingContact) throws EppException { + Optional 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. */ diff --git a/java/google/registry/flows/domain/DomainFlowUtils.java b/java/google/registry/flows/domain/DomainFlowUtils.java index ef0f4b8dd..8ab7fb2f8 100644 --- a/java/google/registry/flows/domain/DomainFlowUtils.java +++ b/java/google/registry/flows/domain/DomainFlowUtils.java @@ -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 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 { diff --git a/java/google/registry/flows/domain/DomainTransferApproveFlow.java b/java/google/registry/flows/domain/DomainTransferApproveFlow.java index 17f579284..332448b34 100644 --- a/java/google/registry/flows/domain/DomainTransferApproveFlow.java +++ b/java/google/registry/flows/domain/DomainTransferApproveFlow.java @@ -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}. * + *

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. + * + *

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. + * *

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 { +public final class DomainTransferApproveFlow extends LoggedInFlow implements TransactionalFlow { + @Inject Optional 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() - .setReason(Reason.TRANSFER) - .setTargetId(targetId) - .setClientId(gainingClientId) - .setPeriodYears(extraYears) - .setCost( - getDomainRenewCost(targetId, transferData.getTransferRequestTime(), extraYears)) - .setEventTime(now) - .setBillingTime(now.plus(Registry.get(tld).getTransferGracePeriodLength())) - .setParent(historyEntry) - .build(); - ofy().save().entity(billingEvent); + BillingEvent.OneTime billingEvent = new BillingEvent.OneTime.Builder() + .setReason(Reason.TRANSFER) + .setTargetId(targetId) + .setClientId(gainingClientId) + .setPeriodYears(extraYears) + .setCost(getDomainRenewCost(targetId, transferData.getTransferRequestTime(), extraYears)) + .setEventTime(now) + .setBillingTime(now.plus(Registry.get(tld).getTransferGracePeriodLength())) + .setParent(historyEntry) + .build(); // 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(){ - @Override - public boolean apply(GracePeriod gracePeriod) { - return GracePeriodStatus.AUTO_RENEW.equals(gracePeriod.getType()); - }}), null); + GracePeriod autorenewGrace = getOnlyElement( + filter( + existingDomain.getGracePeriods(), + new Predicate() { + @Override + public boolean apply(GracePeriod gracePeriod) { + return GracePeriodStatus.AUTO_RENEW.equals(gracePeriod.getType()); + }}), + 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 - .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; + 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))) + .build(); + // Create a poll message for the gaining client. + PollMessage gainingClientPollMessage = createGainingTransferPollMessage( + targetId, + newDomain.getTransferData(), + newExpirationTime, + historyEntry); + ofy().save().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())); } } diff --git a/java/google/registry/flows/domain/DomainTransferCancelFlow.java b/java/google/registry/flows/domain/DomainTransferCancelFlow.java index 527ac926e..6471eb6a5 100644 --- a/java/google/registry/flows/domain/DomainTransferCancelFlow.java +++ b/java/google/registry/flows/domain/DomainTransferCancelFlow.java @@ -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} + *

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. + * + *

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 { +public final class DomainTransferCancelFlow extends LoggedInFlow implements TransactionalFlow { + @Inject Optional 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().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)); } } diff --git a/java/google/registry/flows/domain/DomainTransferQueryFlow.java b/java/google/registry/flows/domain/DomainTransferQueryFlow.java index c6586cc8f..e016eca23 100644 --- a/java/google/registry/flows/domain/DomainTransferQueryFlow.java +++ b/java/google/registry/flows/domain/DomainTransferQueryFlow.java @@ -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}. * + *

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 { +public final class DomainTransferQueryFlow extends LoggedInFlow { + + @Inject Optional 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)); + } } diff --git a/java/google/registry/flows/domain/DomainTransferRejectFlow.java b/java/google/registry/flows/domain/DomainTransferRejectFlow.java index 780036ce2..0b921b7c5 100644 --- a/java/google/registry/flows/domain/DomainTransferRejectFlow.java +++ b/java/google/registry/flows/domain/DomainTransferRejectFlow.java @@ -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} + *

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. + * + *

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 { +public final class DomainTransferRejectFlow extends LoggedInFlow implements TransactionalFlow { + @Inject Optional 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().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)); } } diff --git a/java/google/registry/flows/domain/DomainTransferRequestFlow.java b/java/google/registry/flows/domain/DomainTransferRequestFlow.java index c330a34db..ca1bcd022 100644 --- a/java/google/registry/flows/domain/DomainTransferRequestFlow.java +++ b/java/google/registry/flows/domain/DomainTransferRequestFlow.java @@ -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} + *

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. + * + *

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 { +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 extraFlowLogic; - - /** - * An optional extension from the client specifying how much they think the transfer should cost. - */ - private FeeTransformCommandExtension feeTransfer; + private static final ImmutableSet DISALLOWED_STATUSES = ImmutableSet.of( + StatusValue.CLIENT_TRANSFER_PROHIBITED, + StatusValue.PENDING_DELETE, + StatusValue.SERVER_TRANSFER_PROHIBITED); + @Inject ResourceCommand resourceCommand; + @Inject Optional 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 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 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 pollMessages = createPollMessages( + existingDomain, + historyEntry, + automaticTransferTime, + serverApproveNewExpirationTime, + years); + entitiesToSave.addAll(pollMessages); + ImmutableSet.Builder> 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 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. - * - *

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> getTransferServerApproveEntities() { - ofy().save().entities( - transferBillingEvent, gainingClientAutorenewEvent, gainingClientAutorenewPollMessage); + private ImmutableSet createBillingEvents( + Money renewCost, + Registry registry, + DomainResource existingDomain, + HistoryEntry historyEntry, + DateTime automaticTransferTime, + DateTime serverApproveNewExpirationTime, + int years) { + ImmutableSet.Builder 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> 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() - .setReason(Reason.RENEW) - .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)); + 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); } - serverApproveEntities.add(Key.create(transferBillingEvent)); - serverApproveEntities.add(Key.create(gainingClientAutorenewEvent)); - serverApproveEntities.add(Key.create(gainingClientAutorenewPollMessage)); - return serverApproveEntities; + return billingEvents.build(); } - /** 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); + 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(gainingClientId) + .setEventTime(serverApproveNewExpirationTime) + .setRecurrenceEndTime(END_OF_TIME) + .setParent(historyEntry) + .build(); + } + + /** + * Creates an autorenew cancellation. + * + *

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 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 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 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()); } } diff --git a/java/google/registry/model/transfer/TransferResponse.java b/java/google/registry/model/transfer/TransferResponse.java index 0de8e3ac6..64a03e483 100644 --- a/java/google/registry/model/transfer/TransferResponse.java +++ b/java/google/registry/model/transfer/TransferResponse.java @@ -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 { diff --git a/javatests/google/registry/flows/EppLifecycleDomainTest.java b/javatests/google/registry/flows/EppLifecycleDomainTest.java index 50be3b506..f1af533fd 100644 --- a/javatests/google/registry/flows/EppLifecycleDomainTest.java +++ b/javatests/google/registry/flows/EppLifecycleDomainTest.java @@ -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")); diff --git a/javatests/google/registry/flows/FlowTestCase.java b/javatests/google/registry/flows/FlowTestCase.java index dd932d9f7..4e636f0bb 100644 --- a/javatests/google/registry/flows/FlowTestCase.java +++ b/javatests/google/registry/flows/FlowTestCase.java @@ -304,7 +304,7 @@ public abstract class FlowTestCase 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 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 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); + 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); } } diff --git a/javatests/google/registry/flows/domain/DomainTransferApproveFlowTest.java b/javatests/google/registry/flows/domain/DomainTransferApproveFlowTest.java index 79c40c3ea..adc5fa639 100644 --- a/javatests/google/registry/flows/domain/DomainTransferApproveFlowTest.java +++ b/javatests/google/registry/flows/domain/DomainTransferApproveFlowTest.java @@ -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 - .from(gainingTransferPollMessage.getResponseData()) - .filter(TransferResponse.class)) - .getTransferStatus()) - .isEqualTo(TransferStatus.CLIENT_APPROVED); + DomainTransferResponse transferResponse = getOnlyElement(FluentIterable + .from(gainingTransferPollMessage.getResponseData()) + .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)); diff --git a/javatests/google/registry/flows/domain/DomainTransferCancelFlowTest.java b/javatests/google/registry/flows/domain/DomainTransferCancelFlowTest.java index b4121ea46..9350c9b0f 100644 --- a/javatests/google/registry/flows/domain/DomainTransferCancelFlowTest.java +++ b/javatests/google/registry/flows/domain/DomainTransferCancelFlowTest.java @@ -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; diff --git a/javatests/google/registry/flows/domain/DomainTransferQueryFlowTest.java b/javatests/google/registry/flows/domain/DomainTransferQueryFlowTest.java index 412cf5735..bfa7d0201 100644 --- a/javatests/google/registry/flows/domain/DomainTransferQueryFlowTest.java +++ b/javatests/google/registry/flows/domain/DomainTransferQueryFlowTest.java @@ -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 diff --git a/javatests/google/registry/flows/domain/DomainTransferRejectFlowTest.java b/javatests/google/registry/flows/domain/DomainTransferRejectFlowTest.java index c38cbd6b2..984b85f27 100644 --- a/javatests/google/registry/flows/domain/DomainTransferRejectFlowTest.java +++ b/javatests/google/registry/flows/domain/DomainTransferRejectFlowTest.java @@ -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; diff --git a/javatests/google/registry/flows/domain/DomainTransferRequestFlowTest.java b/javatests/google/registry/flows/domain/DomainTransferRequestFlowTest.java index dcb22f679..90fe3953a 100644 --- a/javatests/google/registry/flows/domain/DomainTransferRequestFlowTest.java +++ b/javatests/google/registry/flows/domain/DomainTransferRequestFlowTest.java @@ -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( diff --git a/javatests/google/registry/flows/domain/testdata/domain_transfer_query_response_10_years.xml b/javatests/google/registry/flows/domain/testdata/domain_transfer_query_response_10_years.xml new file mode 100644 index 000000000..fd0624ce2 --- /dev/null +++ b/javatests/google/registry/flows/domain/testdata/domain_transfer_query_response_10_years.xml @@ -0,0 +1,23 @@ + + + + Command completed successfully + + + + example.tld + pending + NewRegistrar + 2000-06-06T22:00:00.0Z + TheRegistrar + 2000-06-11T22:00:00.0Z + 2010-06-09T22:00:00.0Z + + + + ABC-12345 + server-trid + + + diff --git a/javatests/google/registry/flows/testdata/poll_response_domain_transfer_request.xml b/javatests/google/registry/flows/testdata/poll_response_domain_transfer_request.xml index cb8a34b05..21da8507a 100644 --- a/javatests/google/registry/flows/testdata/poll_response_domain_transfer_request.xml +++ b/javatests/google/registry/flows/testdata/poll_response_domain_transfer_request.xml @@ -3,7 +3,7 @@ Command completed successfully; ack to dequeue - + 2001-01-01T00:00:00Z Transfer requested. diff --git a/javatests/google/registry/flows/testdata/poll_response_domain_transfer_server_approve_loser.xml b/javatests/google/registry/flows/testdata/poll_response_domain_transfer_server_approve_loser.xml index 851c9dc3a..881e70f3a 100644 --- a/javatests/google/registry/flows/testdata/poll_response_domain_transfer_server_approve_loser.xml +++ b/javatests/google/registry/flows/testdata/poll_response_domain_transfer_server_approve_loser.xml @@ -3,7 +3,7 @@ Command completed successfully; ack to dequeue - + 2001-01-06T00:00:00Z Transfer approved. diff --git a/javatests/google/registry/flows/testdata/poll_response_domain_transfer_server_approve_winner.xml b/javatests/google/registry/flows/testdata/poll_response_domain_transfer_server_approve_winner.xml index ce140c107..8e3e7785c 100644 --- a/javatests/google/registry/flows/testdata/poll_response_domain_transfer_server_approve_winner.xml +++ b/javatests/google/registry/flows/testdata/poll_response_domain_transfer_server_approve_winner.xml @@ -4,7 +4,7 @@ Command completed successfully; ack to dequeue - + 2001-01-06T00:00:00Z Transfer approved.