Add a command to fix duplicate id issue for BillingEvent.Recurring (#798)

* Refactor ResaveEntitiesWithUniqueIdCommand to extract common methods

* Add a command to dedupe BillingEvent.Recurring entities
This commit is contained in:
Shicong Huang 2020-11-03 16:11:56 -05:00 committed by GitHub
parent aa84d5d138
commit e3d400958c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 577 additions and 72 deletions

View file

@ -124,4 +124,16 @@ public class GracePeriod extends GracePeriodBase implements DatastoreAndSqlEntit
clone.restoreHistoryIds();
return clone;
}
/**
* Returns a clone of this {@link GracePeriod} with {@link #billingEventRecurring} set to the
* given value.
*
* <p>TODO(b/162231099): Remove this function after duplicate id issue is solved.
*/
public GracePeriod cloneWithRecurringBillingEvent(VKey<BillingEvent.Recurring> recurring) {
GracePeriod clone = clone(this);
clone.billingEventRecurring = recurring;
return clone;
}
}

View file

@ -19,42 +19,22 @@ import static google.registry.model.ofy.ObjectifyService.ofy;
import static java.nio.charset.StandardCharsets.UTF_8;
import com.beust.jcommander.Parameter;
import com.beust.jcommander.Parameters;
import com.google.appengine.api.datastore.KeyFactory;
import com.google.common.base.Splitter;
import com.google.common.io.CharStreams;
import com.google.common.io.Files;
import com.googlecode.objectify.Key;
import google.registry.model.ImmutableObject;
import google.registry.model.billing.BillingEvent;
import google.registry.model.domain.DomainBase;
import google.registry.util.NonFinalForTesting;
import google.registry.util.TypeUtils.TypeInstantiator;
import java.io.File;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.List;
/**
* Command to resave entities with a unique id.
*
* <p>This command is used to address the duplicate id issue we found for certain {@link
* BillingEvent.OneTime} entities. The command reassigns an application wide unique id to the
* problematic entity and resaves it, it also resaves the entity having reference to the problematic
* entity with the updated id.
*
* <p>To use this command, you will need to provide the path to a file containing a list of strings
* representing the literal of Objectify key for the problematic entities. An example key literal
* is:
*
* <pre>
* "DomainBase", "111111-TEST", "HistoryEntry", 2222222, "OneTime", 3333333
* </pre>
*
* <p>Note that the double quotes are part of the key literal. The key literal can be retrieved from
* the column <code>__key__.path</code> in BigQuery.
*/
@Parameters(separators = " =", commandDescription = "Resave entities with a unique id.")
public class ResaveEntitiesWithUniqueIdCommand extends MutatingCommand {
/** Base Command to dedupe entities with duplicate IDs. */
abstract class DedupeEntityIdsCommand<T> extends MutatingCommand {
@Parameter(
names = "--key_paths_file",
@ -66,7 +46,9 @@ public class ResaveEntitiesWithUniqueIdCommand extends MutatingCommand {
@NonFinalForTesting private static InputStream stdin = System.in;
private String keyChangeMessage;
private StringBuilder changeMessage = new StringBuilder();
abstract void dedupe(T entity);
@Override
protected void init() throws Exception {
@ -85,8 +67,9 @@ public class ResaveEntitiesWithUniqueIdCommand extends MutatingCommand {
keyPathsFile == null ? "STDIN" : "File " + keyPathsFile.getAbsolutePath()));
continue;
}
if (entity instanceof BillingEvent.OneTime) {
resaveBillingEvent((BillingEvent.OneTime) entity);
Class<T> clazz = new TypeInstantiator<T>(getClass()) {}.getExactType();
if (clazz.isInstance(entity)) {
dedupe((T) entity);
} else {
throw new IllegalArgumentException("Unsupported entity key: " + untypedKey);
}
@ -96,37 +79,19 @@ public class ResaveEntitiesWithUniqueIdCommand extends MutatingCommand {
@Override
protected void postBatchExecute() {
System.out.println(keyChangeMessage);
System.out.println(changeMessage);
}
private void deleteOldAndSaveNewEntity(ImmutableObject oldEntity, ImmutableObject newEntity) {
void stageEntityKeyChange(ImmutableObject oldEntity, ImmutableObject newEntity) {
stageEntityChange(oldEntity, null);
stageEntityChange(null, newEntity);
appendChangeMessage(
String.format(
"Changed entity key from: %s to: %s", Key.create(oldEntity), Key.create(newEntity)));
}
private void resaveBillingEvent(BillingEvent.OneTime billingEvent) {
Key<BillingEvent> key = Key.create(billingEvent);
Key<DomainBase> domainKey = getGrandParentAsDomain(key);
DomainBase domain = ofy().load().key(domainKey).now();
// The BillingEvent.OneTime entity to be resaved should be the billing event created a few
// years ago, so they should not be referenced from TransferData and GracePeriod in the domain.
assertNotInDomainTransferData(domain, key);
domain
.getGracePeriods()
.forEach(
gracePeriod ->
checkState(
!gracePeriod.getOneTimeBillingEvent().getOfyKey().equals(key),
"Entity %s is referenced by a grace period in domain %s",
key,
domainKey));
// By setting id to 0L, Buildable.build() will assign an application wide unique id to it.
BillingEvent.OneTime uniqIdBillingEvent = billingEvent.asBuilder().setId(0L).build();
deleteOldAndSaveNewEntity(billingEvent, uniqIdBillingEvent);
keyChangeMessage =
String.format("Old Entity Key: %s New Entity Key: %s", key, Key.create(uniqIdBillingEvent));
void appendChangeMessage(String message) {
changeMessage.append(message);
}
private static boolean isKind(Key<?> key, Class<?> clazz) {
@ -165,7 +130,7 @@ public class ResaveEntitiesWithUniqueIdCommand extends MutatingCommand {
return literal.substring(1, literal.length() - 1);
}
private static Key<DomainBase> getGrandParentAsDomain(Key<?> key) {
static Key<DomainBase> getGrandParentAsDomain(Key<?> key) {
Key<?> grandParent;
try {
grandParent = key.getParent().getParent();
@ -178,19 +143,4 @@ public class ResaveEntitiesWithUniqueIdCommand extends MutatingCommand {
}
return (Key<DomainBase>) grandParent;
}
private static void assertNotInDomainTransferData(DomainBase domainBase, Key<?> key) {
if (!domainBase.getTransferData().isEmpty()) {
domainBase
.getTransferData()
.getServerApproveEntities()
.forEach(
entityKey ->
checkState(
!entityKey.getOfyKey().equals(key),
"Entity %s is referenced by the transfer data in domain %s",
key,
domainBase.createVKey().getOfyKey()));
}
}
}

View file

@ -0,0 +1,88 @@
// Copyright 2020 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.checkState;
import static google.registry.model.ofy.ObjectifyService.ofy;
import com.beust.jcommander.Parameters;
import com.googlecode.objectify.Key;
import google.registry.model.billing.BillingEvent;
import google.registry.model.billing.BillingEvent.OneTime;
import google.registry.model.domain.DomainBase;
/**
* Command to dedupe {@link BillingEvent.OneTime} entities having duplicate IDs.
*
* <p>This command is used to address the duplicate id issue we found for certain {@link
* BillingEvent.OneTime} entities. The command reassigns an application wide unique id to the
* problematic entity and resaves it, it also resaves the entity having reference to the problematic
* entity with the updated id.
*
* <p>To use this command, you will need to provide the path to a file containing a list of strings
* representing the literal of Objectify key for the problematic entities. An example key literal
* is:
*
* <pre>
* "DomainBase", "111111-TEST", "HistoryEntry", 2222222, "OneTime", 3333333
* </pre>
*
* <p>Note that the double quotes are part of the key literal. The key literal can be retrieved from
* the column <code>__key__.path</code> in BigQuery.
*/
@Parameters(
separators = " =",
commandDescription = "Dedupe BillingEvent.OneTime entities with duplicate IDs.")
public class DedupeOneTimeBillingEventIdsCommand extends DedupeEntityIdsCommand<OneTime> {
@Override
void dedupe(OneTime entity) {
Key<BillingEvent> key = Key.create(entity);
Key<DomainBase> domainKey = getGrandParentAsDomain(key);
DomainBase domain = ofy().load().key(domainKey).now();
// The BillingEvent.OneTime entity to be resaved should be the billing event created a few
// years ago, so they should not be referenced from TransferData and GracePeriod in the domain.
assertNotInDomainTransferData(domain, key);
domain
.getGracePeriods()
.forEach(
gracePeriod ->
checkState(
!gracePeriod.getOneTimeBillingEvent().getOfyKey().equals(key),
"Entity %s is referenced by a grace period in domain %s",
key,
domainKey));
// By setting id to 0L, Buildable.build() will assign an application wide unique id to it.
BillingEvent.OneTime uniqIdBillingEvent = entity.asBuilder().setId(0L).build();
stageEntityKeyChange(entity, uniqIdBillingEvent);
}
private static void assertNotInDomainTransferData(DomainBase domainBase, Key<?> key) {
if (!domainBase.getTransferData().isEmpty()) {
domainBase
.getTransferData()
.getServerApproveEntities()
.forEach(
entityKey ->
checkState(
!entityKey.getOfyKey().equals(key),
"Entity %s is referenced by the transfer data in domain %s",
key,
domainBase.createVKey().getOfyKey()));
}
}
}

View file

@ -0,0 +1,192 @@
// Copyright 2020 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.collect.ImmutableSet.toImmutableSet;
import static google.registry.model.ofy.ObjectifyService.ofy;
import com.beust.jcommander.Parameters;
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.billing.BillingEvent.OneTime;
import google.registry.model.billing.BillingEvent.Recurring;
import google.registry.model.domain.DomainBase;
import google.registry.model.domain.GracePeriod;
import google.registry.model.transfer.DomainTransferData;
import google.registry.model.transfer.TransferData.TransferServerApproveEntity;
import google.registry.persistence.VKey;
import java.util.List;
import java.util.Set;
/**
* A command that re-saves the problematic {@link BillingEvent.Recurring} entities with unique IDs.
*
* <p>This command is used to address the duplicate id issue we found for certain {@link
* BillingEvent.Recurring} entities. The command reassigns an application wide unique id to the
* problematic entity and resaves it, it also resaves the entity having reference to the problematic
* entity with the updated id.
*
* <p>To use this command, you will need to provide the path to a file containing a list of strings
* representing the literal of Objectify key for the problematic entities. An example key literal
* is:
*
* <pre>
* "DomainBase", "111111-TEST", "HistoryEntry", 2222222, "Recurring", 3333333
* </pre>
*
* <p>Note that the double quotes are part of the key literal. The key literal can be retrieved from
* the column <code>__key__.path</code> in BigQuery.
*/
@Parameters(
separators = " =",
commandDescription = "Dedupe BillingEvent.Recurring entities with duplicate IDs.")
public class DedupeRecurringBillingEventIdsCommand
extends DedupeEntityIdsCommand<BillingEvent.Recurring> {
@Override
void dedupe(Recurring recurring) {
// Loads the associated DomainBase and BillingEvent.OneTime entities that
// may have reference to this BillingEvent.Recurring entity.
Key<DomainBase> domainKey = getGrandParentAsDomain(Key.create(recurring));
DomainBase domain = ofy().load().key(domainKey).now();
List<BillingEvent.OneTime> oneTimes =
ofy().load().type(BillingEvent.OneTime.class).ancestor(domainKey).list();
VKey<Recurring> oldRecurringVKey = recurring.createVKey();
// By setting id to 0L, Buildable.build() will assign an application wide unique id to it.
Recurring uniqIdRecurring = recurring.asBuilder().setId(0L).build();
VKey<Recurring> newRecurringVKey = uniqIdRecurring.createVKey();
// After having the unique id for the BillingEvent.Recurring entity, we also need to
// update the references in other entities to point to the new BillingEvent.Recurring
// entity.
updateReferenceInOneTimeBillingEvent(oneTimes, oldRecurringVKey, newRecurringVKey);
updateReferenceInDomain(domain, oldRecurringVKey, newRecurringVKey);
stageEntityKeyChange(recurring, uniqIdRecurring);
}
/**
* Resaves {@link BillingEvent.OneTime} entities with updated {@link
* BillingEvent.OneTime#cancellationMatchingBillingEvent}.
*
* <p>{@link BillingEvent.OneTime#cancellationMatchingBillingEvent} is a {@link VKey} to a {@link
* BillingEvent.Recurring} entity. So, if the {@link BillingEvent.Recurring} entity gets a new key
* by changing its id, we need to update {@link
* BillingEvent.OneTime#cancellationMatchingBillingEvent} as well.
*/
private void updateReferenceInOneTimeBillingEvent(
List<OneTime> oneTimes, VKey<Recurring> oldRecurringVKey, VKey<Recurring> newRecurringVKey) {
oneTimes.forEach(
oneTime -> {
if (oneTime.getCancellationMatchingBillingEvent() != null
&& oneTime.getCancellationMatchingBillingEvent().equals(oldRecurringVKey)) {
BillingEvent.OneTime updatedOneTime =
oneTime.asBuilder().setCancellationMatchingBillingEvent(newRecurringVKey).build();
stageEntityChange(oneTime, updatedOneTime);
appendChangeMessage(
String.format(
"Changed cancellationMatchingBillingEvent in entity %s from %s to %s\n",
oneTime.createVKey().getOfyKey(),
oneTime.getCancellationMatchingBillingEvent().getOfyKey(),
updatedOneTime.getCancellationMatchingBillingEvent().getOfyKey()));
}
});
}
/**
* Resaves {@link DomainBase} entity with updated references to {@link BillingEvent.Recurring}
* entity.
*
* <p>The following 4 fields in the domain entity can be or have a reference to this
* BillingEvent.Recurring entity, so we need to check them and replace them with the new entity
* when necessary:
*
* <ol>
* <li>domain.autorenewBillingEvent, see {@link DomainBase#autorenewBillingEvent}
* <li>domain.transferData.serverApproveAutorenewEvent, see {@link
* DomainTransferData#serverApproveAutorenewEvent}
* <li>domain.transferData.serverApproveEntities, see {@link
* DomainTransferData#serverApproveEntities}
* <li>domain.gracePeriods.billingEventRecurring, see {@link GracePeriod#billingEventRecurring}
* </ol>
*/
private void updateReferenceInDomain(
DomainBase domain, VKey<Recurring> oldRecurringVKey, VKey<Recurring> newRecurringVKey) {
DomainBase.Builder domainBuilder = domain.asBuilder();
StringBuilder domainChange =
new StringBuilder(
String.format(
"Resaved domain %s with following changes:\n", domain.createVKey().getOfyKey()));
if (domain.getAutorenewBillingEvent() != null
&& domain.getAutorenewBillingEvent().equals(oldRecurringVKey)) {
domainBuilder.setAutorenewBillingEvent(newRecurringVKey);
domainChange.append(
String.format(
" Changed autorenewBillingEvent from %s to %s.\n",
oldRecurringVKey, newRecurringVKey));
}
if (domain.getTransferData().getServerApproveAutorenewEvent() != null
&& domain.getTransferData().getServerApproveAutorenewEvent().equals(oldRecurringVKey)) {
Set<VKey<? extends TransferServerApproveEntity>> serverApproveEntities =
Sets.union(
Sets.difference(
domain.getTransferData().getServerApproveEntities(),
ImmutableSet.of(oldRecurringVKey)),
ImmutableSet.of(newRecurringVKey));
domainBuilder.setTransferData(
domain
.getTransferData()
.asBuilder()
.setServerApproveEntities(ImmutableSet.copyOf(serverApproveEntities))
.setServerApproveAutorenewEvent(newRecurringVKey)
.build());
domainChange.append(
String.format(
" Changed transferData.serverApproveAutoRenewEvent from %s to %s.\n",
oldRecurringVKey, newRecurringVKey));
domainChange.append(
String.format(
" Changed transferData.serverApproveEntities to remove %s and add %s.\n",
oldRecurringVKey, newRecurringVKey));
}
ImmutableSet<GracePeriod> updatedGracePeriod =
domain.getGracePeriods().stream()
.map(
gracePeriod ->
gracePeriod.getRecurringBillingEvent().equals(oldRecurringVKey)
? gracePeriod.cloneWithRecurringBillingEvent(newRecurringVKey)
: gracePeriod)
.collect(toImmutableSet());
if (!updatedGracePeriod.equals(domain.getGracePeriods())) {
domainBuilder.setGracePeriods(updatedGracePeriod);
domainChange.append(
String.format(
" Changed gracePeriods to remove %s and add %s.\n",
oldRecurringVKey, newRecurringVKey));
}
DomainBase updatedDomain = domainBuilder.build();
if (!updatedDomain.equals(domain)) {
stageEntityChange(domain, updatedDomain);
appendChangeMessage(domainChange.toString());
}
}
}

View file

@ -48,6 +48,8 @@ public final class RegistryTool {
.put("create_reserved_list", CreateReservedListCommand.class)
.put("create_tld", CreateTldCommand.class)
.put("curl", CurlCommand.class)
.put("dedupe_one_time_billing_event_ids", DedupeOneTimeBillingEventIdsCommand.class)
.put("dedupe_recurring_billing_event_ids", DedupeRecurringBillingEventIdsCommand.class)
.put("delete_allocation_tokens", DeleteAllocationTokensCommand.class)
.put("delete_domain", DeleteDomainCommand.class)
.put("delete_host", DeleteHostCommand.class)
@ -99,7 +101,6 @@ public final class RegistryTool {
.put("remove_ip_address", RemoveIpAddressCommand.class)
.put("renew_domain", RenewDomainCommand.class)
.put("resave_entities", ResaveEntitiesCommand.class)
.put("resave_entities_with_unique_id", ResaveEntitiesWithUniqueIdCommand.class)
.put("resave_environment_entities", ResaveEnvironmentEntitiesCommand.class)
.put("resave_epp_resource", ResaveEppResourceCommand.class)
.put("send_escrow_report_to_icann", SendEscrowReportToIcannCommand.class)

View file

@ -38,9 +38,9 @@ import org.joda.money.Money;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
/** Unit tests for {@link ResaveEntitiesWithUniqueIdCommand}. */
class ResaveEntitiesWithUniqueIdCommandTest
extends CommandTestCase<ResaveEntitiesWithUniqueIdCommand> {
/** Unit tests for {@link DedupeOneTimeBillingEventIdsCommand}. */
class DedupeOneTimeBillingEventIdsCommandTest
extends CommandTestCase<DedupeOneTimeBillingEventIdsCommand> {
DomainBase domain;
HistoryEntry historyEntry;
@ -48,7 +48,7 @@ class ResaveEntitiesWithUniqueIdCommandTest
BillingEvent.OneTime billingEventToResave;
@BeforeEach
void setUp() {
void beforeEach() {
createTld("foobar");
domain = persistActiveDomain("foo.foobar");
historyEntry = persistHistoryEntry(domain);

View file

@ -0,0 +1,262 @@
// Copyright 2020 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.ImmutableObjectSubject.assertAboutImmutableObjects;
import static google.registry.model.ofy.ObjectifyService.ofy;
import static google.registry.testing.DatastoreHelper.createTld;
import static google.registry.testing.DatastoreHelper.persistActiveDomain;
import static google.registry.testing.DatastoreHelper.persistResource;
import static google.registry.util.DateTimeUtils.END_OF_TIME;
import static org.joda.money.CurrencyUnit.USD;
import static org.joda.time.DateTimeZone.UTC;
import static org.junit.jupiter.api.Assertions.fail;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.googlecode.objectify.Key;
import google.registry.model.ImmutableObject;
import google.registry.model.billing.BillingEvent;
import google.registry.model.billing.BillingEvent.Flag;
import google.registry.model.billing.BillingEvent.Reason;
import google.registry.model.domain.DomainBase;
import google.registry.model.domain.GracePeriod;
import google.registry.model.domain.rgp.GracePeriodStatus;
import google.registry.model.reporting.HistoryEntry;
import google.registry.model.transfer.DomainTransferData;
import java.util.Arrays;
import org.joda.money.Money;
import org.joda.time.DateTime;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
/** Unit tests for {@link DedupeRecurringBillingEventIdsCommand}. */
class DedupeRecurringBillingEventIdsCommandTest
extends CommandTestCase<DedupeRecurringBillingEventIdsCommand> {
private final DateTime now = DateTime.now(UTC);
private DomainBase domain1;
private DomainBase domain2;
private HistoryEntry historyEntry1;
private HistoryEntry historyEntry2;
private BillingEvent.Recurring recurring1;
private BillingEvent.Recurring recurring2;
@BeforeEach
void beforeEach() {
createTld("tld");
domain1 = persistActiveDomain("foo.tld");
domain2 = persistActiveDomain("bar.tld");
historyEntry1 =
persistResource(
new HistoryEntry.Builder().setParent(domain1).setModificationTime(now).build());
historyEntry2 =
persistResource(
new HistoryEntry.Builder()
.setParent(domain2)
.setModificationTime(now.plusDays(1))
.build());
recurring1 =
persistResource(
new BillingEvent.Recurring.Builder()
.setParent(historyEntry1)
.setFlags(ImmutableSet.of(Flag.AUTO_RENEW))
.setReason(Reason.RENEW)
.setEventTime(now.plusYears(1))
.setRecurrenceEndTime(END_OF_TIME)
.setClientId("a registrar")
.setTargetId("foo.tld")
.build());
recurring2 =
persistResource(
new BillingEvent.Recurring.Builder()
.setId(recurring1.getId())
.setParent(historyEntry2)
.setFlags(ImmutableSet.of(Flag.AUTO_RENEW))
.setReason(Reason.RENEW)
.setEventTime(now.plusYears(1))
.setRecurrenceEndTime(END_OF_TIME)
.setClientId("a registrar")
.setTargetId("bar.tld")
.build());
}
@Test
void testOnlyResaveBillingEventsCorrectly() throws Exception {
assertThat(recurring1.getId()).isEqualTo(recurring2.getId());
runCommand(
"--force",
"--key_paths_file",
writeToNamedTmpFile("keypath.txt", getKeyPathLiteral(recurring1, recurring2)));
assertNotChangeExceptUpdateTime(domain1, domain2, historyEntry1, historyEntry2);
assertNotInDatastore(recurring1, recurring2);
ImmutableList<BillingEvent.Recurring> recurrings = loadAllRecurrings();
assertThat(recurrings.size()).isEqualTo(2);
recurrings.forEach(
newRecurring -> {
if (newRecurring.getTargetId().equals("foo.tld")) {
assertSameRecurringEntityExceptId(newRecurring, recurring1);
} else if (newRecurring.getTargetId().equals("bar.tld")) {
assertSameRecurringEntityExceptId(newRecurring, recurring2);
} else {
fail("Unknown BillingEvent.Recurring entity: " + newRecurring.createVKey());
}
});
}
@Test
void testResaveAssociatedDomainAndOneTimeBillingEventCorrectly() throws Exception {
assertThat(recurring1.getId()).isEqualTo(recurring2.getId());
domain1 =
persistResource(
domain1
.asBuilder()
.setAutorenewBillingEvent(recurring1.createVKey())
.setGracePeriods(
ImmutableSet.of(
GracePeriod.createForRecurring(
GracePeriodStatus.AUTO_RENEW,
domain1.getRepoId(),
now.plusDays(45),
"a registrar",
recurring1.createVKey())))
.setTransferData(
new DomainTransferData.Builder()
.setServerApproveAutorenewEvent(recurring1.createVKey())
.setServerApproveEntities(ImmutableSet.of(recurring1.createVKey()))
.build())
.build());
BillingEvent.OneTime oneTime =
persistResource(
new BillingEvent.OneTime.Builder()
.setClientId("a registrar")
.setTargetId("foo.tld")
.setParent(historyEntry1)
.setReason(Reason.CREATE)
.setFlags(ImmutableSet.of(Flag.SYNTHETIC))
.setSyntheticCreationTime(now)
.setPeriodYears(2)
.setCost(Money.of(USD, 1))
.setEventTime(now)
.setBillingTime(now.plusDays(5))
.setCancellationMatchingBillingEvent(recurring1.createVKey())
.build());
runCommand(
"--force",
"--key_paths_file",
writeToNamedTmpFile("keypath.txt", getKeyPathLiteral(recurring1, recurring2)));
assertNotChangeExceptUpdateTime(domain2, historyEntry1, historyEntry2);
assertNotInDatastore(recurring1, recurring2);
ImmutableList<BillingEvent.Recurring> recurrings = loadAllRecurrings();
assertThat(recurrings.size()).isEqualTo(2);
recurrings.forEach(
newRecurring -> {
if (newRecurring.getTargetId().equals("foo.tld")) {
assertSameRecurringEntityExceptId(newRecurring, recurring1);
BillingEvent.OneTime persistedOneTime = ofy().load().entity(oneTime).now();
assertAboutImmutableObjects()
.that(persistedOneTime)
.isEqualExceptFields(oneTime, "cancellationMatchingBillingEvent");
assertThat(persistedOneTime.getCancellationMatchingBillingEvent())
.isEqualTo(newRecurring.createVKey());
DomainBase persistedDomain = ofy().load().entity(domain1).now();
assertAboutImmutableObjects()
.that(persistedDomain)
.isEqualExceptFields(
domain1,
"updateTimestamp",
"revisions",
"gracePeriods",
"transferData",
"autorenewBillingEvent");
assertThat(persistedDomain.getAutorenewBillingEvent())
.isEqualTo(newRecurring.createVKey());
assertThat(persistedDomain.getGracePeriods())
.containsExactly(
GracePeriod.createForRecurring(
GracePeriodStatus.AUTO_RENEW,
domain1.getRepoId(),
now.plusDays(45),
"a registrar",
newRecurring.createVKey()));
assertThat(persistedDomain.getTransferData().getServerApproveAutorenewEvent())
.isEqualTo(newRecurring.createVKey());
assertThat(persistedDomain.getTransferData().getServerApproveEntities())
.containsExactly(newRecurring.createVKey());
} else if (newRecurring.getTargetId().equals("bar.tld")) {
assertSameRecurringEntityExceptId(newRecurring, recurring2);
} else {
fail("Unknown BillingEvent.Recurring entity: " + newRecurring.createVKey());
}
});
}
private static void assertNotInDatastore(ImmutableObject... entities) {
for (ImmutableObject entity : entities) {
assertThat(ofy().load().entity(entity).now()).isNull();
}
}
private static void assertNotChangeInDatastore(ImmutableObject... entities) {
for (ImmutableObject entity : entities) {
assertThat(ofy().load().entity(entity).now()).isEqualTo(entity);
}
}
private static void assertNotChangeExceptUpdateTime(ImmutableObject... entities) {
for (ImmutableObject entity : entities) {
assertAboutImmutableObjects()
.that(ofy().load().entity(entity).now())
.isEqualExceptFields(entity, "updateTimestamp", "revisions");
}
}
private static void assertSameRecurringEntityExceptId(
BillingEvent.Recurring recurring1, BillingEvent.Recurring recurring2) {
assertAboutImmutableObjects().that(recurring1).isEqualExceptFields(recurring2, "id");
}
private static ImmutableList<BillingEvent.Recurring> loadAllRecurrings() {
return ImmutableList.copyOf(ofy().load().type(BillingEvent.Recurring.class));
}
private static String getKeyPathLiteral(Object... entities) {
return Arrays.stream(entities)
.map(
entity -> {
Key<?> key = Key.create(entity);
return String.format(
"\"DomainBase\", \"%s\", \"HistoryEntry\", %s, \"%s\", %s",
key.getParent().getParent().getName(),
key.getParent().getId(),
key.getKind(),
key.getId());
})
.reduce((k1, k2) -> k1 + "\n" + k2)
.get();
}
}