Add support for a domain transfer request superuser EPP extension

Allow superusers to change the transfer period to zero years and allow
superusers to change the automatic transfer length.

-------------
Created by MOE: https://github.com/google/moe
MOE_MIGRATED_REVID=167598314
This commit is contained in:
bbilbo 2017-09-05 10:39:03 -07:00 committed by jianglai
parent 263aea3b2a
commit 2e4b63bb79
28 changed files with 1018 additions and 124 deletions

View file

@ -896,10 +896,15 @@ new ones with the correct approval time).
* Resource with this id does not exist. * Resource with this id does not exist.
* 2304 * 2304
* Resource status prohibits this operation. * Resource status prohibits this operation.
* Superuser extensions cannot be used during autorenew grace periods.
* Domain transfer period cannot be zero when using the fee transfer
extension.
* The requested domain name is on the premium price list, and this * The requested domain name is on the premium price list, and this
registrar has blocked premium registrations. registrar has blocked premium registrations.
* 2306 * 2306
* Domain transfer period must be one year. * Domain transfer period must be one year.
* Domain transfer period must be zero or one year when using the superuser
EPP extension.
* Periods for domain registrations must be specified in years. * Periods for domain registrations must be specified in years.
* The requested fees cannot be provided in the requested currency. * The requested fees cannot be provided in the requested currency.

View file

@ -63,7 +63,8 @@ public class EppXmlTransformer {
"dsig.xsd", "dsig.xsd",
"smd.xsd", "smd.xsd",
"launch.xsd", "launch.xsd",
"allocate.xsd"); "allocate.xsd",
"superuser.xsd");
private static final XmlTransformer INPUT_TRANSFORMER = private static final XmlTransformer INPUT_TRANSFORMER =
new XmlTransformer(SCHEMAS, EppInput.class); new XmlTransformer(SCHEMAS, EppInput.class);

View file

@ -29,8 +29,11 @@ import google.registry.flows.EppException.CommandUseErrorException;
import google.registry.flows.EppException.SyntaxErrorException; import google.registry.flows.EppException.SyntaxErrorException;
import google.registry.flows.EppException.UnimplementedExtensionException; import google.registry.flows.EppException.UnimplementedExtensionException;
import google.registry.flows.FlowModule.ClientId; import google.registry.flows.FlowModule.ClientId;
import google.registry.flows.FlowModule.Superuser;
import google.registry.flows.exceptions.OnlyToolCanPassMetadataException; import google.registry.flows.exceptions.OnlyToolCanPassMetadataException;
import google.registry.flows.exceptions.UnauthorizedForSuperuserExtensionException;
import google.registry.model.domain.metadata.MetadataExtension; import google.registry.model.domain.metadata.MetadataExtension;
import google.registry.model.domain.superuser.SuperuserExtension;
import google.registry.model.eppinput.EppInput; import google.registry.model.eppinput.EppInput;
import google.registry.model.eppinput.EppInput.CommandExtension; import google.registry.model.eppinput.EppInput.CommandExtension;
import google.registry.util.FormattingLogger; import google.registry.util.FormattingLogger;
@ -56,6 +59,7 @@ public final class ExtensionManager {
@Inject EppInput eppInput; @Inject EppInput eppInput;
@Inject SessionMetadata sessionMetadata; @Inject SessionMetadata sessionMetadata;
@Inject @ClientId String clientId; @Inject @ClientId String clientId;
@Inject @Superuser boolean isSuperuser;
@Inject Class<? extends Flow> flowClass; @Inject Class<? extends Flow> flowClass;
@Inject EppRequestSource eppRequestSource; @Inject EppRequestSource eppRequestSource;
@Inject ExtensionManager() {} @Inject ExtensionManager() {}
@ -107,11 +111,18 @@ public final class ExtensionManager {
private void checkForRestrictedExtensions( private void checkForRestrictedExtensions(
ImmutableSet<Class<? extends CommandExtension>> suppliedExtensions) ImmutableSet<Class<? extends CommandExtension>> suppliedExtensions)
throws OnlyToolCanPassMetadataException { throws OnlyToolCanPassMetadataException, UnauthorizedForSuperuserExtensionException {
if (suppliedExtensions.contains(MetadataExtension.class) if (suppliedExtensions.contains(MetadataExtension.class)
&& !eppRequestSource.equals(EppRequestSource.TOOL)) { && !eppRequestSource.equals(EppRequestSource.TOOL)) {
throw new OnlyToolCanPassMetadataException(); throw new OnlyToolCanPassMetadataException();
} }
// Can't use suppliedExtension.contains() here because the SuperuserExtension has child classes.
for (Class<? extends CommandExtension> suppliedExtension : suppliedExtensions) {
if (SuperuserExtension.class.isAssignableFrom(suppliedExtension)
&& (!eppRequestSource.equals(EppRequestSource.TOOL) || !isSuperuser)) {
throw new UnauthorizedForSuperuserExtensionException();
}
}
} }
private static void checkForDuplicateExtensions( private static void checkForDuplicateExtensions(

View file

@ -115,24 +115,29 @@ public final class DomainTransferApproveFlow implements TransactionalFlow {
String gainingClientId = transferData.getGainingClientId(); String gainingClientId = transferData.getGainingClientId();
Registry registry = Registry.get(existingDomain.getTld()); Registry registry = Registry.get(existingDomain.getTld());
HistoryEntry historyEntry = buildHistoryEntry(existingDomain, registry, now, gainingClientId); HistoryEntry historyEntry = buildHistoryEntry(existingDomain, registry, now, gainingClientId);
// Bill for the transfer. // Create a transfer billing event for 1 year, unless the superuser extension was used to set
BillingEvent.OneTime billingEvent = new BillingEvent.OneTime.Builder() // the transfer period to zero. There is not a transfer cost if the transfer period is zero.
.setReason(Reason.TRANSFER) Optional<BillingEvent.OneTime> billingEvent =
.setTargetId(targetId) (transferData.getTransferPeriod().getValue() == 0)
.setClientId(gainingClientId) ? Optional.absent()
.setPeriodYears(1) : Optional.of(
.setCost(getDomainRenewCost(targetId, transferData.getTransferRequestTime(), 1)) new BillingEvent.OneTime.Builder()
.setEventTime(now) .setReason(Reason.TRANSFER)
.setBillingTime(now.plus(Registry.get(tld).getTransferGracePeriodLength())) .setTargetId(targetId)
.setParent(historyEntry) .setClientId(gainingClientId)
.build(); .setPeriodYears(1)
.setCost(getDomainRenewCost(targetId, transferData.getTransferRequestTime(), 1))
.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 don't // If we are within an autorenew grace period, cancel the autorenew billing event and don't
// increase the registration time, since the transfer subsumes the autorenew's extra year. // increase the registration time, since the transfer subsumes the autorenew's extra year.
int extraYears = 1; // All transfers are one year.
GracePeriod autorenewGrace = GracePeriod autorenewGrace =
getOnlyElement(existingDomain.getGracePeriodsOfType(GracePeriodStatus.AUTO_RENEW), null); getOnlyElement(existingDomain.getGracePeriodsOfType(GracePeriodStatus.AUTO_RENEW), null);
int extraYears = transferData.getTransferPeriod().getValue();
if (autorenewGrace != null) { if (autorenewGrace != null) {
extraYears--; extraYears = 0;
ofy().save().entity( ofy().save().entity(
BillingEvent.Cancellation.forGracePeriod(autorenewGrace, historyEntry, targetId)); BillingEvent.Cancellation.forGracePeriod(autorenewGrace, historyEntry, targetId));
} }
@ -167,8 +172,11 @@ public final class DomainTransferApproveFlow implements TransactionalFlow {
.setAutorenewBillingEvent(Key.create(autorenewEvent)) .setAutorenewBillingEvent(Key.create(autorenewEvent))
.setAutorenewPollMessage(Key.create(gainingClientAutorenewPollMessage)) .setAutorenewPollMessage(Key.create(gainingClientAutorenewPollMessage))
// Remove all the old grace periods and add a new one for the transfer. // Remove all the old grace periods and add a new one for the transfer.
.setGracePeriods(ImmutableSet.of( .setGracePeriods(
GracePeriod.forBillingEvent(GracePeriodStatus.TRANSFER, billingEvent))) (billingEvent.isPresent())
? ImmutableSet.of(
GracePeriod.forBillingEvent(GracePeriodStatus.TRANSFER, billingEvent.get()))
: ImmutableSet.of())
.build(); .build();
// Create a poll message for the gaining client. // Create a poll message for the gaining client.
PollMessage gainingClientPollMessage = createGainingTransferPollMessage( PollMessage gainingClientPollMessage = createGainingTransferPollMessage(
@ -176,13 +184,17 @@ public final class DomainTransferApproveFlow implements TransactionalFlow {
newDomain.getTransferData(), newDomain.getTransferData(),
newExpirationTime, newExpirationTime,
historyEntry); historyEntry);
ofy().save().<ImmutableObject>entities( ImmutableSet.Builder<ImmutableObject> entitiesToSave = new ImmutableSet.Builder<>();
entitiesToSave.add(
newDomain, newDomain,
historyEntry, historyEntry,
billingEvent,
autorenewEvent, autorenewEvent,
gainingClientPollMessage, gainingClientPollMessage,
gainingClientAutorenewPollMessage); gainingClientAutorenewPollMessage);
if (billingEvent.isPresent()) {
entitiesToSave.add(billingEvent.get());
}
ofy().save().entities(entitiesToSave.build());
// Delete the billing event and poll messages that were written in case the transfer would have // Delete the billing event and poll messages that were written in case the transfer would have
// been implicitly server approved. // been implicitly server approved.
ofy().delete().keys(existingDomain.getTransferData().getServerApproveEntities()); ofy().delete().keys(existingDomain.getTransferData().getServerApproveEntities());

View file

@ -44,8 +44,11 @@ import google.registry.flows.FlowModule.TargetId;
import google.registry.flows.TransactionalFlow; import google.registry.flows.TransactionalFlow;
import google.registry.flows.annotations.ReportingSpec; import google.registry.flows.annotations.ReportingSpec;
import google.registry.flows.exceptions.AlreadyPendingTransferException; import google.registry.flows.exceptions.AlreadyPendingTransferException;
import google.registry.flows.exceptions.InvalidTransferPeriodValueException;
import google.registry.flows.exceptions.ObjectAlreadySponsoredException; import google.registry.flows.exceptions.ObjectAlreadySponsoredException;
import google.registry.flows.exceptions.SuperuserExtensionAndAutorenewGracePeriodException;
import google.registry.flows.exceptions.TransferPeriodMustBeOneYearException; import google.registry.flows.exceptions.TransferPeriodMustBeOneYearException;
import google.registry.flows.exceptions.TransferPeriodZeroAndFeeTransferExtensionException;
import google.registry.model.domain.DomainCommand.Transfer; import google.registry.model.domain.DomainCommand.Transfer;
import google.registry.model.domain.DomainResource; import google.registry.model.domain.DomainResource;
import google.registry.model.domain.Period; import google.registry.model.domain.Period;
@ -53,6 +56,7 @@ import google.registry.model.domain.fee.FeeTransferCommandExtension;
import google.registry.model.domain.fee.FeeTransformResponseExtension; import google.registry.model.domain.fee.FeeTransformResponseExtension;
import google.registry.model.domain.metadata.MetadataExtension; import google.registry.model.domain.metadata.MetadataExtension;
import google.registry.model.domain.rgp.GracePeriodStatus; import google.registry.model.domain.rgp.GracePeriodStatus;
import google.registry.model.domain.superuser.DomainTransferRequestSuperuserExtension;
import google.registry.model.eppcommon.AuthInfo; import google.registry.model.eppcommon.AuthInfo;
import google.registry.model.eppcommon.StatusValue; import google.registry.model.eppcommon.StatusValue;
import google.registry.model.eppcommon.Trid; import google.registry.model.eppcommon.Trid;
@ -94,7 +98,10 @@ import org.joda.time.DateTime;
* @error {@link google.registry.flows.exceptions.MissingTransferRequestAuthInfoException} * @error {@link google.registry.flows.exceptions.MissingTransferRequestAuthInfoException}
* @error {@link google.registry.flows.exceptions.ObjectAlreadySponsoredException} * @error {@link google.registry.flows.exceptions.ObjectAlreadySponsoredException}
* @error {@link google.registry.flows.exceptions.ResourceStatusProhibitsOperationException} * @error {@link google.registry.flows.exceptions.ResourceStatusProhibitsOperationException}
* @error {@link google.registry.flows.exceptions.SuperuserExtensionAndAutorenewGracePeriodException}
* @error {@link google.registry.flows.exceptions.TransferPeriodMustBeOneYearException} * @error {@link google.registry.flows.exceptions.TransferPeriodMustBeOneYearException}
* @error {@link InvalidTransferPeriodValueException}
* @error {@link google.registry.flows.exceptions.TransferPeriodZeroAndFeeTransferExtensionException}
* @error {@link DomainFlowUtils.BadPeriodUnitException} * @error {@link DomainFlowUtils.BadPeriodUnitException}
* @error {@link DomainFlowUtils.CurrencyUnitMismatchException} * @error {@link DomainFlowUtils.CurrencyUnitMismatchException}
* @error {@link DomainFlowUtils.CurrencyValueScaleException} * @error {@link DomainFlowUtils.CurrencyValueScaleException}
@ -128,24 +135,43 @@ public final class DomainTransferRequestFlow implements TransactionalFlow {
@Override @Override
public final EppResponse run() throws EppException { public final EppResponse run() throws EppException {
extensionManager.register( extensionManager.register(
DomainTransferRequestSuperuserExtension.class,
FeeTransferCommandExtension.class, FeeTransferCommandExtension.class,
MetadataExtension.class); MetadataExtension.class);
extensionManager.validate(); extensionManager.validate();
validateClientIsLoggedIn(gainingClientId); validateClientIsLoggedIn(gainingClientId);
Period period = ((Transfer) resourceCommand).getPeriod();
DateTime now = ofy().getTransactionTime(); DateTime now = ofy().getTransactionTime();
DomainResource existingDomain = loadAndVerifyExistence(DomainResource.class, targetId, now); DomainResource existingDomain = loadAndVerifyExistence(DomainResource.class, targetId, now);
verifyTransferAllowed(existingDomain, period, now); DomainTransferRequestSuperuserExtension superuserExtension =
eppInput.getSingleExtension(DomainTransferRequestSuperuserExtension.class);
Period period =
(superuserExtension == null)
? ((Transfer) resourceCommand).getPeriod()
: superuserExtension.getRenewalPeriod();
verifyTransferAllowed(existingDomain, period, now, superuserExtension);
String tld = existingDomain.getTld(); String tld = existingDomain.getTld();
Registry registry = Registry.get(tld); Registry registry = Registry.get(tld);
// An optional extension from the client specifying what they think the transfer should cost. // An optional extension from the client specifying what they think the transfer should cost.
FeeTransferCommandExtension feeTransfer = FeeTransferCommandExtension feeTransfer =
eppInput.getSingleExtension(FeeTransferCommandExtension.class); eppInput.getSingleExtension(FeeTransferCommandExtension.class);
FeesAndCredits feesAndCredits = pricingLogic.getTransferPrice(registry, targetId, now); if (period.getValue() == 0 && feeTransfer != null) {
validateFeeChallenge(targetId, tld, now, feeTransfer, feesAndCredits); // If the period is zero, then there is no transfer billing event, so using the fee transfer
// extension does not make sense.
throw new TransferPeriodZeroAndFeeTransferExtensionException();
}
// If the period is zero, then there is no fee for the transfer.
Optional<FeesAndCredits> feesAndCredits =
(period.getValue() == 0)
? Optional.absent()
: Optional.of(pricingLogic.getTransferPrice(registry, targetId, now));
if (feesAndCredits.isPresent()) {
validateFeeChallenge(targetId, tld, now, feeTransfer, feesAndCredits.get());
}
HistoryEntry historyEntry = buildHistoryEntry(existingDomain, registry, now, period); HistoryEntry historyEntry = buildHistoryEntry(existingDomain, registry, now, period);
DateTime automaticTransferTime = now.plus(registry.getAutomaticTransferLength()); DateTime automaticTransferTime =
(superuserExtension == null)
? now.plus(registry.getAutomaticTransferLength())
: now.plusDays(superuserExtension.getAutomaticTransferLength());
// If the domain will be in the auto-renew grace period at the moment of transfer, the transfer // If the domain will be in the auto-renew grace period at the moment of transfer, the transfer
// will subsume the autorenew, so we don't add the normal extra year from the transfer. // will subsume the autorenew, so we don't add the normal extra year from the transfer.
// The gaining registrar is still billed for the extra year; the losing registrar will get a // The gaining registrar is still billed for the extra year; the losing registrar will get a
@ -153,11 +179,15 @@ public final class DomainTransferRequestFlow implements TransactionalFlow {
// //
// See b/19430703#comment17 and https://www.icann.org/news/advisory-2002-06-06-en for the // See b/19430703#comment17 and https://www.icann.org/news/advisory-2002-06-06-en for the
// policy documentation for transfers subsuming autorenews within the autorenew grace period. // policy documentation for transfers subsuming autorenews within the autorenew grace period.
int extraYears = 1; int extraYears = period.getValue();
DomainResource domainAtTransferTime = DomainResource domainAtTransferTime =
existingDomain.cloneProjectedAtTime(automaticTransferTime); existingDomain.cloneProjectedAtTime(automaticTransferTime);
if (!domainAtTransferTime.getGracePeriodsOfType(GracePeriodStatus.AUTO_RENEW).isEmpty()) { if (!domainAtTransferTime.getGracePeriodsOfType(GracePeriodStatus.AUTO_RENEW).isEmpty()) {
extraYears--; if (superuserExtension != null) {
// We don't allow the superuser extension for domains in the auto renew grace period
throw new SuperuserExtensionAndAutorenewGracePeriodException();
}
extraYears = 0;
} }
// The new expiration time if there is a server approval. // The new expiration time if there is a server approval.
DateTime serverApproveNewExpirationTime = DateTime serverApproveNewExpirationTime =
@ -174,12 +204,16 @@ public final class DomainTransferRequestFlow implements TransactionalFlow {
existingDomain, existingDomain,
trid, trid,
gainingClientId, gainingClientId,
feesAndCredits.getTotalCost(), (feesAndCredits.isPresent())
? Optional.of(feesAndCredits.get().getTotalCost())
: Optional.absent(),
now); now);
// Create the transfer data that represents the pending transfer. // Create the transfer data that represents the pending transfer.
TransferData pendingTransferData = createPendingTransferData( TransferData pendingTransferData =
createTransferDataBuilder(existingDomain, automaticTransferTime, now), createPendingTransferData(
serverApproveEntities); createTransferDataBuilder(existingDomain, automaticTransferTime, now),
serverApproveEntities,
period);
// Create a poll message to notify the losing registrar that a transfer was requested. // Create a poll message to notify the losing registrar that a transfer was requested.
PollMessage requestPollMessage = createLosingTransferPollMessage( PollMessage requestPollMessage = createLosingTransferPollMessage(
targetId, pendingTransferData, serverApproveNewExpirationTime, historyEntry) targetId, pendingTransferData, serverApproveNewExpirationTime, historyEntry)
@ -206,7 +240,11 @@ public final class DomainTransferRequestFlow implements TransactionalFlow {
.build(); .build();
} }
private void verifyTransferAllowed(DomainResource existingDomain, Period period, DateTime now) private void verifyTransferAllowed(
DomainResource existingDomain,
Period period,
DateTime now,
final DomainTransferRequestSuperuserExtension superuserExtension)
throws EppException { throws EppException {
verifyNoDisallowedStatuses(existingDomain, DISALLOWED_STATUSES); verifyNoDisallowedStatuses(existingDomain, DISALLOWED_STATUSES);
verifyAuthInfoPresentForResourceTransfer(authInfo); verifyAuthInfoPresentForResourceTransfer(authInfo);
@ -219,7 +257,7 @@ public final class DomainTransferRequestFlow implements TransactionalFlow {
if (gainingClientId.equals(existingDomain.getCurrentSponsorClientId())) { if (gainingClientId.equals(existingDomain.getCurrentSponsorClientId())) {
throw new ObjectAlreadySponsoredException(); throw new ObjectAlreadySponsoredException();
} }
verifyTransferPeriodIsOneYear(period); verifyTransferPeriod(period, superuserExtension);
if (!isSuperuser) { if (!isSuperuser) {
checkAllowedAccessToTld(gainingClientId, existingDomain.getTld()); checkAllowedAccessToTld(gainingClientId, existingDomain.getTld());
verifyPremiumNameIsNotBlocked(targetId, now, gainingClientId); verifyPremiumNameIsNotBlocked(targetId, now, gainingClientId);
@ -227,7 +265,8 @@ public final class DomainTransferRequestFlow implements TransactionalFlow {
} }
/** /**
* Verify that the transfer period is one year. * Verify that the transfer period is one year. If the superuser extension is being used, then it
* can be zero.
* *
* <p>Restricting transfers to one year is seemingly required by ICANN's <a * <p>Restricting transfers to one year is seemingly required by ICANN's <a
* href="https://www.icann.org/resources/pages/policy-2012-03-07-en">Policy on Transfer of * href="https://www.icann.org/resources/pages/policy-2012-03-07-en">Policy on Transfer of
@ -246,10 +285,20 @@ public final class DomainTransferRequestFlow implements TransactionalFlow {
* <p>Note that clients can omit the period element from the transfer EPP entirely, but then it * <p>Note that clients can omit the period element from the transfer EPP entirely, but then it
* will simply default to one year. * will simply default to one year.
*/ */
private static void verifyTransferPeriodIsOneYear(Period period) throws EppException { private static void verifyTransferPeriod(
Period period, DomainTransferRequestSuperuserExtension superuserExtension)
throws EppException {
verifyUnitIsYears(period); verifyUnitIsYears(period);
if (period.getValue() != 1) { if (superuserExtension == null) {
throw new TransferPeriodMustBeOneYearException(); // If the superuser extension is not being used, then the period can only be one.
if (period.getValue() != 1) {
throw new TransferPeriodMustBeOneYearException();
}
} else {
// If the superuser extension is being used, then the period can be one or zero.
if (period.getValue() != 1 && period.getValue() != 0) {
throw new InvalidTransferPeriodValueException();
}
} }
} }
@ -296,13 +345,13 @@ public final class DomainTransferRequestFlow implements TransactionalFlow {
} }
private static ImmutableList<FeeTransformResponseExtension> createResponseExtensions( private static ImmutableList<FeeTransformResponseExtension> createResponseExtensions(
FeesAndCredits feesAndCredits, FeeTransferCommandExtension feeTransfer) { Optional<FeesAndCredits> feesAndCredits, FeeTransferCommandExtension feeTransfer) {
return feeTransfer == null return (feeTransfer == null || !feesAndCredits.isPresent())
? ImmutableList.<FeeTransformResponseExtension>of() ? ImmutableList.<FeeTransformResponseExtension>of()
: ImmutableList.of(feeTransfer.createResponseBuilder() : ImmutableList.of(feeTransfer.createResponseBuilder()
.setFees(feesAndCredits.getFees()) .setFees(feesAndCredits.get().getFees())
.setCredits(feesAndCredits.getCredits()) .setCredits(feesAndCredits.get().getCredits())
.setCurrency(feesAndCredits.getCurrency()) .setCurrency(feesAndCredits.get().getCurrency())
.build()); .build());
} }
} }

View file

@ -27,6 +27,7 @@ import google.registry.model.billing.BillingEvent.Flag;
import google.registry.model.billing.BillingEvent.Reason; import google.registry.model.billing.BillingEvent.Reason;
import google.registry.model.domain.DomainResource; import google.registry.model.domain.DomainResource;
import google.registry.model.domain.GracePeriod; import google.registry.model.domain.GracePeriod;
import google.registry.model.domain.Period;
import google.registry.model.domain.rgp.GracePeriodStatus; import google.registry.model.domain.rgp.GracePeriodStatus;
import google.registry.model.eppcommon.Trid; import google.registry.model.eppcommon.Trid;
import google.registry.model.poll.PendingActionNotificationResponse.DomainPendingActionNotificationResponse; import google.registry.model.poll.PendingActionNotificationResponse.DomainPendingActionNotificationResponse;
@ -47,26 +48,29 @@ import org.joda.time.DateTime;
*/ */
public final class DomainTransferUtils { public final class DomainTransferUtils {
/** /** Sets up {@link TransferData} for a domain with links to entities for server approval. */
* Sets up {@link TransferData} for a domain with links to entities for server approval.
*/
public static TransferData createPendingTransferData( public static TransferData createPendingTransferData(
TransferData.Builder transferDataBuilder, TransferData.Builder transferDataBuilder,
ImmutableSet<TransferServerApproveEntity> serverApproveEntities) { ImmutableSet<TransferServerApproveEntity> serverApproveEntities,
Period transferPeriod) {
ImmutableSet.Builder<Key<? extends TransferServerApproveEntity>> serverApproveEntityKeys = ImmutableSet.Builder<Key<? extends TransferServerApproveEntity>> serverApproveEntityKeys =
new ImmutableSet.Builder<>(); new ImmutableSet.Builder<>();
for (TransferServerApproveEntity entity : serverApproveEntities) { for (TransferServerApproveEntity entity : serverApproveEntities) {
serverApproveEntityKeys.add(Key.create(entity)); serverApproveEntityKeys.add(Key.create(entity));
} }
if (transferPeriod.getValue() != 0) {
// Unless superuser sets period to 0, add a transfer billing event.
transferDataBuilder.setServerApproveBillingEvent(
Key.create(getOnlyElement(filter(serverApproveEntities, BillingEvent.OneTime.class))));
}
return transferDataBuilder return transferDataBuilder
.setTransferStatus(TransferStatus.PENDING) .setTransferStatus(TransferStatus.PENDING)
.setServerApproveBillingEvent(Key.create(
getOnlyElement(filter(serverApproveEntities, BillingEvent.OneTime.class))))
.setServerApproveAutorenewEvent(Key.create( .setServerApproveAutorenewEvent(Key.create(
getOnlyElement(filter(serverApproveEntities, BillingEvent.Recurring.class)))) getOnlyElement(filter(serverApproveEntities, BillingEvent.Recurring.class))))
.setServerApproveAutorenewPollMessage(Key.create( .setServerApproveAutorenewPollMessage(Key.create(
getOnlyElement(filter(serverApproveEntities, PollMessage.Autorenew.class)))) getOnlyElement(filter(serverApproveEntities, PollMessage.Autorenew.class))))
.setServerApproveEntities(serverApproveEntityKeys.build()) .setServerApproveEntities(serverApproveEntityKeys.build())
.setTransferPeriod(transferPeriod)
.build(); .build();
} }
@ -91,7 +95,7 @@ public final class DomainTransferUtils {
DomainResource existingDomain, DomainResource existingDomain,
Trid trid, Trid trid,
String gainingClientId, String gainingClientId,
Money transferCost, Optional<Money> transferCost,
DateTime now) { DateTime now) {
String targetId = existingDomain.getFullyQualifiedDomainName(); String targetId = existingDomain.getFullyQualifiedDomainName();
// Create a TransferData for the server-approve case to use for the speculative poll messages. // Create a TransferData for the server-approve case to use for the speculative poll messages.
@ -101,15 +105,18 @@ public final class DomainTransferUtils {
.setTransferStatus(TransferStatus.SERVER_APPROVED) .setTransferStatus(TransferStatus.SERVER_APPROVED)
.build(); .build();
Registry registry = Registry.get(existingDomain.getTld()); Registry registry = Registry.get(existingDomain.getTld());
return new ImmutableSet.Builder<TransferServerApproveEntity>() ImmutableSet.Builder<TransferServerApproveEntity> builder = new ImmutableSet.Builder<>();
.add( if (transferCost.isPresent()) {
createTransferBillingEvent( builder.add(
automaticTransferTime, createTransferBillingEvent(
historyEntry, automaticTransferTime,
targetId, historyEntry,
gainingClientId, targetId,
registry, gainingClientId,
transferCost)) registry,
transferCost.get()));
}
return builder
.addAll( .addAll(
createOptionalAutorenewCancellation( createOptionalAutorenewCancellation(
automaticTransferTime, historyEntry, targetId, existingDomain) automaticTransferTime, historyEntry, targetId, existingDomain)

View file

@ -0,0 +1,25 @@
// Copyright 2017 The Nomulus Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package google.registry.flows.exceptions;
import google.registry.flows.EppException.ParameterValuePolicyErrorException;
/** Domain transfer period must be zero or one year when using the superuser EPP extension. */
public class InvalidTransferPeriodValueException extends ParameterValuePolicyErrorException {
public InvalidTransferPeriodValueException() {
super(
"Domain transfer period must be zero or one year when using the superuser EPP extension.");
}
}

View file

@ -0,0 +1,25 @@
// Copyright 2017 The Nomulus Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package google.registry.flows.exceptions;
import google.registry.flows.EppException.StatusProhibitsOperationException;
/** Superuser extensions cannot be used during autorenew grace periods. */
public class SuperuserExtensionAndAutorenewGracePeriodException extends
StatusProhibitsOperationException {
public SuperuserExtensionAndAutorenewGracePeriodException() {
super("Superuser extensions cannot be used during autorenew grace periods.");
}
}

View file

@ -0,0 +1,25 @@
// Copyright 2017 The Nomulus Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package google.registry.flows.exceptions;
import google.registry.flows.EppException.StatusProhibitsOperationException;
/** Domain transfer period cannot be zero when using the fee transfer extension. */
public class TransferPeriodZeroAndFeeTransferExtensionException
extends StatusProhibitsOperationException {
public TransferPeriodZeroAndFeeTransferExtensionException() {
super("Domain transfer period cannot be zero when using the fee transfer extension.");
}
}

View file

@ -0,0 +1,24 @@
// Copyright 2017 The Nomulus Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package google.registry.flows.exceptions;
import google.registry.flows.EppException.AuthorizationErrorException;
/** Superuser extension used by non-superuser or not passed by tool. */
public class UnauthorizedForSuperuserExtensionException extends AuthorizationErrorException {
public UnauthorizedForSuperuserExtensionException() {
super("Superuser extension used by non-superuser or not passed by tool.");
}
}

View file

@ -245,9 +245,11 @@ public class DomainResource extends DomainBase
// If we are within an autorenew grace period, the transfer will subsume the autorenew. There // If we are within an autorenew grace period, the transfer will subsume the autorenew. There
// will already be a cancellation written in advance by the transfer request flow, so we don't // will already be a cancellation written in advance by the transfer request flow, so we don't
// need to worry about billing, but we do need to cancel out the expiration time increase. // need to worry about billing, but we do need to cancel out the expiration time increase.
int extraYears = 1; // All transfers are one year. // The transfer period saved in the transfer data will be one year, unless the superuser
// extension set the transfer period to zero.
int extraYears = transferData.getTransferPeriod().getValue();
if (domainAtTransferTime.getGracePeriodStatuses().contains(GracePeriodStatus.AUTO_RENEW)) { if (domainAtTransferTime.getGracePeriodStatuses().contains(GracePeriodStatus.AUTO_RENEW)) {
extraYears--; extraYears = 0;
} }
// Set the expiration, autorenew events, and grace period for the transfer. (Transfer ends // Set the expiration, autorenew events, and grace period for the transfer. (Transfer ends
// all other graces). // all other graces).
@ -261,14 +263,22 @@ public class DomainResource extends DomainBase
extraYears)) extraYears))
// Set the speculatively-written new autorenew events as the domain's autorenew events. // Set the speculatively-written new autorenew events as the domain's autorenew events.
.setAutorenewBillingEvent(transferData.getServerApproveAutorenewEvent()) .setAutorenewBillingEvent(transferData.getServerApproveAutorenewEvent())
.setAutorenewPollMessage(transferData.getServerApproveAutorenewPollMessage()) .setAutorenewPollMessage(transferData.getServerApproveAutorenewPollMessage());
// Set the grace period using a key to the prescheduled transfer billing event. Not using if (transferData.getTransferPeriod().getValue() == 1) {
// GracePeriod.forBillingEvent() here in order to avoid the actual Datastore fetch. // Set the grace period using a key to the prescheduled transfer billing event. Not using
.setGracePeriods(ImmutableSet.of(GracePeriod.create( // GracePeriod.forBillingEvent() here in order to avoid the actual Datastore fetch.
GracePeriodStatus.TRANSFER, builder.setGracePeriods(
transferExpirationTime.plus(Registry.get(getTld()).getTransferGracePeriodLength()), ImmutableSet.of(
transferData.getGainingClientId(), GracePeriod.create(
transferData.getServerApproveBillingEvent()))); GracePeriodStatus.TRANSFER,
transferExpirationTime.plus(
Registry.get(getTld()).getTransferGracePeriodLength()),
transferData.getGainingClientId(),
transferData.getServerApproveBillingEvent())));
} else {
// There won't be a billing event, so we don't need a grace period
builder.setGracePeriods(ImmutableSet.of());
}
// Set all remaining transfer properties. // Set all remaining transfer properties.
setAutomaticTransferSuccessProperties(builder, transferData); setAutomaticTransferSuccessProperties(builder, transferData);
// Finish projecting to now. // Finish projecting to now.

View file

@ -0,0 +1,40 @@
// Copyright 2017 The Nomulus Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package google.registry.model.domain.superuser;
import google.registry.model.domain.Period;
import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlRootElement;
/** A superuser extension that may be present on domain transfer request commands. */
@XmlRootElement(name = "domainTransferRequest")
public class DomainTransferRequestSuperuserExtension extends SuperuserExtension {
// We need to specify the period here because the transfer object's period cannot be set to zero.
@XmlElement(name = "renewalPeriod")
Period renewalPeriod;
// The number of days before the transfer will be automatically approved by the server. A value of
// zero means the transfer will happen immediately.
@XmlElement(name = "automaticTransferLength")
int automaticTransferLength;
public Period getRenewalPeriod() {
return renewalPeriod;
}
public int getAutomaticTransferLength() {
return automaticTransferLength;
}
}

View file

@ -0,0 +1,23 @@
// Copyright 2017 The Nomulus Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package google.registry.model.domain.superuser;
import google.registry.model.ImmutableObject;
import google.registry.model.eppinput.EppInput.CommandExtension;
import javax.xml.bind.annotation.XmlTransient;
/** Base class for superuser EPP extensions. */
@XmlTransient
public abstract class SuperuserExtension extends ImmutableObject implements CommandExtension {}

View file

@ -0,0 +1,26 @@
// Copyright 2017 The Nomulus Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
@XmlSchema(
namespace = "urn:google:params:xml:ns:superuser-1.0",
xmlns = @XmlNs(prefix = "superuser", namespaceURI = "urn:google:params:xml:ns:superuser-1.0"),
elementFormDefault = XmlNsForm.QUALIFIED)
@XmlAccessorType(XmlAccessType.FIELD)
package google.registry.model.domain.superuser;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlNs;
import javax.xml.bind.annotation.XmlNsForm;
import javax.xml.bind.annotation.XmlSchema;

View file

@ -51,6 +51,7 @@ import google.registry.model.domain.metadata.MetadataExtension;
import google.registry.model.domain.rgp.RgpUpdateExtension; import google.registry.model.domain.rgp.RgpUpdateExtension;
import google.registry.model.domain.secdns.SecDnsCreateExtension; import google.registry.model.domain.secdns.SecDnsCreateExtension;
import google.registry.model.domain.secdns.SecDnsUpdateExtension; import google.registry.model.domain.secdns.SecDnsUpdateExtension;
import google.registry.model.domain.superuser.DomainTransferRequestSuperuserExtension;
import google.registry.model.eppinput.ResourceCommand.ResourceCheck; import google.registry.model.eppinput.ResourceCommand.ResourceCheck;
import google.registry.model.eppinput.ResourceCommand.SingleResourceCommand; import google.registry.model.eppinput.ResourceCommand.SingleResourceCommand;
import google.registry.model.host.HostCommand; import google.registry.model.host.HostCommand;
@ -345,7 +346,8 @@ public class EppInput extends ImmutableObject {
@XmlElementRef(type = MetadataExtension.class), @XmlElementRef(type = MetadataExtension.class),
@XmlElementRef(type = RgpUpdateExtension.class), @XmlElementRef(type = RgpUpdateExtension.class),
@XmlElementRef(type = SecDnsCreateExtension.class), @XmlElementRef(type = SecDnsCreateExtension.class),
@XmlElementRef(type = SecDnsUpdateExtension.class) }) @XmlElementRef(type = SecDnsUpdateExtension.class),
@XmlElementRef(type = DomainTransferRequestSuperuserExtension.class) })
@XmlElementWrapper @XmlElementWrapper
List<CommandExtension> extension; List<CommandExtension> extension;

View file

@ -25,6 +25,8 @@ import com.googlecode.objectify.condition.IfNull;
import google.registry.model.Buildable; import google.registry.model.Buildable;
import google.registry.model.EppResource; import google.registry.model.EppResource;
import google.registry.model.billing.BillingEvent; import google.registry.model.billing.BillingEvent;
import google.registry.model.domain.Period;
import google.registry.model.domain.Period.Unit;
import google.registry.model.eppcommon.Trid; import google.registry.model.eppcommon.Trid;
import google.registry.model.poll.PollMessage; import google.registry.model.poll.PollMessage;
import java.util.Set; import java.util.Set;
@ -81,6 +83,14 @@ public class TransferData extends BaseTransferObject implements Buildable {
/** The transaction id of the most recent transfer request (or null if there never was one). */ /** The transaction id of the most recent transfer request (or null if there never was one). */
Trid transferRequestTrid; Trid transferRequestTrid;
/**
* The period to extend the registration upon completion of the transfer.
*
* <p>By default, domain transfers are for one year. This can be changed to zero by using the
* superuser EPP extension.
*/
Period transferPeriod = Period.create(1, Unit.YEARS);
public ImmutableSet<Key<? extends TransferServerApproveEntity>> getServerApproveEntities() { public ImmutableSet<Key<? extends TransferServerApproveEntity>> getServerApproveEntities() {
return nullToEmptyImmutableCopy(serverApproveEntities); return nullToEmptyImmutableCopy(serverApproveEntities);
} }
@ -101,6 +111,10 @@ public class TransferData extends BaseTransferObject implements Buildable {
return transferRequestTrid; return transferRequestTrid;
} }
public Period getTransferPeriod() {
return transferPeriod;
}
@Override @Override
public Builder asBuilder() { public Builder asBuilder() {
return new Builder(clone(this)); return new Builder(clone(this));
@ -145,6 +159,11 @@ public class TransferData extends BaseTransferObject implements Buildable {
getInstance().transferRequestTrid = transferRequestTrid; getInstance().transferRequestTrid = transferRequestTrid;
return this; return this;
} }
public Builder setTransferPeriod(Period transferPeriod) {
getInstance().transferPeriod = transferPeriod;
return this;
}
} }
/** /**

View file

@ -42,6 +42,8 @@ import google.registry.gcs.GcsUtils;
import google.registry.mapreduce.MapreduceRunner; import google.registry.mapreduce.MapreduceRunner;
import google.registry.model.billing.BillingEvent; import google.registry.model.billing.BillingEvent;
import google.registry.model.domain.DomainResource; import google.registry.model.domain.DomainResource;
import google.registry.model.domain.Period;
import google.registry.model.domain.Period.Unit;
import google.registry.model.poll.PollMessage; import google.registry.model.poll.PollMessage;
import google.registry.model.reporting.HistoryEntry; import google.registry.model.reporting.HistoryEntry;
import google.registry.model.transfer.TransferData; import google.registry.model.transfer.TransferData;
@ -203,10 +205,13 @@ public class RdeDomainImportAction implements Runnable {
domain, domain,
historyEntry.getTrid(), historyEntry.getTrid(),
transferData.getGainingClientId(), transferData.getGainingClientId(),
transferCost, Optional.of(transferCost),
transferData.getTransferRequestTime()); transferData.getTransferRequestTime());
transferData = transferData =
createPendingTransferData(transferData.asBuilder(), serverApproveEntities); createPendingTransferData(
transferData.asBuilder(),
serverApproveEntities,
Period.create(1, Unit.YEARS));
// Create a poll message to notify the losing registrar that a transfer was requested. // Create a poll message to notify the losing registrar that a transfer was requested.
PollMessage requestPollMessage = createLosingTransferPollMessage(domain.getRepoId(), PollMessage requestPollMessage = createLosingTransferPollMessage(domain.getRepoId(),
transferData, transferData.getPendingTransferExpirationTime(), historyEntry) transferData, transferData.getPendingTransferExpirationTime(), historyEntry)

View file

@ -0,0 +1,41 @@
<?xml version="1.0" encoding="utf-8"?>
<schema
xmlns="http://www.w3.org/2001/XMLSchema"
xmlns:superuser="urn:google:params:xml:ns:superuser-1.0"
targetNamespace="urn:google:params:xml:ns:superuser-1.0"
elementFormDefault="qualified">
<element name="domainTransferRequest"
type="superuser:domainTransferRequest" />
<complexType name="domainTransferRequest">
<all>
<element name="renewalPeriod" type="superuser:periodType" />
<element name="automaticTransferLength" type="nonNegativeInteger" />
</all>
</complexType>
<complexType name="periodType">
<simpleContent>
<extension base="superuser:pLimitType">
<attribute name="unit" type="superuser:pUnitType"
use="required"/>
</extension>
</simpleContent>
</complexType>
<simpleType name="pLimitType">
<restriction base="unsignedShort">
<minInclusive value="0"/>
<maxInclusive value="99"/>
</restriction>
</simpleType>
<simpleType name="pUnitType">
<restriction base="token">
<enumeration value="y"/>
<enumeration value="m"/>
</restriction>
</simpleType>
</schema>

View file

@ -23,10 +23,12 @@ import google.registry.flows.EppException.UnimplementedExtensionException;
import google.registry.flows.ExtensionManager.UndeclaredServiceExtensionException; import google.registry.flows.ExtensionManager.UndeclaredServiceExtensionException;
import google.registry.flows.ExtensionManager.UnsupportedRepeatedExtensionException; import google.registry.flows.ExtensionManager.UnsupportedRepeatedExtensionException;
import google.registry.flows.exceptions.OnlyToolCanPassMetadataException; import google.registry.flows.exceptions.OnlyToolCanPassMetadataException;
import google.registry.flows.exceptions.UnauthorizedForSuperuserExtensionException;
import google.registry.flows.session.HelloFlow; import google.registry.flows.session.HelloFlow;
import google.registry.model.domain.fee06.FeeInfoCommandExtensionV06; import google.registry.model.domain.fee06.FeeInfoCommandExtensionV06;
import google.registry.model.domain.launch.LaunchCreateExtension; import google.registry.model.domain.launch.LaunchCreateExtension;
import google.registry.model.domain.metadata.MetadataExtension; import google.registry.model.domain.metadata.MetadataExtension;
import google.registry.model.domain.superuser.DomainTransferRequestSuperuserExtension;
import google.registry.model.eppcommon.ProtocolDefinition.ServiceExtension; import google.registry.model.eppcommon.ProtocolDefinition.ServiceExtension;
import google.registry.model.eppinput.EppInput; import google.registry.model.eppinput.EppInput;
import google.registry.model.eppinput.EppInput.CommandExtension; import google.registry.model.eppinput.EppInput.CommandExtension;
@ -133,6 +135,44 @@ public class ExtensionManagerTest {
manager.validate(); manager.validate();
} }
@Test
public void testSuperuserExtension_allowedForToolSource() throws Exception {
ExtensionManager manager = new TestInstanceBuilder()
.setEppRequestSource(EppRequestSource.TOOL)
.setDeclaredUris()
.setSuppliedExtensions(DomainTransferRequestSuperuserExtension.class)
.setIsSuperuser(true)
.build();
manager.register(DomainTransferRequestSuperuserExtension.class);
manager.validate();
}
@Test
public void testSuperuserExtension_forbiddenWhenNotSuperuser() throws Exception {
ExtensionManager manager = new TestInstanceBuilder()
.setEppRequestSource(EppRequestSource.TOOL)
.setDeclaredUris()
.setSuppliedExtensions(DomainTransferRequestSuperuserExtension.class)
.setIsSuperuser(false)
.build();
manager.register(DomainTransferRequestSuperuserExtension.class);
thrown.expect(UnauthorizedForSuperuserExtensionException.class);
manager.validate();
}
@Test
public void testSuperuserExtension_forbiddenWhenNotToolSource() throws Exception {
ExtensionManager manager = new TestInstanceBuilder()
.setEppRequestSource(EppRequestSource.CONSOLE)
.setDeclaredUris()
.setSuppliedExtensions(DomainTransferRequestSuperuserExtension.class)
.setIsSuperuser(true)
.build();
manager.register(DomainTransferRequestSuperuserExtension.class);
thrown.expect(UnauthorizedForSuperuserExtensionException.class);
manager.validate();
}
@Test @Test
public void testUnimplementedExtensionsForbidden() throws Exception { public void testUnimplementedExtensionsForbidden() throws Exception {
ExtensionManager manager = new TestInstanceBuilder() ExtensionManager manager = new TestInstanceBuilder()
@ -160,6 +200,11 @@ public class ExtensionManagerTest {
return this; return this;
} }
TestInstanceBuilder setIsSuperuser(boolean isSuperuser) {
manager.isSuperuser = isSuperuser;
return this;
}
@SafeVarargs @SafeVarargs
final TestInstanceBuilder setSuppliedExtensions( final TestInstanceBuilder setSuppliedExtensions(
Class<? extends CommandExtension>... suppliedExtensionClasses) { Class<? extends CommandExtension>... suppliedExtensionClasses) {

View file

@ -50,11 +50,14 @@ import google.registry.flows.exceptions.NotPendingTransferException;
import google.registry.model.billing.BillingEvent; import google.registry.model.billing.BillingEvent;
import google.registry.model.billing.BillingEvent.Cancellation; import google.registry.model.billing.BillingEvent.Cancellation;
import google.registry.model.billing.BillingEvent.Cancellation.Builder; import google.registry.model.billing.BillingEvent.Cancellation.Builder;
import google.registry.model.billing.BillingEvent.OneTime;
import google.registry.model.billing.BillingEvent.Reason; import google.registry.model.billing.BillingEvent.Reason;
import google.registry.model.contact.ContactAuthInfo; import google.registry.model.contact.ContactAuthInfo;
import google.registry.model.domain.DomainAuthInfo; import google.registry.model.domain.DomainAuthInfo;
import google.registry.model.domain.DomainResource; import google.registry.model.domain.DomainResource;
import google.registry.model.domain.GracePeriod; import google.registry.model.domain.GracePeriod;
import google.registry.model.domain.Period;
import google.registry.model.domain.Period.Unit;
import google.registry.model.domain.rgp.GracePeriodStatus; import google.registry.model.domain.rgp.GracePeriodStatus;
import google.registry.model.eppcommon.AuthInfo.PasswordAuth; import google.registry.model.eppcommon.AuthInfo.PasswordAuth;
import google.registry.model.eppcommon.StatusValue; import google.registry.model.eppcommon.StatusValue;
@ -64,6 +67,7 @@ import google.registry.model.poll.PollMessage;
import google.registry.model.registry.Registry; import google.registry.model.registry.Registry;
import google.registry.model.reporting.DomainTransactionRecord; import google.registry.model.reporting.DomainTransactionRecord;
import google.registry.model.reporting.HistoryEntry; import google.registry.model.reporting.HistoryEntry;
import google.registry.model.transfer.TransferData;
import google.registry.model.transfer.TransferResponse.DomainTransferResponse; import google.registry.model.transfer.TransferResponse.DomainTransferResponse;
import google.registry.model.transfer.TransferStatus; import google.registry.model.transfer.TransferStatus;
import org.joda.money.Money; import org.joda.money.Money;
@ -128,6 +132,20 @@ public class DomainTransferApproveFlowTest
DateTime expectedExpirationTime, DateTime expectedExpirationTime,
int expectedYearsToCharge, int expectedYearsToCharge,
BillingEvent.Cancellation.Builder... expectedCancellationBillingEvents) throws Exception { BillingEvent.Cancellation.Builder... expectedCancellationBillingEvents) throws Exception {
runSuccessfulFlowWithAssertions(
tld,
commandFilename,
expectedXmlFilename,
expectedExpirationTime);
assertHistoryEntriesContainBillingEventsAndGracePeriods(
tld, expectedYearsToCharge, expectedCancellationBillingEvents);
}
private void runSuccessfulFlowWithAssertions(
String tld,
String commandFilename,
String expectedXmlFilename,
DateTime expectedExpirationTime) throws Exception {
setEppLoader(commandFilename); setEppLoader(commandFilename);
Registry registry = Registry.get(tld); Registry registry = Registry.get(tld);
// Make sure the implicit billing event is there; it will be deleted by the flow. // Make sure the implicit billing event is there; it will be deleted by the flow.
@ -157,46 +175,6 @@ public class DomainTransferApproveFlowTest
assertAboutDomains().that(domain).hasRegistrationExpirationTime(expectedExpirationTime); assertAboutDomains().that(domain).hasRegistrationExpirationTime(expectedExpirationTime);
assertThat(ofy().load().key(domain.getAutorenewBillingEvent()).now().getEventTime()) assertThat(ofy().load().key(domain.getAutorenewBillingEvent()).now().getEventTime())
.isEqualTo(expectedExpirationTime); .isEqualTo(expectedExpirationTime);
// We expect three billing events: one for the transfer, a closed autorenew for the losing
// client and an open autorenew for the gaining client that begins at the new expiration time.
BillingEvent.OneTime transferBillingEvent = new BillingEvent.OneTime.Builder()
.setReason(Reason.TRANSFER)
.setTargetId(domain.getFullyQualifiedDomainName())
.setEventTime(clock.nowUtc())
.setBillingTime(clock.nowUtc().plus(registry.getTransferGracePeriodLength()))
.setClientId("NewRegistrar")
.setCost(Money.of(USD, 11).multipliedBy(expectedYearsToCharge))
.setPeriodYears(expectedYearsToCharge)
.setParent(historyEntryTransferApproved)
.build();
assertBillingEventsForResource(
domain,
FluentIterable.from(expectedCancellationBillingEvents)
.transform(new Function<BillingEvent.Cancellation.Builder, BillingEvent>() {
@Override
public Cancellation apply(Builder builder) {
return builder.setParent(historyEntryTransferApproved).build();
}})
.append(
transferBillingEvent,
getLosingClientAutorenewEvent().asBuilder()
.setRecurrenceEndTime(clock.nowUtc())
.build(),
getGainingClientAutorenewEvent().asBuilder()
.setEventTime(domain.getRegistrationExpirationTime())
.setParent(historyEntryTransferApproved)
.build())
.toArray(BillingEvent.class));
// There should be a grace period for the new transfer billing event.
assertGracePeriods(
domain.getGracePeriods(),
ImmutableMap.of(
GracePeriod.create(
GracePeriodStatus.TRANSFER,
clock.nowUtc().plus(registry.getTransferGracePeriodLength()),
"NewRegistrar",
null),
transferBillingEvent));
// The poll message (in the future) to the losing registrar for implicit ack should be gone. // The poll message (in the future) to the losing registrar for implicit ack should be gone.
assertThat(getPollMessages(domain, "TheRegistrar", clock.nowUtc().plusMonths(1))).isEmpty(); assertThat(getPollMessages(domain, "TheRegistrar", clock.nowUtc().plusMonths(1))).isEmpty();
@ -236,6 +214,97 @@ public class DomainTransferApproveFlowTest
.getGracePeriods()).isEmpty(); .getGracePeriods()).isEmpty();
} }
private void assertHistoryEntriesContainBillingEventsAndGracePeriods(
String tld,
int expectedYearsToCharge,
BillingEvent.Cancellation.Builder... expectedCancellationBillingEvents)
throws Exception {
Registry registry = Registry.get(tld);
domain = reloadResourceByForeignKey();
final HistoryEntry historyEntryTransferApproved =
getOnlyHistoryEntryOfType(domain, DOMAIN_TRANSFER_APPROVE);
// We expect three billing events: one for the transfer, a closed autorenew for the losing
// client and an open autorenew for the gaining client that begins at the new expiration time.
OneTime transferBillingEvent =
new BillingEvent.OneTime.Builder()
.setReason(Reason.TRANSFER)
.setTargetId(domain.getFullyQualifiedDomainName())
.setEventTime(clock.nowUtc())
.setBillingTime(clock.nowUtc().plus(registry.getTransferGracePeriodLength()))
.setClientId("NewRegistrar")
.setCost(Money.of(USD, 11).multipliedBy(expectedYearsToCharge))
.setPeriodYears(expectedYearsToCharge)
.setParent(historyEntryTransferApproved)
.build();
assertBillingEventsForResource(
domain,
FluentIterable.from(expectedCancellationBillingEvents)
.transform(
new Function<BillingEvent.Cancellation.Builder, BillingEvent>() {
@Override
public Cancellation apply(Builder builder) {
return builder.setParent(historyEntryTransferApproved).build();
}
})
.append(
transferBillingEvent,
getLosingClientAutorenewEvent()
.asBuilder()
.setRecurrenceEndTime(clock.nowUtc())
.build(),
getGainingClientAutorenewEvent()
.asBuilder()
.setEventTime(domain.getRegistrationExpirationTime())
.setParent(historyEntryTransferApproved)
.build())
.toArray(BillingEvent.class));
// There should be a grace period for the new transfer billing event.
assertGracePeriods(
domain.getGracePeriods(),
ImmutableMap.of(
GracePeriod.create(
GracePeriodStatus.TRANSFER,
clock.nowUtc().plus(registry.getTransferGracePeriodLength()),
"NewRegistrar",
null),
transferBillingEvent));
}
private void assertHistoryEntriesDoNotContainTransferBillingEventsOrGracePeriods(
BillingEvent.Cancellation.Builder... expectedCancellationBillingEvents)
throws Exception {
domain = reloadResourceByForeignKey();
final HistoryEntry historyEntryTransferApproved =
getOnlyHistoryEntryOfType(domain, DOMAIN_TRANSFER_APPROVE);
// We expect two billing events: a closed autorenew for the losing client and an open autorenew
// for the gaining client that begins at the new expiration time.
assertBillingEventsForResource(
domain,
FluentIterable.from(expectedCancellationBillingEvents)
.transform(
new Function<BillingEvent.Cancellation.Builder, BillingEvent>() {
@Override
public Cancellation apply(Builder builder) {
return builder.setParent(historyEntryTransferApproved).build();
}
})
.append(
getLosingClientAutorenewEvent()
.asBuilder()
.setRecurrenceEndTime(clock.nowUtc())
.build(),
getGainingClientAutorenewEvent()
.asBuilder()
.setEventTime(domain.getRegistrationExpirationTime())
.setParent(historyEntryTransferApproved)
.build())
.toArray(BillingEvent.class));
// There should be no grace period.
assertGracePeriods(
domain.getGracePeriods(),
ImmutableMap.of());
}
private void doSuccessfulTest(String tld, String commandFilename, String expectedXmlFilename) private void doSuccessfulTest(String tld, String commandFilename, String expectedXmlFilename)
throws Exception { throws Exception {
clock.advanceOneMilli(); clock.advanceOneMilli();
@ -503,4 +572,23 @@ public class DomainTransferApproveFlowTest
"tld", clock.nowUtc().plusDays(3), TRANSFER_SUCCESSFUL, 1)); "tld", clock.nowUtc().plusDays(3), TRANSFER_SUCCESSFUL, 1));
} }
@Test
public void testSuccess_superuserExtension_transferPeriodZero() throws Exception {
DomainResource domain = reloadResourceByForeignKey();
TransferData.Builder transferDataBuilder = domain.getTransferData().asBuilder();
persistResource(
domain
.asBuilder()
.setTransferData(
transferDataBuilder.setTransferPeriod(Period.create(0, Unit.YEARS)).build())
.build());
clock.advanceOneMilli();
runSuccessfulFlowWithAssertions(
"tld",
"domain_transfer_approve.xml",
"domain_transfer_approve_response_zero_period.xml",
domain.getRegistrationExpirationTime().plusYears(0));
assertHistoryEntriesDoNotContainTransferBillingEventsOrGracePeriods();
}
} }

View file

@ -36,6 +36,7 @@ import static org.joda.money.CurrencyUnit.USD;
import com.google.common.base.Function; import com.google.common.base.Function;
import com.google.common.base.Optional; import com.google.common.base.Optional;
import com.google.common.base.Predicate;
import com.google.common.base.Predicates; import com.google.common.base.Predicates;
import com.google.common.collect.FluentIterable; import com.google.common.collect.FluentIterable;
import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableMap;
@ -43,6 +44,7 @@ import com.google.common.collect.ImmutableSet;
import com.google.common.collect.ImmutableSortedMap; import com.google.common.collect.ImmutableSortedMap;
import com.google.common.collect.Iterables; import com.google.common.collect.Iterables;
import com.googlecode.objectify.Key; import com.googlecode.objectify.Key;
import google.registry.flows.EppRequestSource;
import google.registry.flows.ResourceFlowUtils.BadAuthInfoForResourceException; import google.registry.flows.ResourceFlowUtils.BadAuthInfoForResourceException;
import google.registry.flows.ResourceFlowUtils.ResourceDoesNotExistException; import google.registry.flows.ResourceFlowUtils.ResourceDoesNotExistException;
import google.registry.flows.domain.DomainFlowUtils.BadPeriodUnitException; import google.registry.flows.domain.DomainFlowUtils.BadPeriodUnitException;
@ -54,10 +56,13 @@ import google.registry.flows.domain.DomainFlowUtils.NotAuthorizedForTldException
import google.registry.flows.domain.DomainFlowUtils.PremiumNameBlockedException; import google.registry.flows.domain.DomainFlowUtils.PremiumNameBlockedException;
import google.registry.flows.domain.DomainFlowUtils.UnsupportedFeeAttributeException; import google.registry.flows.domain.DomainFlowUtils.UnsupportedFeeAttributeException;
import google.registry.flows.exceptions.AlreadyPendingTransferException; import google.registry.flows.exceptions.AlreadyPendingTransferException;
import google.registry.flows.exceptions.InvalidTransferPeriodValueException;
import google.registry.flows.exceptions.MissingTransferRequestAuthInfoException; import google.registry.flows.exceptions.MissingTransferRequestAuthInfoException;
import google.registry.flows.exceptions.ObjectAlreadySponsoredException; import google.registry.flows.exceptions.ObjectAlreadySponsoredException;
import google.registry.flows.exceptions.ResourceStatusProhibitsOperationException; import google.registry.flows.exceptions.ResourceStatusProhibitsOperationException;
import google.registry.flows.exceptions.SuperuserExtensionAndAutorenewGracePeriodException;
import google.registry.flows.exceptions.TransferPeriodMustBeOneYearException; import google.registry.flows.exceptions.TransferPeriodMustBeOneYearException;
import google.registry.flows.exceptions.TransferPeriodZeroAndFeeTransferExtensionException;
import google.registry.model.billing.BillingEvent; import google.registry.model.billing.BillingEvent;
import google.registry.model.billing.BillingEvent.Cancellation.Builder; import google.registry.model.billing.BillingEvent.Cancellation.Builder;
import google.registry.model.billing.BillingEvent.Reason; import google.registry.model.billing.BillingEvent.Reason;
@ -65,6 +70,8 @@ import google.registry.model.contact.ContactAuthInfo;
import google.registry.model.domain.DomainAuthInfo; import google.registry.model.domain.DomainAuthInfo;
import google.registry.model.domain.DomainResource; import google.registry.model.domain.DomainResource;
import google.registry.model.domain.GracePeriod; import google.registry.model.domain.GracePeriod;
import google.registry.model.domain.Period;
import google.registry.model.domain.Period.Unit;
import google.registry.model.domain.rgp.GracePeriodStatus; import google.registry.model.domain.rgp.GracePeriodStatus;
import google.registry.model.eppcommon.AuthInfo.PasswordAuth; import google.registry.model.eppcommon.AuthInfo.PasswordAuth;
import google.registry.model.eppcommon.StatusValue; import google.registry.model.eppcommon.StatusValue;
@ -117,26 +124,33 @@ public class DomainTransferRequestFlowTest
setClientIdForFlow("NewRegistrar"); setClientIdForFlow("NewRegistrar");
} }
private void assertTransferRequested(DomainResource domain) throws Exception { private void assertTransferRequested(
DomainResource domain, Optional<Duration> expectedAutomaticTransferLength) throws Exception {
DateTime afterAutoAckTime =
(expectedAutomaticTransferLength.isPresent())
? clock.nowUtc().plus(expectedAutomaticTransferLength.get())
: clock.nowUtc().plus(Registry.get(domain.getTld()).getAutomaticTransferLength());
assertAboutDomains().that(domain) assertAboutDomains().that(domain)
.hasTransferStatus(TransferStatus.PENDING).and() .hasTransferStatus(TransferStatus.PENDING).and()
.hasTransferGainingClientId("NewRegistrar").and() .hasTransferGainingClientId("NewRegistrar").and()
.hasTransferLosingClientId("TheRegistrar").and() .hasTransferLosingClientId("TheRegistrar").and()
.hasTransferRequestClientTrid(getClientTrid()).and() .hasTransferRequestClientTrid(getClientTrid()).and()
.hasCurrentSponsorClientId("TheRegistrar").and() .hasCurrentSponsorClientId("TheRegistrar").and()
.hasPendingTransferExpirationTime( .hasPendingTransferExpirationTime(afterAutoAckTime).and()
clock.nowUtc().plus(Registry.get(domain.getTld()).getAutomaticTransferLength())).and()
.hasStatusValue(StatusValue.PENDING_TRANSFER); .hasStatusValue(StatusValue.PENDING_TRANSFER);
} }
private void assertTransferApproved(DomainResource domain) { private void assertTransferApproved(
DateTime afterAutoAck = DomainResource domain, Optional<Duration> expectedAutomaticTransferLength) {
clock.nowUtc().plus(Registry.get(domain.getTld()).getAutomaticTransferLength()); DateTime afterAutoAckTime =
(expectedAutomaticTransferLength.isPresent())
? clock.nowUtc().plus(expectedAutomaticTransferLength.get())
: clock.nowUtc().plus(Registry.get(domain.getTld()).getAutomaticTransferLength());
assertAboutDomains().that(domain) assertAboutDomains().that(domain)
.hasTransferStatus(TransferStatus.SERVER_APPROVED).and() .hasTransferStatus(TransferStatus.SERVER_APPROVED).and()
.hasCurrentSponsorClientId("NewRegistrar").and() .hasCurrentSponsorClientId("NewRegistrar").and()
.hasLastTransferTime(afterAutoAck).and() .hasLastTransferTime(afterAutoAckTime).and()
.hasPendingTransferExpirationTime(afterAutoAck).and() .hasPendingTransferExpirationTime(afterAutoAckTime).and()
.doesNotHaveStatusValue(StatusValue.PENDING_TRANSFER); .doesNotHaveStatusValue(StatusValue.PENDING_TRANSFER);
} }
@ -169,7 +183,7 @@ public class DomainTransferRequestFlowTest
final HistoryEntry historyEntryTransferRequest = final HistoryEntry historyEntryTransferRequest =
getOnlyHistoryEntryOfType(domain, DOMAIN_TRANSFER_REQUEST); getOnlyHistoryEntryOfType(domain, DOMAIN_TRANSFER_REQUEST);
subordinateHost = reloadResourceAndCloneAtTime(subordinateHost, clock.nowUtc()); subordinateHost = reloadResourceAndCloneAtTime(subordinateHost, clock.nowUtc());
assertTransferRequested(domain); assertTransferRequested(domain, Optional.absent());
assertAboutDomains().that(domain) assertAboutDomains().that(domain)
.hasPendingTransferExpirationTime(implicitTransferTime).and() .hasPendingTransferExpirationTime(implicitTransferTime).and()
.hasOneHistoryEntryEachOfTypes(DOMAIN_CREATE, DOMAIN_TRANSFER_REQUEST); .hasOneHistoryEntryEachOfTypes(DOMAIN_CREATE, DOMAIN_TRANSFER_REQUEST);
@ -179,7 +193,30 @@ public class DomainTransferRequestFlowTest
.and() .and()
.hasOtherClientId("TheRegistrar"); .hasOtherClientId("TheRegistrar");
assertAboutHosts().that(subordinateHost).hasNoHistoryEntries(); assertAboutHosts().that(subordinateHost).hasNoHistoryEntries();
assertThat(getPollMessages("TheRegistrar", clock.nowUtc())).hasSize(1);
assertHistoryEntriesContainBillingEventsAndGracePeriods(
expectedExpirationTime,
implicitTransferTime,
transferCost,
originalGracePeriods,
extraExpectedBillingEvents);
assertPollMessagesEmitted(expectedExpirationTime, implicitTransferTime, Optional.absent());
assertAboutDomainAfterAutomaticTransfer(
expectedExpirationTime, implicitTransferTime, Optional.absent());
}
private void assertHistoryEntriesContainBillingEventsAndGracePeriods(
DateTime expectedExpirationTime,
DateTime implicitTransferTime,
Optional<Money> transferCost,
ImmutableSet<GracePeriod> originalGracePeriods,
BillingEvent.Cancellation.Builder... extraExpectedBillingEvents)
throws Exception {
Registry registry = Registry.get(domain.getTld());
final HistoryEntry historyEntryTransferRequest =
getOnlyHistoryEntryOfType(domain, DOMAIN_TRANSFER_REQUEST);
// A BillingEvent should be created AUTOMATIC_TRANSFER_DAYS in the future, for the case when the // A BillingEvent should be created AUTOMATIC_TRANSFER_DAYS in the future, for the case when the
// transfer is implicitly acked, but there should be no grace period yet. There should also be // transfer is implicitly acked, but there should be no grace period yet. There should also be
@ -235,7 +272,69 @@ public class DomainTransferRequestFlowTest
"NewRegistrar", "NewRegistrar",
null), null),
transferBillingEvent)); transferBillingEvent));
assertTransferApproved(domainAfterAutomaticTransfer); }
public void assertHistoryEntriesDoNotContainTransferBillingEventsOrGracePeriods(
DateTime expectedExpirationTime,
DateTime implicitTransferTime,
ImmutableSet<GracePeriod> originalGracePeriods,
BillingEvent.Cancellation.Builder... extraExpectedBillingEvents)
throws Exception {
final HistoryEntry historyEntryTransferRequest =
getOnlyHistoryEntryOfType(domain, DOMAIN_TRANSFER_REQUEST);
// There should also be two autorenew billing events, one for the losing client that ends at the
// transfer time, and one for the gaining client that starts at that time.
// All of the other transfer flow tests happen on day 3 of the transfer, but the initial
// request by definition takes place on day 1, so we need to edit the times in the
// autorenew events from the base test case.
assertBillingEvents(
FluentIterable.from(extraExpectedBillingEvents)
.transform(
new Function<BillingEvent.Cancellation.Builder, BillingEvent>() {
@Override
public BillingEvent apply(Builder builder) {
return builder.setParent(historyEntryTransferRequest).build();
}
})
.append(
getLosingClientAutorenewEvent()
.asBuilder()
.setRecurrenceEndTime(implicitTransferTime)
.build(),
getGainingClientAutorenewEvent()
.asBuilder()
.setEventTime(expectedExpirationTime)
.build())
.toArray(BillingEvent.class));
// The domain's autorenew billing event should still point to the losing client's event.
BillingEvent.Recurring domainAutorenewEvent =
ofy().load().key(domain.getAutorenewBillingEvent()).now();
assertThat(domainAutorenewEvent.getClientId()).isEqualTo("TheRegistrar");
assertThat(domainAutorenewEvent.getRecurrenceEndTime()).isEqualTo(implicitTransferTime);
// The original grace periods should remain untouched.
assertThat(domain.getGracePeriods()).containsExactlyElementsIn(originalGracePeriods);
// If we fast forward AUTOMATIC_TRANSFER_DAYS, we should see the grace period appear, the
// transfer should have happened, and all other grace periods should be gone. Also, both the
// gaining and losing registrars should have a new poll message.
DomainResource domainAfterAutomaticTransfer = domain.cloneProjectedAtTime(implicitTransferTime);
// There should be no grace period.
assertGracePeriods(domainAfterAutomaticTransfer.getGracePeriods(), ImmutableMap.of());
}
private void assertPollMessagesEmitted(
DateTime expectedExpirationTime,
DateTime implicitTransferTime,
Optional<Duration> expectedAutomaticTransferLength) {
// Assert that there exists a poll message to notify the losing registrar that a transfer was
// requested. If the expected automatic transfer length is zero, then also expect a server
// approved poll message.
assertThat(getPollMessages("TheRegistrar", clock.nowUtc()))
.hasSize(
(expectedAutomaticTransferLength.isPresent()
&& expectedAutomaticTransferLength.get().equals(Duration.ZERO))
? 2
: 1);
// Two poll messages on the gaining registrar's side at the expected expiration time: a // Two poll messages on the gaining registrar's side at the expected expiration time: a
// (OneTime) transfer approved message, and an Autorenew poll message. // (OneTime) transfer approved message, and an Autorenew poll message.
@ -261,8 +360,15 @@ public class DomainTransferRequestFlowTest
// Two poll messages on the losing registrar's side at the implicit transfer time: a // Two poll messages on the losing registrar's side at the implicit transfer time: a
// transfer pending message, and a transfer approved message (both OneTime messages). // transfer pending message, and a transfer approved message (both OneTime messages).
assertThat(getPollMessages("TheRegistrar", implicitTransferTime)).hasSize(2); assertThat(getPollMessages("TheRegistrar", implicitTransferTime)).hasSize(2);
PollMessage losingTransferPendingPollMessage = PollMessage losingTransferPendingPollMessage = Iterables.getOnlyElement(
getOnlyPollMessage("TheRegistrar", clock.nowUtc()); FluentIterable.from(getPollMessages("TheRegistrar", clock.nowUtc()))
.filter(
new Predicate<PollMessage>() {
@Override
public boolean apply(PollMessage pollMessage) {
return TransferStatus.PENDING.getMessage().equals(pollMessage.getMsg());
}
}));
PollMessage losingTransferApprovedPollMessage = Iterables.getOnlyElement(FluentIterable PollMessage losingTransferApprovedPollMessage = Iterables.getOnlyElement(FluentIterable
.from(getPollMessages("TheRegistrar", implicitTransferTime)) .from(getPollMessages("TheRegistrar", implicitTransferTime))
.filter(Predicates.not(Predicates.equalTo(losingTransferPendingPollMessage)))); .filter(Predicates.not(Predicates.equalTo(losingTransferPendingPollMessage))));
@ -280,7 +386,15 @@ public class DomainTransferRequestFlowTest
.filter(TransferResponse.class)) .filter(TransferResponse.class))
.getTransferStatus()) .getTransferStatus())
.isEqualTo(TransferStatus.SERVER_APPROVED); .isEqualTo(TransferStatus.SERVER_APPROVED);
}
private void assertAboutDomainAfterAutomaticTransfer(
DateTime expectedExpirationTime,
DateTime implicitTransferTime,
Optional<Duration> expectedAutomaticTransferLength) {
Registry registry = Registry.get(domain.getTld());
DomainResource domainAfterAutomaticTransfer = domain.cloneProjectedAtTime(implicitTransferTime);
assertTransferApproved(domainAfterAutomaticTransfer, expectedAutomaticTransferLength);
assertAboutDomains().that(domainAfterAutomaticTransfer) assertAboutDomains().that(domainAfterAutomaticTransfer)
.hasRegistrationExpirationTime(expectedExpirationTime); .hasRegistrationExpirationTime(expectedExpirationTime);
assertThat(ofy().load().key(domainAfterAutomaticTransfer.getAutorenewBillingEvent()).now() assertThat(ofy().load().key(domainAfterAutomaticTransfer.getAutorenewBillingEvent()).now()
@ -327,6 +441,76 @@ public class DomainTransferRequestFlowTest
commandFilename, expectedXmlFilename, domain.getRegistrationExpirationTime().plusYears(1)); commandFilename, expectedXmlFilename, domain.getRegistrationExpirationTime().plusYears(1));
} }
private void doSuccessfulSuperuserExtensionTest(
String commandFilename,
String expectedXmlFilename,
DateTime expectedExpirationTime,
Map<String, String> substitutions,
Optional<Money> transferCost,
Period expectedPeriod,
Duration expectedAutomaticTransferLength,
BillingEvent.Cancellation.Builder... extraExpectedBillingEvents) throws Exception {
setEppInput(commandFilename, substitutions);
ImmutableSet<GracePeriod> originalGracePeriods = domain.getGracePeriods();
// Replace the ROID in the xml file with the one generated in our test.
eppLoader.replaceAll("JD1234-REP", contact.getRepoId());
// For all of the other transfer flow tests, 'now' corresponds to day 3 of the transfer, but
// for the request test we want that same 'now' to be the initial request time, so we shift
// the transfer timeline 3 days later by adjusting the implicit transfer time here.
DateTime implicitTransferTime = clock.nowUtc().plus(expectedAutomaticTransferLength);
// Setup done; run the test.
assertTransactionalFlow(true);
runFlowAssertResponse(
CommitMode.LIVE, UserPrivileges.SUPERUSER, readFile(expectedXmlFilename, substitutions));
if (expectedAutomaticTransferLength.equals(Duration.ZERO)) {
// The transfer is going to happen immediately. To observe the domain in the pending transfer
// state, grab it directly from the database.
domain = Iterables.getOnlyElement(ofy().load().type(DomainResource.class).list());
assertThat(domain.getFullyQualifiedDomainName()).isEqualTo("example.tld");
} else {
// Transfer should have been requested.
domain = reloadResourceByForeignKey();
}
// Verify correct fields were set.
subordinateHost = reloadResourceAndCloneAtTime(subordinateHost, clock.nowUtc());
assertTransferRequested(domain, Optional.of(expectedAutomaticTransferLength));
assertAboutDomains().that(domain)
.hasPendingTransferExpirationTime(implicitTransferTime).and()
.hasOneHistoryEntryEachOfTypes(DOMAIN_CREATE, DOMAIN_TRANSFER_REQUEST);
final HistoryEntry historyEntryTransferRequest =
getOnlyHistoryEntryOfType(domain, DOMAIN_TRANSFER_REQUEST);
assertAboutHistoryEntries()
.that(historyEntryTransferRequest)
.hasPeriodYears(expectedPeriod.getValue())
.and()
.hasOtherClientId("TheRegistrar");
assertAboutHosts().that(subordinateHost).hasNoHistoryEntries();
if (expectedPeriod.getValue() == 0) {
assertHistoryEntriesDoNotContainTransferBillingEventsOrGracePeriods(
expectedExpirationTime,
implicitTransferTime,
originalGracePeriods,
extraExpectedBillingEvents);
} else {
assertHistoryEntriesContainBillingEventsAndGracePeriods(
expectedExpirationTime,
implicitTransferTime,
transferCost,
originalGracePeriods,
extraExpectedBillingEvents);
}
assertPollMessagesEmitted(
expectedExpirationTime,
implicitTransferTime,
Optional.of(expectedAutomaticTransferLength));
assertAboutDomainAfterAutomaticTransfer(
expectedExpirationTime, implicitTransferTime, Optional.of(expectedAutomaticTransferLength));
}
private void runTest( private void runTest(
String commandFilename, String commandFilename,
UserPrivileges userPrivileges, UserPrivileges userPrivileges,
@ -517,6 +701,95 @@ public class DomainTransferRequestFlowTest
doFailingTest("domain_transfer_request_2_years.xml"); doFailingTest("domain_transfer_request_2_years.xml");
} }
@Test
public void testSuccess_superuserExtension_zeroPeriod_nonZeroAutomaticTransferLength()
throws Exception {
setupDomain("example", "tld");
eppRequestSource = EppRequestSource.TOOL;
clock.advanceOneMilli();
doSuccessfulSuperuserExtensionTest(
"domain_transfer_request_superuser_extension.xml",
"domain_transfer_request_response_su_ext_zero_period_nonzero_transfer_length.xml",
domain.getRegistrationExpirationTime().plusYears(0),
ImmutableMap.of("PERIOD", "0", "AUTOMATIC_TRANSFER_LENGTH", "5"),
Optional.<Money>absent(),
Period.create(0, Unit.YEARS),
Duration.standardDays(5));
}
@Test
public void testSuccess_superuserExtension_zeroPeriod_zeroAutomaticTransferLength()
throws Exception {
setupDomain("example", "tld");
eppRequestSource = EppRequestSource.TOOL;
clock.advanceOneMilli();
doSuccessfulSuperuserExtensionTest(
"domain_transfer_request_superuser_extension.xml",
"domain_transfer_request_response_su_ext_zero_period_zero_transfer_length.xml",
domain.getRegistrationExpirationTime().plusYears(0),
ImmutableMap.of("PERIOD", "0", "AUTOMATIC_TRANSFER_LENGTH", "0"),
Optional.<Money>absent(),
Period.create(0, Unit.YEARS),
Duration.standardDays(0));
}
@Test
public void testSuccess_superuserExtension_nonZeroPeriod_nonZeroAutomaticTransferLength()
throws Exception {
setupDomain("example", "tld");
eppRequestSource = EppRequestSource.TOOL;
clock.advanceOneMilli();
doSuccessfulSuperuserExtensionTest(
"domain_transfer_request_superuser_extension.xml",
"domain_transfer_request_response_su_ext_one_year_period_nonzero_transfer_length.xml",
domain.getRegistrationExpirationTime().plusYears(1),
ImmutableMap.of("PERIOD", "1", "AUTOMATIC_TRANSFER_LENGTH", "5"),
Optional.<Money>absent(),
Period.create(1, Unit.YEARS),
Duration.standardDays(5));
}
@Test
public void testFailure_superuserExtension_duringAutorenewGracePeriod() throws Exception {
setupDomain("example", "tld");
eppRequestSource = EppRequestSource.TOOL;
DomainResource domain = reloadResourceByForeignKey();
DateTime oldExpirationTime = clock.nowUtc().minusDays(1);
persistResource(domain.asBuilder()
.setRegistrationExpirationTime(oldExpirationTime)
.build());
clock.advanceOneMilli();
thrown.expect(SuperuserExtensionAndAutorenewGracePeriodException.class);
runTest(
"domain_transfer_request_superuser_extension.xml",
UserPrivileges.SUPERUSER,
ImmutableMap.of("PERIOD", "1", "AUTOMATIC_TRANSFER_LENGTH", "5"));
}
@Test
public void testFailure_superuserExtension_twoYearPeriod() throws Exception {
setupDomain("example", "tld");
eppRequestSource = EppRequestSource.TOOL;
clock.advanceOneMilli();
thrown.expect(InvalidTransferPeriodValueException.class);
runTest(
"domain_transfer_request_superuser_extension.xml",
UserPrivileges.SUPERUSER,
ImmutableMap.of("PERIOD", "2", "AUTOMATIC_TRANSFER_LENGTH", "5"));
}
@Test
public void testFailure_superuserExtension_zeroPeriod_feeTransferExtension() throws Exception {
setupDomain("example", "tld");
eppRequestSource = EppRequestSource.TOOL;
clock.advanceOneMilli();
thrown.expect(TransferPeriodZeroAndFeeTransferExtensionException.class);
runTest(
"domain_transfer_request_fee_and_superuser_extension.xml",
UserPrivileges.SUPERUSER,
ImmutableMap.of("PERIOD", "0", "AUTOMATIC_TRANSFER_LENGTH", "5"));
}
@Test @Test
public void testSuccess_cappedExpiration() throws Exception { public void testSuccess_cappedExpiration() throws Exception {
setupDomain("example", "tld"); setupDomain("example", "tld");

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>clientApproved</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-09T22:00:00.0Z</domain:acDate>
<domain:exDate>2001-09-08T22:00:00.0Z</domain:exDate>
</domain:trnData>
</resData>
<trID>
<clTRID>ABC-12345</clTRID>
<svTRID>server-trid</svTRID>
</trID>
</response>
</epp>

View file

@ -0,0 +1,24 @@
<epp xmlns="urn:ietf:params:xml:ns:epp-1.0">
<command>
<transfer op="request">
<domain:transfer
xmlns:domain="urn:ietf:params:xml:ns:domain-1.0">
<domain:name>example.tld</domain:name>
<domain:authInfo>
<domain:pw roid="JD1234-REP">2fooBAR</domain:pw>
</domain:authInfo>
</domain:transfer>
</transfer>
<extension>
<fee:transfer xmlns:fee="urn:ietf:params:xml:ns:fee-0.6">
<fee:currency>USD</fee:currency>
<fee:fee>11</fee:fee>
</fee:transfer>
<superuser:domainTransferRequest xmlns:superuser="urn:google:params:xml:ns:superuser-1.0">
<superuser:renewalPeriod unit="y">0</superuser:renewalPeriod>
<superuser:automaticTransferLength>0</superuser:automaticTransferLength>
</superuser:domainTransferRequest>
</extension>
<clTRID>ABC-12345</clTRID>
</command>
</epp>

View file

@ -0,0 +1,23 @@
<epp xmlns="urn:ietf:params:xml:ns:epp-1.0">
<response>
<result code="1001">
<msg>Command completed successfully; action pending</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-09T22:00:00.0Z</domain:reDate>
<domain:acID>TheRegistrar</domain:acID>
<domain:acDate>2000-06-14T22:00:00.0Z</domain:acDate>
<domain:exDate>2002-09-08T22:00:00.0Z</domain:exDate>
</domain:trnData>
</resData>
<trID>
<clTRID>ABC-12345</clTRID>
<svTRID>server-trid</svTRID>
</trID>
</response>
</epp>

View file

@ -0,0 +1,23 @@
<epp xmlns="urn:ietf:params:xml:ns:epp-1.0">
<response>
<result code="1001">
<msg>Command completed successfully; action pending</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-09T22:00:00.0Z</domain:reDate>
<domain:acID>TheRegistrar</domain:acID>
<domain:acDate>2000-06-14T22:00:00.0Z</domain:acDate>
<domain:exDate>2001-09-08T22:00:00.0Z</domain:exDate>
</domain:trnData>
</resData>
<trID>
<clTRID>ABC-12345</clTRID>
<svTRID>server-trid</svTRID>
</trID>
</response>
</epp>

View file

@ -0,0 +1,23 @@
<epp xmlns="urn:ietf:params:xml:ns:epp-1.0">
<response>
<result code="1001">
<msg>Command completed successfully; action pending</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-09T22:00:00.0Z</domain:reDate>
<domain:acID>TheRegistrar</domain:acID>
<domain:acDate>2000-06-09T22:00:00.0Z</domain:acDate>
<domain:exDate>2001-09-08T22:00:00.0Z</domain:exDate>
</domain:trnData>
</resData>
<trID>
<clTRID>ABC-12345</clTRID>
<svTRID>server-trid</svTRID>
</trID>
</response>
</epp>

View file

@ -0,0 +1,21 @@
<epp xmlns="urn:ietf:params:xml:ns:epp-1.0">
<command>
<transfer op="request">
<domain:transfer
xmlns:domain="urn:ietf:params:xml:ns:domain-1.0">
<domain:name>example.tld</domain:name>
<domain:authInfo>
<domain:pw roid="JD1234-REP">2fooBAR</domain:pw>
</domain:authInfo>
</domain:transfer>
</transfer>
<extension>
<superuser:domainTransferRequest xmlns:superuser="urn:google:params:xml:ns:superuser-1.0">
<superuser:renewalPeriod unit="y">%PERIOD%</superuser:renewalPeriod>
<superuser:automaticTransferLength>%AUTOMATIC_TRANSFER_LENGTH%
</superuser:automaticTransferLength>
</superuser:domainTransferRequest>
</extension>
<clTRID>ABC-12345</clTRID>
</command>
</epp>

View file

@ -920,6 +920,7 @@ class google.registry.model.transfer.TransferData {
com.googlecode.objectify.Key<google.registry.model.billing.BillingEvent$OneTime> serverApproveBillingEvent; com.googlecode.objectify.Key<google.registry.model.billing.BillingEvent$OneTime> serverApproveBillingEvent;
com.googlecode.objectify.Key<google.registry.model.billing.BillingEvent$Recurring> serverApproveAutorenewEvent; com.googlecode.objectify.Key<google.registry.model.billing.BillingEvent$Recurring> serverApproveAutorenewEvent;
com.googlecode.objectify.Key<google.registry.model.poll.PollMessage$Autorenew> serverApproveAutorenewPollMessage; com.googlecode.objectify.Key<google.registry.model.poll.PollMessage$Autorenew> serverApproveAutorenewPollMessage;
google.registry.model.domain.Period transferPeriod;
google.registry.model.eppcommon.Trid transferRequestTrid; google.registry.model.eppcommon.Trid transferRequestTrid;
google.registry.model.transfer.TransferStatus transferStatus; google.registry.model.transfer.TransferStatus transferStatus;
java.lang.String gainingClientId; java.lang.String gainingClientId;