diff --git a/docs/flows.md b/docs/flows.md index 64664856b..fde936085 100644 --- a/docs/flows.md +++ b/docs/flows.md @@ -332,6 +332,7 @@ new ones with the correct approval time). * The requested domain name is on the premium price list, and this registrar has blocked premium registrations. * 2306 + * Domain transfer period must be one year. * Periods for domain registrations must be specified in years. * The requested fees cannot be provided in the requested currency. diff --git a/java/google/registry/flows/domain/DomainTransferRequestFlow.java b/java/google/registry/flows/domain/DomainTransferRequestFlow.java index 58e0d6eb6..a1ff6ab77 100644 --- a/java/google/registry/flows/domain/DomainTransferRequestFlow.java +++ b/java/google/registry/flows/domain/DomainTransferRequestFlow.java @@ -44,6 +44,7 @@ import google.registry.flows.FlowModule.TargetId; import google.registry.flows.TransactionalFlow; import google.registry.flows.exceptions.AlreadyPendingTransferException; import google.registry.flows.exceptions.ObjectAlreadySponsoredException; +import google.registry.flows.exceptions.TransferPeriodMustBeOneYearException; import google.registry.model.domain.DomainCommand.Transfer; import google.registry.model.domain.DomainResource; import google.registry.model.domain.Period; @@ -89,6 +90,7 @@ import org.joda.time.DateTime; * @error {@link google.registry.flows.exceptions.MissingTransferRequestAuthInfoException} * @error {@link google.registry.flows.exceptions.ObjectAlreadySponsoredException} * @error {@link google.registry.flows.exceptions.ResourceStatusProhibitsOperationException} + * @error {@link google.registry.flows.exceptions.TransferPeriodMustBeOneYearException} * @error {@link DomainFlowUtils.BadPeriodUnitException} * @error {@link DomainFlowUtils.CurrencyUnitMismatchException} * @error {@link DomainFlowUtils.CurrencyValueScaleException} @@ -216,12 +218,39 @@ public final class DomainTransferRequestFlow implements TransactionalFlow { throw new ObjectAlreadySponsoredException(); } checkAllowedAccessToTld(gainingClientId, existingDomain.getTld()); - verifyUnitIsYears(period); + verifyTransferPeriodIsOneYear(period); if (!isSuperuser) { verifyPremiumNameIsNotBlocked(targetId, now, gainingClientId); } } + /** + * Verify that the transfer period is one year. + * + *
Restricting transfers to one year is seemingly required by ICANN's Policy on Transfer of + * Registrations between Registrars, section A.8. It states that "the completion by Registry + * Operator of a holder-authorized transfer under this Part A shall result in a one-year extension + * of the existing registration, provided that in no event shall the total unexpired term of a + * registration exceed ten (10) years." + * + *
Even if not required, this policy is desirable because it dramatically simplifies the logic + * in transfer flows. Registrars appear to never request 2+ year transfers in practice, and they + * can always decompose an multi-year transfer into a 1-year transfer followed by a manual renewal + * afterwards. The EPP Domain RFC, + * section 3.2.4 says about EPP transfer periods that "the number of units available MAY be + * subject to limits imposed by the server" so we're just limiting the units to one. + * + *
Note that clients can omit the period element from the transfer EPP entirely, but then it
+ * will simply default to one year.
+ */
+ private static void verifyTransferPeriodIsOneYear(Period period) throws EppException {
+ verifyUnitIsYears(period);
+ if (period.getValue() != 1) {
+ throw new TransferPeriodMustBeOneYearException();
+ }
+ }
+
private HistoryEntry buildHistory(Period period, DomainResource existingResource, DateTime now) {
return historyBuilder
.setType(HistoryEntry.Type.DOMAIN_TRANSFER_REQUEST)
diff --git a/java/google/registry/flows/exceptions/TransferPeriodMustBeOneYearException.java b/java/google/registry/flows/exceptions/TransferPeriodMustBeOneYearException.java
new file mode 100644
index 000000000..c4d261544
--- /dev/null
+++ b/java/google/registry/flows/exceptions/TransferPeriodMustBeOneYearException.java
@@ -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.ParameterValuePolicyErrorException;
+
+/** Domain transfer period must be one year. */
+public class TransferPeriodMustBeOneYearException extends ParameterValuePolicyErrorException {
+ public TransferPeriodMustBeOneYearException() {
+ super("Domain transfer period must be one year");
+ }
+}
diff --git a/javatests/google/registry/flows/EppLifecycleDomainTest.java b/javatests/google/registry/flows/EppLifecycleDomainTest.java
index b6d81449d..f4d8efaf8 100644
--- a/javatests/google/registry/flows/EppLifecycleDomainTest.java
+++ b/javatests/google/registry/flows/EppLifecycleDomainTest.java
@@ -373,7 +373,8 @@ public class EppLifecycleDomainTest extends EppTestCase {
}
@Test
- public void testIgnoredTransferDuringAutoRenewPeriod_succeeds() throws Exception {
+ public void testTransfer_autoRenewGraceActive_onlyAtAutomaticTransferTime_getsSubsumed()
+ throws Exception {
// Register the domain as the first registrar.
assertCommandAndResponse("login_valid.xml", "login_response.xml");
createFakesite();
@@ -382,8 +383,8 @@ public class EppLifecycleDomainTest extends EppTestCase {
// Request a transfer of the domain to the second registrar.
assertCommandAndResponse("login2_valid.xml", "login_response.xml");
assertCommandAndResponse(
- "domain_transfer_request_2_years.xml",
- "domain_transfer_response_2_years.xml",
+ "domain_transfer_request.xml",
+ "domain_transfer_response.xml",
DateTime.parse("2002-05-30T00:00:00Z"));
assertCommandAndResponse("logout.xml", "logout_response.xml");
@@ -402,7 +403,8 @@ public class EppLifecycleDomainTest extends EppTestCase {
// Log back in as the second registrar and verify transfer details.
assertCommandAndResponse("login2_valid.xml", "login_response.xml");
- // Verify that domain is in the transfer period now with expiration date two years out.
+ // Verify that domain is in the transfer period now with expiration date still one year out,
+ // since the transfer should subsume the autorenew that happened during the transfer window.
assertCommandAndResponse(
"domain_info_fakesite.xml",
"domain_info_response_fakesite_transfer_period.xml",
@@ -425,8 +427,8 @@ public class EppLifecycleDomainTest extends EppTestCase {
// Request a transfer of the domain to the second registrar.
assertCommandAndResponse("login2_valid.xml", "login_response.xml");
assertCommandAndResponse(
- "domain_transfer_request_2_years.xml",
- "domain_transfer_response_2_years.xml",
+ "domain_transfer_request.xml",
+ "domain_transfer_response.xml",
DateTime.parse("2002-05-30T00:00:00Z"));
assertCommandAndResponse("logout.xml", "logout_response.xml");
@@ -464,8 +466,8 @@ public class EppLifecycleDomainTest extends EppTestCase {
// Request a transfer of the domain to the second registrar.
assertCommandAndResponse("login2_valid.xml", "login_response.xml");
assertCommandAndResponse(
- "domain_transfer_request_2_years.xml",
- "domain_transfer_response_2_years.xml",
+ "domain_transfer_request.xml",
+ "domain_transfer_response.xml",
DateTime.parse("2002-05-30T00:00:00Z"));
assertCommandAndResponse("logout.xml", "logout_response.xml");
diff --git a/javatests/google/registry/flows/domain/DomainTransferRequestFlowTest.java b/javatests/google/registry/flows/domain/DomainTransferRequestFlowTest.java
index 85d7f1f0e..58b8ed0d3 100644
--- a/javatests/google/registry/flows/domain/DomainTransferRequestFlowTest.java
+++ b/javatests/google/registry/flows/domain/DomainTransferRequestFlowTest.java
@@ -53,6 +53,7 @@ import google.registry.flows.exceptions.AlreadyPendingTransferException;
import google.registry.flows.exceptions.MissingTransferRequestAuthInfoException;
import google.registry.flows.exceptions.ObjectAlreadySponsoredException;
import google.registry.flows.exceptions.ResourceStatusProhibitsOperationException;
+import google.registry.flows.exceptions.TransferPeriodMustBeOneYearException;
import google.registry.model.billing.BillingEvent;
import google.registry.model.billing.BillingEvent.Cancellation.Builder;
import google.registry.model.billing.BillingEvent.Reason;
@@ -500,22 +501,35 @@ public class DomainTransferRequestFlowTest
}
@Test
- public void testSuccess_missingPeriod() throws Exception {
+ public void testSuccess_missingPeriod_defaultsToOneYear() throws Exception {
setupDomain("example", "tld");
- doSuccessfulTest("domain_transfer_request_missing_period.xml",
+ doSuccessfulTest(
+ "domain_transfer_request_missing_period.xml",
"domain_transfer_request_response.xml");
}
+ @Test
+ public void testFailure_multiYearPeriod() throws Exception {
+ setupDomain("example", "tld");
+ clock.advanceOneMilli();
+ thrown.expect(TransferPeriodMustBeOneYearException.class);
+ doFailingTest("domain_transfer_request_2_years.xml");
+ }
+
@Test
public void testSuccess_cappedExpiration() throws Exception {
setupDomain("example", "tld");
- // The current expiration is in 15 months, so requesting 10 years would give 11 years 3 months,
- // were it not that we cap at 10 years. (MAX_REGISTRATION_YEARS == 10 and is unlikely to ever
- // change; we just use a constant for readability.)
+ // Set the domain to expire 10 years from now (as if it were just created with a 10-year term).
+ domain = persistResource(domain.asBuilder()
+ .setRegistrationExpirationTime(clock.nowUtc().plusYears(10))
+ .build());
+ // New expiration time should be capped at exactly 10 years from the transfer server-approve
+ // time, so the domain only ends up gaining the 5-day transfer window's worth of extra
+ // registration time.
clock.advanceOneMilli();
doSuccessfulTest(
- "domain_transfer_request_10_years.xml",
- "domain_transfer_request_response_10_years.xml",
+ "domain_transfer_request.xml",
+ "domain_transfer_request_response_10_year_cap.xml",
clock.nowUtc().plus(Registry.get("tld").getAutomaticTransferLength()).plusYears(10));
}
@@ -534,16 +548,16 @@ public class DomainTransferRequestFlowTest
doSuccessfulTest(
"domain_transfer_request_fee.xml",
"domain_transfer_request_response_fees.xml",
- domain.getRegistrationExpirationTime().plusYears(3),
+ domain.getRegistrationExpirationTime().plusYears(1),
new ImmutableMap.Builder