mirror of
https://github.com/google/nomulus.git
synced 2025-05-13 16:07:15 +02:00
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:
parent
263aea3b2a
commit
2e4b63bb79
28 changed files with 1018 additions and 124 deletions
|
@ -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.
|
||||||
|
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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(
|
||||||
|
|
|
@ -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());
|
||||||
|
|
|
@ -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());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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.");
|
||||||
|
}
|
||||||
|
}
|
|
@ -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.");
|
||||||
|
}
|
||||||
|
}
|
|
@ -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.");
|
||||||
|
}
|
||||||
|
}
|
|
@ -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.");
|
||||||
|
}
|
||||||
|
}
|
|
@ -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.
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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 {}
|
|
@ -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;
|
|
@ -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;
|
||||||
|
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -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)
|
||||||
|
|
41
java/google/registry/xml/xsd/superuser.xsd
Normal file
41
java/google/registry/xml/xsd/superuser.xsd
Normal 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>
|
|
@ -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) {
|
||||||
|
|
|
@ -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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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");
|
||||||
|
|
23
javatests/google/registry/flows/domain/testdata/domain_transfer_approve_response_zero_period.xml
vendored
Normal file
23
javatests/google/registry/flows/domain/testdata/domain_transfer_approve_response_zero_period.xml
vendored
Normal 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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
21
javatests/google/registry/flows/domain/testdata/domain_transfer_request_superuser_extension.xml
vendored
Normal file
21
javatests/google/registry/flows/domain/testdata/domain_transfer_request_superuser_extension.xml
vendored
Normal 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>
|
|
@ -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;
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue