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() .put("DOMAIN", "expensive-domain.foo") - .put("YEARS", "3") - .put("AMOUNT", "133.00") - .put("EXDATE", "2004-09-08T22:00:00.0Z") + .put("YEARS", "1") + .put("AMOUNT", "111.00") + .put("EXDATE", "2002-09-08T22:00:00.0Z") .put("FEE_VERSION", "0.6") .put("FEE_NS", "fee") .build(), - Optional.of(Money.of(USD, 133))); + Optional.of(Money.of(USD, 111))); } @Test diff --git a/javatests/google/registry/flows/domain/testdata/domain_transfer_request_10_years.xml b/javatests/google/registry/flows/domain/testdata/domain_transfer_request_10_years.xml deleted file mode 100644 index 0d86e10a5..000000000 --- a/javatests/google/registry/flows/domain/testdata/domain_transfer_request_10_years.xml +++ /dev/null @@ -1,15 +0,0 @@ - - - - - example.tld - 10 - - 2fooBAR - - - - ABC-12345 - - diff --git a/javatests/google/registry/flows/domain/testdata/domain_transfer_request_response_10_years.xml b/javatests/google/registry/flows/domain/testdata/domain_transfer_request_response_10_year_cap.xml similarity index 100% rename from javatests/google/registry/flows/domain/testdata/domain_transfer_request_response_10_years.xml rename to javatests/google/registry/flows/domain/testdata/domain_transfer_request_response_10_year_cap.xml diff --git a/javatests/google/registry/flows/domain/testdata/domain_transfer_request_response_fees.xml b/javatests/google/registry/flows/domain/testdata/domain_transfer_request_response_fees.xml index dcce1ed03..da7f26795 100644 --- a/javatests/google/registry/flows/domain/testdata/domain_transfer_request_response_fees.xml +++ b/javatests/google/registry/flows/domain/testdata/domain_transfer_request_response_fees.xml @@ -18,7 +18,7 @@ USD - 33.00 + 11.00 100.00 diff --git a/javatests/google/registry/flows/testdata/domain_info_response_fakesite_3_nameservers_transfer_successful.xml b/javatests/google/registry/flows/testdata/domain_info_response_fakesite_3_nameservers_transfer_successful.xml index d085e5b59..8cce591ed 100644 --- a/javatests/google/registry/flows/testdata/domain_info_response_fakesite_3_nameservers_transfer_successful.xml +++ b/javatests/google/registry/flows/testdata/domain_info_response_fakesite_3_nameservers_transfer_successful.xml @@ -23,7 +23,7 @@ 2000-06-01T00:04:00Z NewRegistrar 2000-06-08T00:00:00Z - 2004-06-01T00:04:00Z + 2003-06-01T00:04:00Z 2002-06-04T00:00:00Z 2fooBAR diff --git a/javatests/google/registry/flows/testdata/domain_info_response_fakesite_transfer_complete.xml b/javatests/google/registry/flows/testdata/domain_info_response_fakesite_transfer_complete.xml index 254c13631..455b6c3cd 100644 --- a/javatests/google/registry/flows/testdata/domain_info_response_fakesite_transfer_complete.xml +++ b/javatests/google/registry/flows/testdata/domain_info_response_fakesite_transfer_complete.xml @@ -19,7 +19,7 @@ TheRegistrar NewRegistrar 2000-06-01T00:04:00Z - 2004-06-01T00:04:00Z + 2003-06-01T00:04:00Z 2002-06-04T00:00:00Z 2fooBAR diff --git a/javatests/google/registry/flows/testdata/domain_info_response_fakesite_transfer_period.xml b/javatests/google/registry/flows/testdata/domain_info_response_fakesite_transfer_period.xml index 049343940..66ad7d004 100644 --- a/javatests/google/registry/flows/testdata/domain_info_response_fakesite_transfer_period.xml +++ b/javatests/google/registry/flows/testdata/domain_info_response_fakesite_transfer_period.xml @@ -19,7 +19,7 @@ TheRegistrar NewRegistrar 2000-06-01T00:04:00Z - 2004-06-01T00:04:00Z + 2003-06-01T00:04:00Z 2002-06-04T00:00:00Z 2fooBAR diff --git a/javatests/google/registry/flows/testdata/domain_transfer_request_2_years.xml b/javatests/google/registry/flows/testdata/domain_transfer_request.xml similarity index 88% rename from javatests/google/registry/flows/testdata/domain_transfer_request_2_years.xml rename to javatests/google/registry/flows/testdata/domain_transfer_request.xml index 8bda67620..d1f6fb298 100644 --- a/javatests/google/registry/flows/testdata/domain_transfer_request_2_years.xml +++ b/javatests/google/registry/flows/testdata/domain_transfer_request.xml @@ -4,7 +4,7 @@ fakesite.example - 2 + 1 2fooBAR diff --git a/javatests/google/registry/flows/testdata/domain_transfer_response_2_years.xml b/javatests/google/registry/flows/testdata/domain_transfer_response.xml similarity index 92% rename from javatests/google/registry/flows/testdata/domain_transfer_response_2_years.xml rename to javatests/google/registry/flows/testdata/domain_transfer_response.xml index 5f3742d67..aaa677e8a 100644 --- a/javatests/google/registry/flows/testdata/domain_transfer_response_2_years.xml +++ b/javatests/google/registry/flows/testdata/domain_transfer_response.xml @@ -11,7 +11,7 @@ 2002-05-30T00:00:00Z NewRegistrar 2002-06-04T00:00:00Z - 2004-06-01T00:04:00Z + 2003-06-01T00:04:00Z