diff --git a/java/google/registry/tools/RegistryTool.java b/java/google/registry/tools/RegistryTool.java index eee918d55..de9eda05d 100644 --- a/java/google/registry/tools/RegistryTool.java +++ b/java/google/registry/tools/RegistryTool.java @@ -104,6 +104,7 @@ public final class RegistryTool { .put("registrar_activity_report", RegistrarActivityReportCommand.class) .put("registrar_contact", RegistrarContactCommand.class) .put("remove_ip_address", RemoveIpAddressCommand.class) + .put("renew_domain", RenewDomainCommand.class) .put("resave_entities", ResaveEntitiesCommand.class) .put("resave_environment_entities", ResaveEnvironmentEntitiesCommand.class) .put("resave_epp_resource", ResaveEppResourceCommand.class) diff --git a/java/google/registry/tools/RegistryToolComponent.java b/java/google/registry/tools/RegistryToolComponent.java index 99f97a337..fd688c6c6 100644 --- a/java/google/registry/tools/RegistryToolComponent.java +++ b/java/google/registry/tools/RegistryToolComponent.java @@ -94,6 +94,7 @@ interface RegistryToolComponent { void inject(LoginCommand command); void inject(LogoutCommand command); void inject(PendingEscrowCommand command); + void inject(RenewDomainCommand command); void inject(SendEscrowReportToIcannCommand command); void inject(SetupOteCommand command); void inject(UpdateCursorsCommand command); diff --git a/java/google/registry/tools/RenewDomainCommand.java b/java/google/registry/tools/RenewDomainCommand.java new file mode 100644 index 000000000..6cb956527 --- /dev/null +++ b/java/google/registry/tools/RenewDomainCommand.java @@ -0,0 +1,70 @@ +// Copyright 2018 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.tools; + +import static com.google.common.base.Preconditions.checkArgument; +import static google.registry.model.EppResourceUtils.loadByForeignKey; +import static google.registry.util.CollectionUtils.findDuplicates; +import static google.registry.util.PreconditionsUtils.checkArgumentNotNull; + +import com.beust.jcommander.Parameter; +import com.beust.jcommander.Parameters; +import com.google.common.base.Joiner; +import com.google.template.soy.data.SoyMapData; +import google.registry.model.domain.DomainResource; +import google.registry.tools.soy.RenewDomainSoyInfo; +import google.registry.util.Clock; +import java.util.List; +import javax.inject.Inject; +import org.joda.time.DateTime; +import org.joda.time.format.DateTimeFormat; +import org.joda.time.format.DateTimeFormatter; + +/** A command to renew domain(s) via EPP. */ +@Parameters(separators = " =", commandDescription = "Renew domain(s) via EPP.") +final class RenewDomainCommand extends MutatingEppToolCommand { + + @Parameter( + names = "--period", + description = "Number of years to renew the registration for (defaults to 1).") + private int period = 1; + + @Parameter(description = "Names of the domains to renew.", required = true) + private List mainParameters; + + @Inject + Clock clock; + + private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormat.forPattern("YYYY-MM-dd"); + + @Override + protected void initMutatingEppToolCommand() { + String duplicates = Joiner.on(", ").join(findDuplicates(mainParameters)); + checkArgument(duplicates.isEmpty(), "Duplicate domain arguments found: '%s'", duplicates); + checkArgument(period < 10, "Cannot renew domains for 10 or more years"); + DateTime now = clock.nowUtc(); + for (String domainName : mainParameters) { + DomainResource domain = loadByForeignKey(DomainResource.class, domainName, now); + checkArgumentNotNull(domain, "Domain '%s' does not exist or is deleted", domainName); + setSoyTemplate(RenewDomainSoyInfo.getInstance(), RenewDomainSoyInfo.RENEWDOMAIN); + addSoyRecord( + domain.getCurrentSponsorClientId(), + new SoyMapData( + "domainName", domain.getFullyQualifiedDomainName(), + "expirationDate", domain.getRegistrationExpirationTime().toString(DATE_FORMATTER), + "period", String.valueOf(period))); + } + } +} diff --git a/java/google/registry/tools/soy/RenewDomain.soy b/java/google/registry/tools/soy/RenewDomain.soy new file mode 100644 index 000000000..e0ec5c53d --- /dev/null +++ b/java/google/registry/tools/soy/RenewDomain.soy @@ -0,0 +1,38 @@ +// Copyright 2018 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. + +{namespace domain.registry.tools} + +/** + * Renew domain request + */ +{template .renewdomain stricthtml="false"} +{@param domainName: string} +{@param expirationDate: string} +{@param period: string} + + + + + + {$domainName} + {$expirationDate} + {$period} + + + RegistryTool + + +{/template} diff --git a/javatests/google/registry/testing/DatastoreHelper.java b/javatests/google/registry/testing/DatastoreHelper.java index c836097ed..8c4b6de4a 100644 --- a/javatests/google/registry/testing/DatastoreHelper.java +++ b/javatests/google/registry/testing/DatastoreHelper.java @@ -316,6 +316,16 @@ public class DatastoreHelper { newDomainResource(domainName).asBuilder().setCreationTimeForTest(creationTime).build()); } + public static DomainResource persistActiveDomain( + String domainName, DateTime creationTime, DateTime expirationTime) { + return persistResource( + newDomainResource(domainName) + .asBuilder() + .setCreationTimeForTest(creationTime) + .setRegistrationExpirationTime(expirationTime) + .build()); + } + public static DomainApplication persistActiveDomainApplication(String domainName) { return persistResource(newDomainApplication(domainName)); } diff --git a/javatests/google/registry/tools/RenewDomainCommandTest.java b/javatests/google/registry/tools/RenewDomainCommandTest.java new file mode 100644 index 000000000..112df9901 --- /dev/null +++ b/javatests/google/registry/tools/RenewDomainCommandTest.java @@ -0,0 +1,140 @@ +// Copyright 2018 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.tools; + +import static com.google.common.truth.Truth.assertThat; +import static google.registry.testing.DatastoreHelper.persistActiveDomain; +import static google.registry.testing.DatastoreHelper.persistDeletedDomain; +import static google.registry.testing.DatastoreHelper.persistResource; +import static google.registry.testing.JUnitBackports.assertThrows; + +import com.beust.jcommander.ParameterException; +import com.google.common.collect.ImmutableMap; +import google.registry.model.ofy.Ofy; +import google.registry.testing.FakeClock; +import google.registry.testing.InjectRule; +import google.registry.util.Clock; +import org.joda.time.DateTime; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; + +/** Unit tests for {@link RenewDomainCommand}. */ +public class RenewDomainCommandTest extends EppToolCommandTestCase { + + @Rule public final InjectRule inject = new InjectRule(); + + private final Clock clock = new FakeClock(DateTime.parse("2015-04-05T05:05:05Z")); + + @Before + public void before() { + inject.setStaticField(Ofy.class, "clock", clock); + command.clock = clock; + } + + @Test + public void testSuccess() throws Exception { + persistResource( + persistActiveDomain( + "domain.tld", + DateTime.parse("2014-09-05T05:05:05Z"), + DateTime.parse("2015-09-05T05:05:05Z")) + .asBuilder() + .setPersistedCurrentSponsorClientId("NewRegistrar") + .build()); + runCommandForced("domain.tld"); + eppVerifier + .expectClientId("NewRegistrar") + .verifySent( + "domain_renew.xml", + ImmutableMap.of("DOMAIN", "domain.tld", "EXPDATE", "2015-09-05", "YEARS", "1")) + .verifyNoMoreSent(); + } + + @Test + public void testSuccess_multipleDomains() throws Exception { + persistActiveDomain( + "domain1.tld", + DateTime.parse("2014-09-05T05:05:05Z"), + DateTime.parse("2015-09-05T05:05:05Z")); + persistActiveDomain( + "domain2.tld", + DateTime.parse("2014-11-05T05:05:05Z"), + DateTime.parse("2015-11-05T05:05:05Z")); + persistActiveDomain( + "domain3.tld", + DateTime.parse("2015-01-05T05:05:05Z"), + DateTime.parse("2016-01-05T05:05:05Z")); + runCommandForced("--period 3", "domain1.tld", "domain2.tld", "domain3.tld"); + eppVerifier + .expectClientId("TheRegistrar") + .verifySent( + "domain_renew.xml", + ImmutableMap.of("DOMAIN", "domain1.tld", "EXPDATE", "2015-09-05", "YEARS", "3")) + .verifySent( + "domain_renew.xml", + ImmutableMap.of("DOMAIN", "domain2.tld", "EXPDATE", "2015-11-05", "YEARS", "3")) + .verifySent( + "domain_renew.xml", + ImmutableMap.of("DOMAIN", "domain3.tld", "EXPDATE", "2016-01-05", "YEARS", "3")) + .verifyNoMoreSent(); + } + + @Test + public void testFailure_domainDoesntExist() { + IllegalArgumentException e = + assertThrows(IllegalArgumentException.class, () -> runCommandForced("nonexistent.tld")); + assertThat(e) + .hasMessageThat() + .isEqualTo("Domain 'nonexistent.tld' does not exist or is deleted"); + } + + @Test + public void testFailure_domainIsDeleted() { + persistDeletedDomain("deleted.tld", DateTime.parse("2012-10-05T05:05:05Z")); + IllegalArgumentException e = + assertThrows(IllegalArgumentException.class, () -> runCommandForced("deleted.tld")); + assertThat(e) + .hasMessageThat() + .isEqualTo("Domain 'deleted.tld' does not exist or is deleted"); + } + + @Test + public void testFailure_duplicateDomainSpecified() { + IllegalArgumentException e = + assertThrows( + IllegalArgumentException.class, () -> runCommandForced("dupe.tld", "dupe.tld")); + assertThat(e).hasMessageThat().isEqualTo("Duplicate domain arguments found: 'dupe.tld'"); + } + + @Test + public void testFailure_cantRenewForTenYears() { + persistActiveDomain( + "domain.tld", + DateTime.parse("2014-09-05T05:05:05Z"), + DateTime.parse("2015-09-05T05:05:05Z")); + IllegalArgumentException e = + assertThrows( + IllegalArgumentException.class, () -> runCommandForced("domain.tld", "--period 10")); + assertThat(e) + .hasMessageThat() + .isEqualTo("Cannot renew domains for 10 or more years"); + } + + @Test + public void testFailure_missingDomainNames() { + assertThrows(ParameterException.class, () -> runCommand("--period 4")); + } +} diff --git a/javatests/google/registry/tools/server/testdata/domain_renew.xml b/javatests/google/registry/tools/server/testdata/domain_renew.xml new file mode 100644 index 000000000..7b0f801ff --- /dev/null +++ b/javatests/google/registry/tools/server/testdata/domain_renew.xml @@ -0,0 +1,13 @@ + + + + + %DOMAIN% + %EXPDATE% + %YEARS% + + + RegistryTool + +