Add an unrenew_domain command to nomulus tool

This is used to reduce the expiration time of domain(s) by some number of years
(if enough length remains in the registration term to do so). This does not back
out the previously saved BillingEvent entities as they may have already been
sent out and invoiced, so any related refunds must be handled out of band.

In addition to reducing the registration expiration time on the domain itself,
this command writes out a new history entry, one-time poll message informing the
registrar of this change, auto-renew billing event and poll message, and
updates/ends the old auto-renew billing event and poll message.

-------------
Created by MOE: https://github.com/google/moe
MOE_MIGRATED_REVID=224999285
This commit is contained in:
mcilwain 2018-12-11 07:31:30 -08:00 committed by jianglai
parent 9c706e79fd
commit f58211402a
21 changed files with 801 additions and 15 deletions

View file

@ -520,7 +520,7 @@ public class DomainFlowUtils {
* Fills in a builder with the data needed for an autorenew billing event for this domain. This * Fills in a builder with the data needed for an autorenew billing event for this domain. This
* does not copy over the id of the current autorenew billing event. * does not copy over the id of the current autorenew billing event.
*/ */
static BillingEvent.Recurring.Builder newAutorenewBillingEvent(DomainResource domain) { public static BillingEvent.Recurring.Builder newAutorenewBillingEvent(DomainResource domain) {
return new BillingEvent.Recurring.Builder() return new BillingEvent.Recurring.Builder()
.setReason(Reason.RENEW) .setReason(Reason.RENEW)
.setFlags(ImmutableSet.of(Flag.AUTO_RENEW)) .setFlags(ImmutableSet.of(Flag.AUTO_RENEW))
@ -533,7 +533,7 @@ public class DomainFlowUtils {
* Fills in a builder with the data needed for an autorenew poll message for this domain. This * Fills in a builder with the data needed for an autorenew poll message for this domain. This
* does not copy over the id of the current autorenew poll message. * does not copy over the id of the current autorenew poll message.
*/ */
static PollMessage.Autorenew.Builder newAutorenewPollMessage(DomainResource domain) { public static PollMessage.Autorenew.Builder newAutorenewPollMessage(DomainResource domain) {
return new PollMessage.Autorenew.Builder() return new PollMessage.Autorenew.Builder()
.setTargetId(domain.getFullyQualifiedDomainName()) .setTargetId(domain.getFullyQualifiedDomainName())
.setClientId(domain.getCurrentSponsorClientId()) .setClientId(domain.getCurrentSponsorClientId())
@ -542,12 +542,14 @@ public class DomainFlowUtils {
} }
/** /**
* Re-saves the current autorenew billing event and poll message with a new end time. This may end * Re-saves the current autorenew billing event and poll message with a new end time.
* up deleting the poll message (if closing the message interval) or recreating it (if opening the *
* message interval). * <p>This may end up deleting the poll message (if closing the message interval) or recreating it
* (if opening the message interval). This may cause an autorenew billing event to have an end
* time earlier than its event time (i.e. if it's being ended before it was ever triggered).
*/ */
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
static void updateAutorenewRecurrenceEndTime(DomainResource domain, DateTime newEndTime) { public static void updateAutorenewRecurrenceEndTime(DomainResource domain, DateTime newEndTime) {
Optional<PollMessage.Autorenew> autorenewPollMessage = Optional<PollMessage.Autorenew> autorenewPollMessage =
Optional.ofNullable(ofy().load().key(domain.getAutorenewPollMessage()).now()); Optional.ofNullable(ofy().load().key(domain.getAutorenewPollMessage()).now());

View file

@ -226,7 +226,7 @@ public final class DomainRenewFlow implements TransactionalFlow {
.setType(HistoryEntry.Type.DOMAIN_RENEW) .setType(HistoryEntry.Type.DOMAIN_RENEW)
.setPeriod(period) .setPeriod(period)
.setModificationTime(now) .setModificationTime(now)
.setParent(Key.create(existingDomain)) .setParent(existingDomain)
.setDomainTransactionRecords( .setDomainTransactionRecords(
ImmutableSet.of( ImmutableSet.of(
DomainTransactionRecord.create( DomainTransactionRecord.create(

View file

@ -64,6 +64,7 @@ public final class GtechTool {
"setup_ote", "setup_ote",
"uniform_rapid_suspension", "uniform_rapid_suspension",
"unlock_domain", "unlock_domain",
"unrenew_domain",
"update_domain", "update_domain",
"update_registrar", "update_registrar",
"update_sandbox_tld", "update_sandbox_tld",

View file

@ -108,6 +108,7 @@ public final class RegistryTool {
.put("setup_ote", SetupOteCommand.class) .put("setup_ote", SetupOteCommand.class)
.put("uniform_rapid_suspension", UniformRapidSuspensionCommand.class) .put("uniform_rapid_suspension", UniformRapidSuspensionCommand.class)
.put("unlock_domain", UnlockDomainCommand.class) .put("unlock_domain", UnlockDomainCommand.class)
.put("unrenew_domain", UnrenewDomainCommand.class)
.put("update_application_status", UpdateApplicationStatusCommand.class) .put("update_application_status", UpdateApplicationStatusCommand.class)
.put("update_claims_notice", UpdateClaimsNoticeCommand.class) .put("update_claims_notice", UpdateClaimsNoticeCommand.class)
.put("update_cursors", UpdateCursorsCommand.class) .put("update_cursors", UpdateCursorsCommand.class)

View file

@ -101,6 +101,7 @@ interface RegistryToolComponent {
void inject(SetNumInstancesCommand command); void inject(SetNumInstancesCommand command);
void inject(SetupOteCommand command); void inject(SetupOteCommand command);
void inject(UnlockDomainCommand command); void inject(UnlockDomainCommand command);
void inject(UnrenewDomainCommand command);
void inject(UpdateCursorsCommand command); void inject(UpdateCursorsCommand command);
void inject(UpdateDomainCommand command); void inject(UpdateDomainCommand command);
void inject(UpdateKmsKeyringCommand command); void inject(UpdateKmsKeyringCommand command);

View file

@ -37,7 +37,7 @@ import org.joda.time.format.DateTimeFormatter;
final class RenewDomainCommand extends MutatingEppToolCommand { final class RenewDomainCommand extends MutatingEppToolCommand {
@Parameter( @Parameter(
names = "--period", names = {"-p", "--period"},
description = "Number of years to renew the registration for (defaults to 1).") description = "Number of years to renew the registration for (defaults to 1).")
private int period = 1; private int period = 1;

View file

@ -0,0 +1,225 @@
// 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 com.google.common.base.Preconditions.checkState;
import static google.registry.flows.domain.DomainFlowUtils.newAutorenewBillingEvent;
import static google.registry.flows.domain.DomainFlowUtils.newAutorenewPollMessage;
import static google.registry.flows.domain.DomainFlowUtils.updateAutorenewRecurrenceEndTime;
import static google.registry.model.EppResourceUtils.loadByForeignKey;
import static google.registry.model.ofy.ObjectifyService.ofy;
import static google.registry.util.DateTimeUtils.isBeforeOrAt;
import static google.registry.util.DateTimeUtils.leapSafeSubtractYears;
import com.beust.jcommander.Parameter;
import com.beust.jcommander.Parameters;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableMultimap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Sets;
import com.googlecode.objectify.Key;
import google.registry.model.billing.BillingEvent;
import google.registry.model.domain.DomainResource;
import google.registry.model.domain.Period;
import google.registry.model.domain.Period.Unit;
import google.registry.model.eppcommon.StatusValue;
import google.registry.model.index.ForeignKeyIndex.ForeignKeyDomainIndex;
import google.registry.model.poll.PollMessage;
import google.registry.model.reporting.HistoryEntry;
import google.registry.model.reporting.HistoryEntry.Type;
import google.registry.util.Clock;
import google.registry.util.NonFinalForTesting;
import java.util.List;
import javax.inject.Inject;
import org.joda.time.DateTime;
/**
* Command to unrenew a domain.
*
* <p>This removes years off a domain's registration period. Note that the expiration time cannot be
* set to prior than the present. Reversal of the charges for these years (if desired) must happen
* out of band, as they may already have been billed out and thus cannot and won't be reversed in
* Datastore.
*/
@Parameters(separators = " =", commandDescription = "Unrenew a domain.")
@NonFinalForTesting
class UnrenewDomainCommand extends ConfirmingCommand implements CommandWithRemoteApi {
@Parameter(
names = {"-p", "--period"},
description = "Number of years to unrenew the registration for (defaults to 1).")
int period = 1;
@Parameter(description = "Names of the domains to unrenew.", required = true)
List<String> mainParameters;
@Inject Clock clock;
private static final ImmutableSet<StatusValue> DISALLOWED_STATUSES =
ImmutableSet.of(
StatusValue.PENDING_TRANSFER,
StatusValue.SERVER_RENEW_PROHIBITED,
StatusValue.SERVER_UPDATE_PROHIBITED);
@Override
protected void init() {
checkArgument(period >= 1 && period <= 9, "Period must be in the range 1-9");
DateTime now = clock.nowUtc();
ImmutableSet.Builder<String> domainsNonexistentBuilder = new ImmutableSet.Builder<>();
ImmutableSet.Builder<String> domainsDeletingBuilder = new ImmutableSet.Builder<>();
ImmutableMultimap.Builder<String, StatusValue> domainsWithDisallowedStatusesBuilder =
new ImmutableMultimap.Builder<>();
ImmutableMap.Builder<String, DateTime> domainsExpiringTooSoonBuilder =
new ImmutableMap.Builder<>();
for (String domainName : mainParameters) {
if (ofy().load().type(ForeignKeyDomainIndex.class).id(domainName).now() == null) {
domainsNonexistentBuilder.add(domainName);
continue;
}
DomainResource domain = loadByForeignKey(DomainResource.class, domainName, now);
if (domain == null || domain.getStatusValues().contains(StatusValue.PENDING_DELETE)) {
domainsDeletingBuilder.add(domainName);
continue;
}
domainsWithDisallowedStatusesBuilder.putAll(
domainName, Sets.intersection(domain.getStatusValues(), DISALLOWED_STATUSES));
if (isBeforeOrAt(
leapSafeSubtractYears(domain.getRegistrationExpirationTime(), period), now)) {
domainsExpiringTooSoonBuilder.put(domainName, domain.getRegistrationExpirationTime());
}
}
ImmutableSet<String> domainsNonexistent = domainsNonexistentBuilder.build();
ImmutableSet<String> domainsDeleting = domainsDeletingBuilder.build();
ImmutableMultimap<String, StatusValue> domainsWithDisallowedStatuses =
domainsWithDisallowedStatusesBuilder.build();
ImmutableMap<String, DateTime> domainsExpiringTooSoon = domainsExpiringTooSoonBuilder.build();
boolean foundInvalidDomains =
!(domainsNonexistent.isEmpty()
&& domainsDeleting.isEmpty()
&& domainsWithDisallowedStatuses.isEmpty()
&& domainsExpiringTooSoon.isEmpty());
if (foundInvalidDomains) {
System.err.print("Found domains that cannot be unrenewed for the following reasons:\n\n");
}
if (!domainsNonexistent.isEmpty()) {
System.err.printf("Domains that don't exist: %s\n\n", domainsNonexistent);
}
if (!domainsDeleting.isEmpty()) {
System.err.printf("Domains that are deleted or pending delete: %s\n\n", domainsDeleting);
}
if (!domainsWithDisallowedStatuses.isEmpty()) {
System.err.printf("Domains with disallowed statuses: %s\n\n", domainsWithDisallowedStatuses);
}
if (!domainsExpiringTooSoon.isEmpty()) {
System.err.printf("Domains expiring too soon: %s\n\n", domainsExpiringTooSoon);
}
checkArgument(!foundInvalidDomains, "Aborting because some domains cannot be unrewed");
}
@Override
protected String prompt() {
return String.format("Unrenew these domain(s) for %d years?", period);
}
@Override
protected String execute() {
for (String domainName : mainParameters) {
ofy().transact(() -> unrenewDomain(domainName));
System.out.printf("Unrenewed %s\n", domainName);
}
return "Successfully unrenewed all domains.";
}
private void unrenewDomain(String domainName) {
ofy().assertInTransaction();
DateTime now = ofy().getTransactionTime();
DomainResource domain = loadByForeignKey(DomainResource.class, domainName, now);
// Transactional sanity checks on the off chance that something changed between init() running
// and here.
checkState(
domain != null && !domain.getStatusValues().contains(StatusValue.PENDING_DELETE),
"Domain %s was deleted or is pending deletion",
domainName);
checkState(
Sets.intersection(domain.getStatusValues(), DISALLOWED_STATUSES).isEmpty(),
"Domain %s has prohibited status values",
domainName);
checkState(
leapSafeSubtractYears(domain.getRegistrationExpirationTime(), period).isAfter(now),
"Domain %s expires too soon",
domainName);
DateTime newExpirationTime =
leapSafeSubtractYears(domain.getRegistrationExpirationTime(), period);
HistoryEntry historyEntry =
new HistoryEntry.Builder()
.setParent(domain)
.setModificationTime(now)
.setBySuperuser(true)
.setType(Type.SYNTHETIC)
.setClientId(domain.getCurrentSponsorClientId())
.setReason("Domain unrenewal")
.setPeriod(Period.create(period, Unit.YEARS))
.setRequestedByRegistrar(false)
.build();
PollMessage oneTimePollMessage =
new PollMessage.OneTime.Builder()
.setClientId(domain.getCurrentSponsorClientId())
.setMsg(
String.format(
"Domain %s was unrenewed by %d years; now expires at %s.",
domainName, period, newExpirationTime))
.setParent(historyEntry)
.setEventTime(now)
.build();
// Create a new autorenew billing event and poll message starting at the new expiration time.
BillingEvent.Recurring newAutorenewEvent =
newAutorenewBillingEvent(domain)
.setEventTime(newExpirationTime)
.setParent(historyEntry)
.build();
PollMessage.Autorenew newAutorenewPollMessage =
newAutorenewPollMessage(domain)
.setEventTime(newExpirationTime)
.setParent(historyEntry)
.build();
// End the old autorenew billing event and poll message now.
updateAutorenewRecurrenceEndTime(domain, now);
DomainResource newDomain =
domain
.asBuilder()
.setRegistrationExpirationTime(newExpirationTime)
.setLastEppUpdateTime(now)
.setLastEppUpdateClientId(domain.getCurrentSponsorClientId())
.setAutorenewBillingEvent(Key.create(newAutorenewEvent))
.setAutorenewPollMessage(Key.create(newAutorenewPollMessage))
.build();
// In order to do it'll need to write out a new HistoryEntry (likely of type SYNTHETIC), a new
// autorenew billing event and poll message, and a new one time poll message at the present time
// informing the registrar of this out-of-band change.
ofy()
.save()
.entities(
newDomain,
historyEntry,
oneTimePollMessage,
newAutorenewEvent,
newAutorenewPollMessage);
}
}

View file

@ -77,4 +77,13 @@ public class DateTimeUtils {
checkArgument(years >= 0); checkArgument(years >= 0);
return years == 0 ? now : now.plusYears(1).plusYears(years - 1); return years == 0 ? now : now.plusYears(1).plusYears(years - 1);
} }
/**
* Subtracts years from a date, in the {@code Duration} sense of semantic years. Use this instead
* of {@link DateTime#minusYears} to ensure that we never end up on February 29.
*/
public static DateTime leapSafeSubtractYears(DateTime now, int years) {
checkArgument(years >= 0);
return years == 0 ? now : now.minusYears(1).minusYears(years - 1);
}
} }

View file

@ -130,7 +130,7 @@ public class EppLifecycleDomainTest extends EppTestCase {
domain, domain,
// Check the existence of the expected create one-time billing event. // Check the existence of the expected create one-time billing event.
oneTimeCreateBillingEvent, oneTimeCreateBillingEvent,
makeRecurringCreateBillingEvent(domain, createTime, deleteTime), makeRecurringCreateBillingEvent(domain, createTime.plusYears(2), deleteTime),
// Check for the existence of a cancellation for the given one-time billing event. // Check for the existence of a cancellation for the given one-time billing event.
makeCancellationBillingEventFor( makeCancellationBillingEventFor(
domain, oneTimeCreateBillingEvent, createTime, deleteTime)); domain, oneTimeCreateBillingEvent, createTime, deleteTime));
@ -189,7 +189,7 @@ public class EppLifecycleDomainTest extends EppTestCase {
assertBillingEventsForResource( assertBillingEventsForResource(
domain, domain,
makeOneTimeCreateBillingEvent(domain, createTime), makeOneTimeCreateBillingEvent(domain, createTime),
makeRecurringCreateBillingEvent(domain, createTime, deleteTime)); makeRecurringCreateBillingEvent(domain, createTime.plusYears(2), deleteTime));
assertThatLogoutSucceeds(); assertThatLogoutSucceeds();
} }
@ -248,7 +248,7 @@ public class EppLifecycleDomainTest extends EppTestCase {
expectedOneTimeCreateBillingEvent, expectedOneTimeCreateBillingEvent,
// ... and the expected one-time EAP fee billing event ... // ... and the expected one-time EAP fee billing event ...
expectedCreateEapBillingEvent, expectedCreateEapBillingEvent,
makeRecurringCreateBillingEvent(domain, createTime, deleteTime), makeRecurringCreateBillingEvent(domain, createTime.plusYears(2), deleteTime),
// ... and verify that the create one-time billing event was canceled ... // ... and verify that the create one-time billing event was canceled ...
makeCancellationBillingEventFor( makeCancellationBillingEventFor(
domain, expectedOneTimeCreateBillingEvent, createTime, deleteTime)); domain, expectedOneTimeCreateBillingEvent, createTime, deleteTime));

View file

@ -37,6 +37,7 @@ import google.registry.model.billing.BillingEvent.Reason;
import google.registry.model.domain.DomainResource; import google.registry.model.domain.DomainResource;
import google.registry.model.ofy.Ofy; import google.registry.model.ofy.Ofy;
import google.registry.model.registry.Registry; import google.registry.model.registry.Registry;
import google.registry.model.reporting.HistoryEntry;
import google.registry.model.reporting.HistoryEntry.Type; import google.registry.model.reporting.HistoryEntry.Type;
import google.registry.monitoring.whitebox.EppMetric; import google.registry.monitoring.whitebox.EppMetric;
import google.registry.testing.FakeClock; import google.registry.testing.FakeClock;
@ -289,15 +290,22 @@ public class EppTestCase extends ShardableTestCase {
/** Makes a recurring billing event corresponding to the given domain's creation. */ /** Makes a recurring billing event corresponding to the given domain's creation. */
protected static BillingEvent.Recurring makeRecurringCreateBillingEvent( protected static BillingEvent.Recurring makeRecurringCreateBillingEvent(
DomainResource domain, DateTime createTime, DateTime endTime) { DomainResource domain, DateTime eventTime, DateTime endTime) {
return makeRecurringCreateBillingEvent(
domain, getOnlyHistoryEntryOfType(domain, Type.DOMAIN_CREATE), eventTime, endTime);
}
/** Makes a recurring billing event corresponding to the given history entry. */
protected static BillingEvent.Recurring makeRecurringCreateBillingEvent(
DomainResource domain, HistoryEntry historyEntry, DateTime eventTime, DateTime endTime) {
return new BillingEvent.Recurring.Builder() return new BillingEvent.Recurring.Builder()
.setReason(Reason.RENEW) .setReason(Reason.RENEW)
.setFlags(ImmutableSet.of(Flag.AUTO_RENEW)) .setFlags(ImmutableSet.of(Flag.AUTO_RENEW))
.setTargetId(domain.getFullyQualifiedDomainName()) .setTargetId(domain.getFullyQualifiedDomainName())
.setClientId(domain.getCurrentSponsorClientId()) .setClientId(domain.getCurrentSponsorClientId())
.setEventTime(createTime.plusYears(2)) .setEventTime(eventTime)
.setRecurrenceEndTime(endTime) .setRecurrenceEndTime(endTime)
.setParent(getOnlyHistoryEntryOfType(domain, Type.DOMAIN_CREATE)) .setParent(historyEntry)
.build(); .build();
} }

View file

@ -0,0 +1,31 @@
<epp xmlns="urn:ietf:params:xml:ns:epp-1.0">
<response>
<result code="1000">
<msg>Command completed successfully</msg>
</result>
<resData>
<domain:infData
xmlns:domain="urn:ietf:params:xml:ns:domain-1.0">
<domain:name>%DOMAIN%</domain:name>
<domain:roid>8-TLD</domain:roid>
<domain:status s="inactive"/>
<domain:registrant>jd1234</domain:registrant>
<domain:contact type="tech">sh8013</domain:contact>
<domain:contact type="admin">sh8013</domain:contact>
<domain:clID>NewRegistrar</domain:clID>
<domain:crID>NewRegistrar</domain:crID>
<domain:crDate>2000-06-01T00:02:00Z</domain:crDate>
<domain:upID>NewRegistrar</domain:upID>
<domain:upDate>%UPDATE%</domain:upDate>
<domain:exDate>%EXDATE%</domain:exDate>
<domain:authInfo>
<domain:pw>2fooBAR</domain:pw>
</domain:authInfo>
</domain:infData>
</resData>
<trID>
<clTRID>ABC-12345</clTRID>
<svTRID>server-trid</svTRID>
</trID>
</response>
</epp>

View file

@ -0,0 +1,36 @@
<epp xmlns="urn:ietf:params:xml:ns:epp-1.0">
<response>
<result code="1000">
<msg>Command completed successfully</msg>
</result>
<resData>
<domain:infData
xmlns:domain="urn:ietf:params:xml:ns:domain-1.0">
<domain:name>%DOMAIN%</domain:name>
<domain:roid>8-TLD</domain:roid>
<domain:status s="inactive"/>
<domain:registrant>jd1234</domain:registrant>
<domain:contact type="tech">sh8013</domain:contact>
<domain:contact type="admin">sh8013</domain:contact>
<domain:clID>NewRegistrar</domain:clID>
<domain:crID>NewRegistrar</domain:crID>
<domain:crDate>2000-06-01T00:02:00Z</domain:crDate>
<domain:upID>NewRegistrar</domain:upID>
<domain:upDate>%UPDATE%</domain:upDate>
<domain:exDate>%EXDATE%</domain:exDate>
<domain:authInfo>
<domain:pw>2fooBAR</domain:pw>
</domain:authInfo>
</domain:infData>
</resData>
<extension>
<rgp:infData xmlns:rgp="urn:ietf:params:xml:ns:rgp-1.0">
<rgp:rgpStatus s="%RGPSTATUS%"/>
</rgp:infData>
</extension>
<trID>
<clTRID>ABC-12345</clTRID>
<svTRID>server-trid</svTRID>
</trID>
</response>
</epp>

View file

@ -0,0 +1,18 @@
<epp xmlns="urn:ietf:params:xml:ns:epp-1.0">
<response>
<result code="1000">
<msg>Command completed successfully</msg>
</result>
<resData>
<domain:renData
xmlns:domain="urn:ietf:params:xml:ns:domain-1.0">
<domain:name>%DOMAIN%</domain:name>
<domain:exDate>%EXDATE%</domain:exDate>
</domain:renData>
</resData>
<trID>
<clTRID>ABC-12345</clTRID>
<svTRID>server-trid</svTRID>
</trID>
</response>
</epp>

View file

@ -0,0 +1,15 @@
<epp xmlns="urn:ietf:params:xml:ns:epp-1.0">
<response>
<result code="1301">
<msg>Command completed successfully; ack to dequeue</msg>
</result>
<msgQ count="1" id="1-8-TLD-17-18-2001">
<qDate>2001-06-07T00:00:00Z</qDate>
<msg>Domain example.tld was unrenewed by 3 years; now expires at 2003-06-01T00:02:00.000Z.</msg>
</msgQ>
<trID>
<clTRID>ABC-12345</clTRID>
<svTRID>server-trid</svTRID>
</trID>
</response>
</epp>

View file

@ -560,7 +560,7 @@ public class DatastoreHelper {
String domainName = String.format("%s.%s", label, tld); String domainName = String.format("%s.%s", label, tld);
DomainResource domain = DomainResource domain =
new DomainResource.Builder() new DomainResource.Builder()
.setRepoId("1-".concat(Ascii.toUpperCase(tld))) .setRepoId(generateNewDomainRoid(tld))
.setFullyQualifiedDomainName(domainName) .setFullyQualifiedDomainName(domainName)
.setPersistedCurrentSponsorClientId("TheRegistrar") .setPersistedCurrentSponsorClientId("TheRegistrar")
.setCreationClientId("TheRegistrar") .setCreationClientId("TheRegistrar")

View file

@ -24,6 +24,7 @@ import google.registry.model.reporting.HistoryEntry;
import google.registry.testing.TruthChainer.And; import google.registry.testing.TruthChainer.And;
import java.util.Objects; import java.util.Objects;
import java.util.Optional; import java.util.Optional;
import org.joda.time.DateTime;
/** Utility methods for asserting things about {@link HistoryEntry} instances. */ /** Utility methods for asserting things about {@link HistoryEntry} instances. */
public class HistoryEntrySubject extends Subject<HistoryEntrySubject, HistoryEntry> { public class HistoryEntrySubject extends Subject<HistoryEntrySubject, HistoryEntry> {
@ -56,6 +57,14 @@ public class HistoryEntrySubject extends Subject<HistoryEntrySubject, HistoryEnt
return hasValue(otherClientId, actual().getOtherClientId(), "has other client ID"); return hasValue(otherClientId, actual().getOtherClientId(), "has other client ID");
} }
public And<HistoryEntrySubject> hasModificationTime(DateTime modificationTime) {
return hasValue(modificationTime, actual().getModificationTime(), "has modification time");
}
public And<HistoryEntrySubject> bySuperuser(boolean superuser) {
return hasValue(superuser, actual().getBySuperuser(), "has modification time");
}
public And<HistoryEntrySubject> hasPeriod() { public And<HistoryEntrySubject> hasPeriod() {
if (actual().getPeriod() == null) { if (actual().getPeriod() == null) {
fail("has a period"); fail("has a period");

View file

@ -30,6 +30,7 @@ java_library(
"//java/google/registry/tools/server", "//java/google/registry/tools/server",
"//java/google/registry/util", "//java/google/registry/util",
"//java/google/registry/xml", "//java/google/registry/xml",
"//javatests/google/registry/flows",
"//javatests/google/registry/rde", "//javatests/google/registry/rde",
"//javatests/google/registry/testing", "//javatests/google/registry/testing",
"//javatests/google/registry/tmch", "//javatests/google/registry/tmch",

View file

@ -191,6 +191,10 @@ public abstract class CommandTestCase<C extends Command> {
assertThat(getStdoutAsString()).doesNotContain(expected); assertThat(getStdoutAsString()).doesNotContain(expected);
} }
protected void assertNotInStderr(String expected) {
assertThat(getStderrAsString()).doesNotContain(expected);
}
protected String getStdoutAsString() { protected String getStdoutAsString() {
return new String(stdout.toByteArray(), UTF_8); return new String(stdout.toByteArray(), UTF_8);
} }

View file

@ -0,0 +1,193 @@
// 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 google.registry.model.EppResourceUtils.loadByForeignKey;
import static google.registry.testing.DatastoreHelper.assertBillingEventsForResource;
import static google.registry.testing.DatastoreHelper.createTlds;
import static google.registry.testing.DatastoreHelper.getOnlyHistoryEntryOfType;
import static google.registry.util.DateTimeUtils.END_OF_TIME;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import google.registry.flows.EppTestCase;
import google.registry.model.billing.BillingEvent;
import google.registry.model.billing.BillingEvent.Reason;
import google.registry.model.domain.DomainResource;
import google.registry.model.reporting.HistoryEntry.Type;
import google.registry.testing.AppEngineRule;
import google.registry.util.Clock;
import java.util.List;
import org.joda.money.Money;
import org.joda.time.DateTime;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
/** Tests for tools that affect EPP lifecycle. */
@RunWith(JUnit4.class)
public class EppLifecycleToolsTest extends EppTestCase {
@Rule
public final AppEngineRule appEngine =
AppEngineRule.builder().withDatastore().withTaskQueue().build();
@Before
public void initTld() {
createTlds("example", "tld");
}
@Test
public void test_renewDomainThenUnrenew() throws Exception {
assertThatLoginSucceeds("NewRegistrar", "foo-BAR2");
createContacts(DateTime.parse("2000-06-01T00:00:00Z"));
// Create the domain for 2 years.
assertThatCommand(
"domain_create_no_hosts_or_dsdata.xml", ImmutableMap.of("DOMAIN", "example.tld"))
.atTime("2000-06-01T00:02:00Z")
.hasResponse(
"domain_create_response.xml",
ImmutableMap.of(
"DOMAIN", "example.tld",
"CRDATE", "2000-06-01T00:02:00Z",
"EXDATE", "2002-06-01T00:02:00Z"));
// Explicitly renew it for 4 more years.
assertThatCommand(
"domain_renew.xml",
ImmutableMap.of("DOMAIN", "example.tld", "EXPDATE", "2002-06-01", "YEARS", "4"))
.atTime("2000-06-07T00:00:00Z")
.hasResponse(
"domain_renew_response.xml",
ImmutableMap.of("DOMAIN", "example.tld", "EXDATE", "2006-06-01T00:02:00Z"));
// Run an info command and verify its registration term is 6 years in total.
assertThatCommand("domain_info.xml", ImmutableMap.of("DOMAIN", "example.tld"))
.atTime("2000-08-07T00:01:00Z")
.hasResponse(
"domain_info_response_inactive.xml",
ImmutableMap.of(
"DOMAIN", "example.tld",
"UPDATE", "2000-06-12T00:00:00Z",
"EXDATE", "2006-06-01T00:02:00Z"));
assertThatCommand("poll.xml")
.atTime("2001-01-01T00:01:00Z")
.hasResponse("poll_response_empty.xml");
// Run the nomulus unrenew_domain command to take 3 years off the registration.
clock.setTo(DateTime.parse("2001-06-07T00:00:00.0Z"));
UnrenewDomainCommand unrenewCmd =
new ForcedUnrenewDomainCommand(ImmutableList.of("example.tld"), 3, clock);
unrenewCmd.run();
// Run an info command and verify that the registration term is now 3 years in total.
assertThatCommand("domain_info.xml", ImmutableMap.of("DOMAIN", "example.tld"))
.atTime("2001-06-07T00:01:00.0Z")
.hasResponse(
"domain_info_response_inactive.xml",
ImmutableMap.of(
"DOMAIN", "example.tld",
"UPDATE", "2001-06-07T00:00:00Z",
"EXDATE", "2003-06-01T00:02:00Z"));
// Verify that the correct one-time poll message for the unrenew was sent.
assertThatCommand("poll.xml")
.atTime("2001-06-08T00:00:00Z")
.hasResponse("poll_response_unrenew.xml");
assertThatCommand("poll_ack.xml", ImmutableMap.of("ID", "1-8-TLD-17-18-2001"))
.atTime("2001-06-08T00:00:01Z")
.hasResponse("poll_ack_response_empty.xml");
// Run an info command after the 3 years to verify that the domain successfully autorenewed.
assertThatCommand("domain_info.xml", ImmutableMap.of("DOMAIN", "example.tld"))
.atTime("2003-06-02T00:00:00.0Z")
.hasResponse(
"domain_info_response_inactive_grace_period.xml",
ImmutableMap.of(
"DOMAIN", "example.tld",
"UPDATE", "2003-06-01T00:02:00Z",
"EXDATE", "2004-06-01T00:02:00Z",
"RGPSTATUS", "autoRenewPeriod"));
// And verify that the autorenew poll message worked as well.
assertThatCommand("poll.xml")
.atTime("2003-06-02T00:01:00Z")
.hasResponse(
"poll_response_autorenew.xml",
ImmutableMap.of(
"ID", "1-8-TLD-17-20-2003",
"QDATE", "2003-06-01T00:02:00Z",
"DOMAIN", "example.tld",
"EXDATE", "2004-06-01T00:02:00Z"));
// Assert about billing events.
DateTime createTime = DateTime.parse("2000-06-01T00:02:00Z");
DomainResource domain =
loadByForeignKey(
DomainResource.class, "example.tld", DateTime.parse("2003-06-02T00:02:00Z"));
BillingEvent.OneTime renewBillingEvent =
new BillingEvent.OneTime.Builder()
.setReason(Reason.RENEW)
.setTargetId(domain.getFullyQualifiedDomainName())
.setClientId(domain.getCurrentSponsorClientId())
.setCost(Money.parse("USD 44.00"))
.setPeriodYears(4)
.setEventTime(DateTime.parse("2000-06-07T00:00:00Z"))
.setBillingTime(DateTime.parse("2000-06-12T00:00:00Z"))
.setParent(getOnlyHistoryEntryOfType(domain, Type.DOMAIN_RENEW))
.build();
assertBillingEventsForResource(
domain,
makeOneTimeCreateBillingEvent(domain, createTime),
renewBillingEvent,
// The initial autorenew billing event, which was closed at the time of the explicit renew.
makeRecurringCreateBillingEvent(
domain,
getOnlyHistoryEntryOfType(domain, Type.DOMAIN_CREATE),
createTime.plusYears(2),
DateTime.parse("2000-06-07T00:00:00.000Z")),
// The renew's autorenew billing event, which was closed at the time of the unrenew.
makeRecurringCreateBillingEvent(
domain,
getOnlyHistoryEntryOfType(domain, Type.DOMAIN_RENEW),
DateTime.parse("2006-06-01T00:02:00.000Z"),
DateTime.parse("2001-06-07T00:00:00.000Z")),
// The remaining active autorenew billing event which was created by the unrenew.
makeRecurringCreateBillingEvent(
domain,
getOnlyHistoryEntryOfType(domain, Type.SYNTHETIC),
DateTime.parse("2003-06-01T00:02:00.000Z"),
END_OF_TIME));
assertThatLogoutSucceeds();
}
static class ForcedUnrenewDomainCommand extends UnrenewDomainCommand {
ForcedUnrenewDomainCommand(List<String> domainNames, int period, Clock clock) {
super();
this.clock = clock;
this.force = true;
this.mainParameters = domainNames;
this.period = period;
}
}
}

View file

@ -0,0 +1,217 @@
// 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.model.EppResourceUtils.loadByForeignKey;
import static google.registry.model.eppcommon.StatusValue.PENDING_DELETE;
import static google.registry.model.eppcommon.StatusValue.PENDING_TRANSFER;
import static google.registry.model.ofy.ObjectifyService.ofy;
import static google.registry.model.reporting.HistoryEntry.Type.SYNTHETIC;
import static google.registry.testing.DatastoreHelper.assertBillingEventsEqual;
import static google.registry.testing.DatastoreHelper.assertPollMessagesEqual;
import static google.registry.testing.DatastoreHelper.createTld;
import static google.registry.testing.DatastoreHelper.getOnlyHistoryEntryOfType;
import static google.registry.testing.DatastoreHelper.newDomainResource;
import static google.registry.testing.DatastoreHelper.persistActiveContact;
import static google.registry.testing.DatastoreHelper.persistActiveDomain;
import static google.registry.testing.DatastoreHelper.persistDeletedDomain;
import static google.registry.testing.DatastoreHelper.persistDomainWithDependentResources;
import static google.registry.testing.DatastoreHelper.persistResource;
import static google.registry.testing.HistoryEntrySubject.assertAboutHistoryEntries;
import static google.registry.testing.JUnitBackports.assertThrows;
import com.google.common.collect.ImmutableSet;
import com.googlecode.objectify.Key;
import google.registry.model.billing.BillingEvent;
import google.registry.model.billing.BillingEvent.Flag;
import google.registry.model.billing.BillingEvent.Reason;
import google.registry.model.contact.ContactResource;
import google.registry.model.domain.DomainResource;
import google.registry.model.eppcommon.StatusValue;
import google.registry.model.ofy.Ofy;
import google.registry.model.poll.PollMessage;
import google.registry.model.reporting.HistoryEntry;
import google.registry.testing.FakeClock;
import google.registry.testing.InjectRule;
import org.joda.time.DateTime;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
/** Unit tests for {@link UnrenewDomainCommand}. */
public class UnrenewDomainCommandTest extends CommandTestCase<UnrenewDomainCommand> {
@Rule public final InjectRule inject = new InjectRule();
private final FakeClock clock = new FakeClock(DateTime.parse("2016-12-06T13:55:01Z"));
@Before
public void before() {
createTld("tld");
inject.setStaticField(Ofy.class, "clock", clock);
command.clock = clock;
}
@Test
public void test_unrenewTwoDomains_worksSuccessfully() throws Exception {
ContactResource contact = persistActiveContact("jd1234");
clock.advanceOneMilli();
persistDomainWithDependentResources(
"foo", "tld", contact, clock.nowUtc(), clock.nowUtc(), clock.nowUtc().plusYears(5));
clock.advanceOneMilli();
persistDomainWithDependentResources(
"bar", "tld", contact, clock.nowUtc(), clock.nowUtc(), clock.nowUtc().plusYears(4));
clock.advanceOneMilli();
runCommandForced("-p", "2", "foo.tld", "bar.tld");
clock.advanceOneMilli();
assertThat(
loadByForeignKey(DomainResource.class, "foo.tld", clock.nowUtc())
.getRegistrationExpirationTime())
.isEqualTo(DateTime.parse("2019-12-06T13:55:01.001Z"));
assertThat(
loadByForeignKey(DomainResource.class, "bar.tld", clock.nowUtc())
.getRegistrationExpirationTime())
.isEqualTo(DateTime.parse("2018-12-06T13:55:01.002Z"));
assertInStdout("Successfully unrenewed all domains.");
}
@Test
public void test_unrenewDomain_savesDependentEntitiesCorrectly() throws Exception {
ContactResource contact = persistActiveContact("jd1234");
clock.advanceOneMilli();
persistDomainWithDependentResources(
"foo", "tld", contact, clock.nowUtc(), clock.nowUtc(), clock.nowUtc().plusYears(5));
DateTime newExpirationTime = clock.nowUtc().plusYears(3);
clock.advanceOneMilli();
runCommandForced("-p", "2", "foo.tld");
DateTime unrenewTime = clock.nowUtc();
clock.advanceOneMilli();
DomainResource domain = loadByForeignKey(DomainResource.class, "foo.tld", clock.nowUtc());
assertAboutHistoryEntries()
.that(getOnlyHistoryEntryOfType(domain, SYNTHETIC))
.hasModificationTime(unrenewTime)
.and()
.hasMetadataReason("Domain unrenewal")
.and()
.hasPeriodYears(2)
.and()
.hasClientId("TheRegistrar")
.and()
.bySuperuser(true)
.and()
.hasMetadataRequestedByRegistrar(false);
HistoryEntry synthetic = getOnlyHistoryEntryOfType(domain, SYNTHETIC);
assertBillingEventsEqual(
ofy().load().key(domain.getAutorenewBillingEvent()).now(),
new BillingEvent.Recurring.Builder()
.setParent(synthetic)
.setReason(Reason.RENEW)
.setFlags(ImmutableSet.of(Flag.AUTO_RENEW))
.setTargetId(domain.getFullyQualifiedDomainName())
.setClientId("TheRegistrar")
.setEventTime(newExpirationTime)
.build());
assertPollMessagesEqual(
ofy().load().type(PollMessage.class).ancestor(synthetic).list(),
ImmutableSet.of(
new PollMessage.OneTime.Builder()
.setParent(synthetic)
.setClientId("TheRegistrar")
.setMsg(
"Domain foo.tld was unrenewed by 2 years; "
+ "now expires at 2019-12-06T13:55:01.001Z.")
.setEventTime(unrenewTime)
.build(),
new PollMessage.Autorenew.Builder()
.setParent(synthetic)
.setTargetId("foo.tld")
.setClientId("TheRegistrar")
.setEventTime(newExpirationTime)
.setMsg("Domain was auto-renewed.")
.build()));
// Check that fields on domain were updated correctly.
assertThat(domain.getAutorenewPollMessage().getParent()).isEqualTo(Key.create(synthetic));
assertThat(domain.getRegistrationExpirationTime()).isEqualTo(newExpirationTime);
assertThat(domain.getLastEppUpdateTime()).isEqualTo(unrenewTime);
assertThat(domain.getLastEppUpdateClientId()).isEqualTo("TheRegistrar");
}
@Test
public void test_periodTooLow_fails() {
IllegalArgumentException thrown =
assertThrows(
IllegalArgumentException.class, () -> runCommandForced("--period", "0", "domain.tld"));
assertThat(thrown).hasMessageThat().isEqualTo("Period must be in the range 1-9");
}
@Test
public void test_periodTooHigh_fails() {
IllegalArgumentException thrown =
assertThrows(
IllegalArgumentException.class, () -> runCommandForced("--period", "10", "domain.tld"));
assertThat(thrown).hasMessageThat().isEqualTo("Period must be in the range 1-9");
}
@Test
public void test_varietyOfInvalidDomains_displaysErrors() {
DateTime now = clock.nowUtc();
persistResource(
newDomainResource("deleting.tld")
.asBuilder()
.setDeletionTime(now.plusHours(1))
.setStatusValues(ImmutableSet.of(PENDING_DELETE))
.build());
persistDeletedDomain("deleted.tld", now.minusHours(1));
persistResource(
newDomainResource("transferring.tld")
.asBuilder()
.setStatusValues(ImmutableSet.of(PENDING_TRANSFER))
.build());
persistResource(
newDomainResource("locked.tld")
.asBuilder()
.setStatusValues(ImmutableSet.of(StatusValue.SERVER_UPDATE_PROHIBITED))
.build());
persistActiveDomain("expiring.tld", now.minusDays(4), now.plusMonths(11));
persistActiveDomain("valid.tld", now.minusDays(4), now.plusYears(3));
IllegalArgumentException thrown =
assertThrows(
IllegalArgumentException.class,
() ->
runCommandForced(
"nonexistent.tld",
"deleting.tld",
"deleted.tld",
"transferring.tld",
"locked.tld",
"expiring.tld",
"valid.tld"));
assertThat(thrown)
.hasMessageThat()
.isEqualTo("Aborting because some domains cannot be unrewed");
assertInStderr(
"Found domains that cannot be unrenewed for the following reasons:",
"Domains that don't exist: [nonexistent.tld]",
"Domains that are deleted or pending delete: [deleting.tld, deleted.tld]",
"Domains with disallowed statuses: "
+ "{transferring.tld=[PENDING_TRANSFER], locked.tld=[SERVER_UPDATE_PROHIBITED]}",
"Domains expiring too soon: {expiring.tld=2017-11-06T13:55:01.000Z}");
assertNotInStderr("valid.tld");
}
}

View file

@ -23,6 +23,7 @@ import static google.registry.util.DateTimeUtils.isAtOrAfter;
import static google.registry.util.DateTimeUtils.isBeforeOrAt; import static google.registry.util.DateTimeUtils.isBeforeOrAt;
import static google.registry.util.DateTimeUtils.latestOf; import static google.registry.util.DateTimeUtils.latestOf;
import static google.registry.util.DateTimeUtils.leapSafeAddYears; import static google.registry.util.DateTimeUtils.leapSafeAddYears;
import static google.registry.util.DateTimeUtils.leapSafeSubtractYears;
import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableList;
import org.joda.time.DateTime; import org.joda.time.DateTime;
@ -70,6 +71,20 @@ public class DateTimeUtilsTest {
assertThat(leapSafeAddYears(startDate, 4)).isEqualTo(DateTime.parse("2016-02-28T00:00:00Z")); assertThat(leapSafeAddYears(startDate, 4)).isEqualTo(DateTime.parse("2016-02-28T00:00:00Z"));
} }
@Test
public void testSuccess_leapSafeSubtractYears() {
DateTime startDate = DateTime.parse("2012-02-29T00:00:00Z");
assertThat(startDate.minusYears(4)).isEqualTo(DateTime.parse("2008-02-29T00:00:00Z"));
assertThat(leapSafeSubtractYears(startDate, 4))
.isEqualTo(DateTime.parse("2008-02-28T00:00:00Z"));
}
@Test
public void testSuccess_leapSafeSubtractYears_zeroYears() {
DateTime leapDay = DateTime.parse("2012-02-29T00:00:00Z");
assertThat(leapDay.minusYears(0)).isEqualTo(leapDay);
}
@Test @Test
public void testFailure_earliestOfEmpty() { public void testFailure_earliestOfEmpty() {
assertThrows(IllegalArgumentException.class, () -> earliestOf(ImmutableList.of())); assertThrows(IllegalArgumentException.class, () -> earliestOf(ImmutableList.of()));