Perform synchronous contact delete in SQL (#1137)

In SQL the contact of a domain is an indexed field and therefore we can
find linked domains synchronously, without the need for MapReduce.

The delete logic is mostly lifted from DeleteContactsAndHostsAction, but
because everything happens in a transaction we do not need to recheck a
lot of the preconditions that were necessary to ensure that the async
delete request still meets the conditions that when the request was
enqueued.
This commit is contained in:
Lai Jiang 2021-05-07 10:48:51 -04:00 committed by GitHub
parent c033720b01
commit 235fbfd18e
13 changed files with 358 additions and 87 deletions

View file

@ -109,6 +109,7 @@ import org.joda.time.Duration;
* A mapreduce that processes batch asynchronous deletions of contact and host resources by mapping * A mapreduce that processes batch asynchronous deletions of contact and host resources by mapping
* over all domains and checking for any references to the contacts/hosts in pending deletion. * over all domains and checking for any references to the contacts/hosts in pending deletion.
*/ */
@Deprecated
@Action( @Action(
service = Action.Service.BACKEND, service = Action.Service.BACKEND,
path = "/_dr/task/deleteContactsAndHosts", path = "/_dr/task/deleteContactsAndHosts",

View file

@ -16,6 +16,7 @@ package google.registry.flows;
import static com.google.common.collect.Sets.intersection; import static com.google.common.collect.Sets.intersection;
import static google.registry.model.EppResourceUtils.getLinkedDomainKeys; import static google.registry.model.EppResourceUtils.getLinkedDomainKeys;
import static google.registry.model.EppResourceUtils.isLinked;
import static google.registry.model.EppResourceUtils.loadByForeignKey; import static google.registry.model.EppResourceUtils.loadByForeignKey;
import static google.registry.model.index.ForeignKeyIndex.loadAndGetKey; import static google.registry.model.index.ForeignKeyIndex.loadAndGetKey;
import static google.registry.persistence.transaction.TransactionManagerFactory.tm; import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
@ -62,7 +63,10 @@ public final class ResourceFlowUtils {
private ResourceFlowUtils() {} private ResourceFlowUtils() {}
/** In {@link #failfastForAsyncDelete}, check this (arbitrary) number of query results. */ /**
* In {@link #checkLinkedDomains(String, DateTime, Class, Function)}, check this (arbitrary)
* number of query results.
*/
private static final int FAILFAST_CHECK_COUNT = 5; private static final int FAILFAST_CHECK_COUNT = 5;
/** Check that the given clientId corresponds to the owner of given resource. */ /** Check that the given clientId corresponds to the owner of given resource. */
@ -73,36 +77,54 @@ public final class ResourceFlowUtils {
} }
} }
/** Check whether an asynchronous delete would obviously fail, and throw an exception if so. */ /**
public static <R extends EppResource> void failfastForAsyncDelete( * Check whether if there are domains linked to the resource to be deleted. Throws an exception if
* so.
*
* <p>Note that in datastore this is a smoke test as the query for linked domains is eventually
* consistent, so we only check a few domains to fail fast.
*/
public static <R extends EppResource> void checkLinkedDomains(
final String targetId, final String targetId,
final DateTime now, final DateTime now,
final Class<R> resourceClass, final Class<R> resourceClass,
final Function<DomainBase, ImmutableSet<?>> getPotentialReferences) throws EppException { final Function<DomainBase, ImmutableSet<?>> getPotentialReferences)
// Enter a transactionless context briefly. throws EppException {
EppException failfastException = EppException failfastException =
tm().doTransactionless( tm().isOfy()
() -> { ? tm().doTransactionless(
final ForeignKeyIndex<R> fki = ForeignKeyIndex.load(resourceClass, targetId, now); () -> {
if (fki == null) { final ForeignKeyIndex<R> fki =
return new ResourceDoesNotExistException(resourceClass, targetId); ForeignKeyIndex.load(resourceClass, targetId, now);
} if (fki == null) {
/* Query for the first few linked domains, and if found, actually load them. The return new ResourceDoesNotExistException(resourceClass, targetId);
* query is eventually consistent and so might be very stale, but the direct }
* load will not be stale, just non-transactional. If we find at least one // Query for the first few linked domains, and if found, actually load them.
* actual reference then we can reliably fail. If we don't find any, we can't // The query is eventually consistent and so might be very stale, but the
* trust the query and need to do the full mapreduce. // direct load will not be stale, just non-transactional. If we find at least
*/ // one actual reference then we can reliably fail. If we don't find any,
Iterable<VKey<DomainBase>> keys = // we can't trust the query and need to do the full mapreduce.
getLinkedDomainKeys(fki.getResourceKey(), now, FAILFAST_CHECK_COUNT); Iterable<VKey<DomainBase>> keys =
getLinkedDomainKeys(fki.getResourceKey(), now, FAILFAST_CHECK_COUNT);
VKey<R> resourceVKey = fki.getResourceKey(); VKey<R> resourceVKey = fki.getResourceKey();
Predicate<DomainBase> predicate = Predicate<DomainBase> predicate =
domain -> getPotentialReferences.apply(domain).contains(resourceVKey); domain -> getPotentialReferences.apply(domain).contains(resourceVKey);
return tm().loadByKeys(keys).values().stream().anyMatch(predicate) return tm().loadByKeys(keys).values().stream().anyMatch(predicate)
? new ResourceToDeleteIsReferencedException() ? new ResourceToDeleteIsReferencedException()
: null; : null;
}); })
: tm().transact(
() -> {
final ForeignKeyIndex<R> fki =
ForeignKeyIndex.load(resourceClass, targetId, now);
if (fki == null) {
return new ResourceDoesNotExistException(resourceClass, targetId);
}
return isLinked(fki.getResourceKey(), now)
? new ResourceToDeleteIsReferencedException()
: null;
});
if (failfastException != null) { if (failfastException != null) {
throw failfastException; throw failfastException;
} }
@ -123,8 +145,7 @@ public final class ResourceFlowUtils {
} }
public static <R extends EppResource & ForeignKeyedEppResource> R loadAndVerifyExistence( public static <R extends EppResource & ForeignKeyedEppResource> R loadAndVerifyExistence(
Class<R> clazz, String targetId, DateTime now) Class<R> clazz, String targetId, DateTime now) throws ResourceDoesNotExistException {
throws ResourceDoesNotExistException {
return verifyExistence(clazz, targetId, loadByForeignKey(clazz, targetId, now)); return verifyExistence(clazz, targetId, loadByForeignKey(clazz, targetId, now));
} }
@ -156,16 +177,16 @@ public final class ResourceFlowUtils {
} }
/** Check that the given AuthInfo is either missing or else is valid for the given resource. */ /** Check that the given AuthInfo is either missing or else is valid for the given resource. */
public static void verifyOptionalAuthInfo( public static void verifyOptionalAuthInfo(Optional<AuthInfo> authInfo, ContactResource contact)
Optional<AuthInfo> authInfo, ContactResource contact) throws EppException { throws EppException {
if (authInfo.isPresent()) { if (authInfo.isPresent()) {
verifyAuthInfo(authInfo.get(), contact); verifyAuthInfo(authInfo.get(), contact);
} }
} }
/** Check that the given AuthInfo is either missing or else is valid for the given resource. */ /** Check that the given AuthInfo is either missing or else is valid for the given resource. */
public static void verifyOptionalAuthInfo( public static void verifyOptionalAuthInfo(Optional<AuthInfo> authInfo, DomainBase domain)
Optional<AuthInfo> authInfo, DomainBase domain) throws EppException { throws EppException {
if (authInfo.isPresent()) { if (authInfo.isPresent()) {
verifyAuthInfo(authInfo.get(), domain); verifyAuthInfo(authInfo.get(), domain);
} }
@ -229,7 +250,7 @@ public final class ResourceFlowUtils {
/** Check that the same values aren't being added and removed in an update command. */ /** Check that the same values aren't being added and removed in an update command. */
public static void checkSameValuesNotAddedAndRemoved( public static void checkSameValuesNotAddedAndRemoved(
ImmutableSet<?> fieldsToAdd, ImmutableSet<?> fieldsToRemove) ImmutableSet<?> fieldsToAdd, ImmutableSet<?> fieldsToRemove)
throws AddRemoveSameValueException { throws AddRemoveSameValueException {
if (!intersection(fieldsToAdd, fieldsToRemove).isEmpty()) { if (!intersection(fieldsToAdd, fieldsToRemove).isEmpty()) {
throw new AddRemoveSameValueException(); throw new AddRemoveSameValueException();
} }

View file

@ -15,12 +15,16 @@
package google.registry.flows.contact; package google.registry.flows.contact;
import static google.registry.flows.FlowUtils.validateClientIsLoggedIn; import static google.registry.flows.FlowUtils.validateClientIsLoggedIn;
import static google.registry.flows.ResourceFlowUtils.failfastForAsyncDelete; import static google.registry.flows.ResourceFlowUtils.checkLinkedDomains;
import static google.registry.flows.ResourceFlowUtils.loadAndVerifyExistence; import static google.registry.flows.ResourceFlowUtils.loadAndVerifyExistence;
import static google.registry.flows.ResourceFlowUtils.verifyNoDisallowedStatuses; import static google.registry.flows.ResourceFlowUtils.verifyNoDisallowedStatuses;
import static google.registry.flows.ResourceFlowUtils.verifyOptionalAuthInfo; import static google.registry.flows.ResourceFlowUtils.verifyOptionalAuthInfo;
import static google.registry.flows.ResourceFlowUtils.verifyResourceOwnership; import static google.registry.flows.ResourceFlowUtils.verifyResourceOwnership;
import static google.registry.model.ResourceTransferUtils.denyPendingTransfer;
import static google.registry.model.ResourceTransferUtils.handlePendingTransferOnDelete;
import static google.registry.model.eppoutput.Result.Code.SUCCESS;
import static google.registry.model.eppoutput.Result.Code.SUCCESS_WITH_ACTION_PENDING; import static google.registry.model.eppoutput.Result.Code.SUCCESS_WITH_ACTION_PENDING;
import static google.registry.model.transfer.TransferStatus.SERVER_CANCELLED;
import static google.registry.persistence.transaction.TransactionManagerFactory.tm; import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
import com.google.common.collect.ImmutableSet; import com.google.common.collect.ImmutableSet;
@ -40,7 +44,8 @@ import google.registry.model.eppcommon.AuthInfo;
import google.registry.model.eppcommon.StatusValue; import google.registry.model.eppcommon.StatusValue;
import google.registry.model.eppcommon.Trid; import google.registry.model.eppcommon.Trid;
import google.registry.model.eppoutput.EppResponse; import google.registry.model.eppoutput.EppResponse;
import google.registry.model.reporting.HistoryEntry; import google.registry.model.eppoutput.Result.Code;
import google.registry.model.reporting.HistoryEntry.Type;
import google.registry.model.reporting.IcannReportingTypes.ActivityReportField; import google.registry.model.reporting.IcannReportingTypes.ActivityReportField;
import java.util.Optional; import java.util.Optional;
import javax.inject.Inject; import javax.inject.Inject;
@ -63,10 +68,11 @@ import org.joda.time.DateTime;
@ReportingSpec(ActivityReportField.CONTACT_DELETE) @ReportingSpec(ActivityReportField.CONTACT_DELETE)
public final class ContactDeleteFlow implements TransactionalFlow { public final class ContactDeleteFlow implements TransactionalFlow {
private static final ImmutableSet<StatusValue> DISALLOWED_STATUSES = ImmutableSet.of( private static final ImmutableSet<StatusValue> DISALLOWED_STATUSES =
StatusValue.CLIENT_DELETE_PROHIBITED, ImmutableSet.of(
StatusValue.PENDING_DELETE, StatusValue.CLIENT_DELETE_PROHIBITED,
StatusValue.SERVER_DELETE_PROHIBITED); StatusValue.PENDING_DELETE,
StatusValue.SERVER_DELETE_PROHIBITED);
@Inject ExtensionManager extensionManager; @Inject ExtensionManager extensionManager;
@Inject @ClientId String clientId; @Inject @ClientId String clientId;
@ -77,7 +83,9 @@ public final class ContactDeleteFlow implements TransactionalFlow {
@Inject ContactHistory.Builder historyBuilder; @Inject ContactHistory.Builder historyBuilder;
@Inject AsyncTaskEnqueuer asyncTaskEnqueuer; @Inject AsyncTaskEnqueuer asyncTaskEnqueuer;
@Inject EppResponse.Builder responseBuilder; @Inject EppResponse.Builder responseBuilder;
@Inject ContactDeleteFlow() {}
@Inject
ContactDeleteFlow() {}
@Override @Override
public final EppResponse run() throws EppException { public final EppResponse run() throws EppException {
@ -85,23 +93,45 @@ public final class ContactDeleteFlow implements TransactionalFlow {
extensionManager.validate(); extensionManager.validate();
validateClientIsLoggedIn(clientId); validateClientIsLoggedIn(clientId);
DateTime now = tm().getTransactionTime(); DateTime now = tm().getTransactionTime();
failfastForAsyncDelete(targetId, now, ContactResource.class, DomainBase::getReferencedContacts); checkLinkedDomains(targetId, now, ContactResource.class, DomainBase::getReferencedContacts);
ContactResource existingContact = loadAndVerifyExistence(ContactResource.class, targetId, now); ContactResource existingContact = loadAndVerifyExistence(ContactResource.class, targetId, now);
verifyNoDisallowedStatuses(existingContact, DISALLOWED_STATUSES); verifyNoDisallowedStatuses(existingContact, DISALLOWED_STATUSES);
verifyOptionalAuthInfo(authInfo, existingContact); verifyOptionalAuthInfo(authInfo, existingContact);
if (!isSuperuser) { if (!isSuperuser) {
verifyResourceOwnership(clientId, existingContact); verifyResourceOwnership(clientId, existingContact);
} }
asyncTaskEnqueuer.enqueueAsyncDelete( Type historyEntryType;
existingContact, tm().getTransactionTime(), clientId, trid, isSuperuser); Code resultCode;
ContactResource newContact = ContactResource newContact;
existingContact.asBuilder().addStatusValue(StatusValue.PENDING_DELETE).build(); if (tm().isOfy()) {
historyBuilder asyncTaskEnqueuer.enqueueAsyncDelete(
.setType(HistoryEntry.Type.CONTACT_PENDING_DELETE) existingContact, tm().getTransactionTime(), clientId, trid, isSuperuser);
.setModificationTime(now) newContact = existingContact.asBuilder().addStatusValue(StatusValue.PENDING_DELETE).build();
.setContactBase(newContact); historyEntryType = Type.CONTACT_PENDING_DELETE;
tm().insert(historyBuilder.build()); resultCode = SUCCESS_WITH_ACTION_PENDING;
} else {
// Handle pending transfers on contact deletion.
newContact =
existingContact.getStatusValues().contains(StatusValue.PENDING_TRANSFER)
? denyPendingTransfer(existingContact, SERVER_CANCELLED, now, clientId)
: existingContact;
// Wipe out PII on contact deletion.
newContact =
newContact.asBuilder().wipeOut().setStatusValues(null).setDeletionTime(now).build();
historyEntryType = Type.CONTACT_DELETE;
resultCode = SUCCESS;
}
ContactHistory contactHistory =
historyBuilder
.setType(historyEntryType)
.setModificationTime(now)
.setContactBase(newContact)
.build();
if (!tm().isOfy()) {
handlePendingTransferOnDelete(existingContact, newContact, now, contactHistory);
}
tm().insert(contactHistory);
tm().update(newContact); tm().update(newContact);
return responseBuilder.setResultFromCode(SUCCESS_WITH_ACTION_PENDING).build(); return responseBuilder.setResultFromCode(resultCode).build();
} }
} }

View file

@ -15,7 +15,7 @@
package google.registry.flows.host; package google.registry.flows.host;
import static google.registry.flows.FlowUtils.validateClientIsLoggedIn; import static google.registry.flows.FlowUtils.validateClientIsLoggedIn;
import static google.registry.flows.ResourceFlowUtils.failfastForAsyncDelete; import static google.registry.flows.ResourceFlowUtils.checkLinkedDomains;
import static google.registry.flows.ResourceFlowUtils.loadAndVerifyExistence; import static google.registry.flows.ResourceFlowUtils.loadAndVerifyExistence;
import static google.registry.flows.ResourceFlowUtils.verifyNoDisallowedStatuses; import static google.registry.flows.ResourceFlowUtils.verifyNoDisallowedStatuses;
import static google.registry.flows.ResourceFlowUtils.verifyResourceOwnership; import static google.registry.flows.ResourceFlowUtils.verifyResourceOwnership;
@ -65,10 +65,11 @@ import org.joda.time.DateTime;
@ReportingSpec(ActivityReportField.HOST_DELETE) @ReportingSpec(ActivityReportField.HOST_DELETE)
public final class HostDeleteFlow implements TransactionalFlow { public final class HostDeleteFlow implements TransactionalFlow {
private static final ImmutableSet<StatusValue> DISALLOWED_STATUSES = ImmutableSet.of( private static final ImmutableSet<StatusValue> DISALLOWED_STATUSES =
StatusValue.CLIENT_DELETE_PROHIBITED, ImmutableSet.of(
StatusValue.PENDING_DELETE, StatusValue.CLIENT_DELETE_PROHIBITED,
StatusValue.SERVER_DELETE_PROHIBITED); StatusValue.PENDING_DELETE,
StatusValue.SERVER_DELETE_PROHIBITED);
@Inject ExtensionManager extensionManager; @Inject ExtensionManager extensionManager;
@Inject @ClientId String clientId; @Inject @ClientId String clientId;
@ -78,7 +79,9 @@ public final class HostDeleteFlow implements TransactionalFlow {
@Inject HistoryEntry.Builder historyBuilder; @Inject HistoryEntry.Builder historyBuilder;
@Inject AsyncTaskEnqueuer asyncTaskEnqueuer; @Inject AsyncTaskEnqueuer asyncTaskEnqueuer;
@Inject EppResponse.Builder responseBuilder; @Inject EppResponse.Builder responseBuilder;
@Inject HostDeleteFlow() {}
@Inject
HostDeleteFlow() {}
@Override @Override
public final EppResponse run() throws EppException { public final EppResponse run() throws EppException {
@ -87,7 +90,7 @@ public final class HostDeleteFlow implements TransactionalFlow {
validateClientIsLoggedIn(clientId); validateClientIsLoggedIn(clientId);
DateTime now = tm().getTransactionTime(); DateTime now = tm().getTransactionTime();
validateHostName(targetId); validateHostName(targetId);
failfastForAsyncDelete(targetId, now, HostResource.class, DomainBase::getNameservers); checkLinkedDomains(targetId, now, HostResource.class, DomainBase::getNameservers);
HostResource existingHost = loadAndVerifyExistence(HostResource.class, targetId, now); HostResource existingHost = loadAndVerifyExistence(HostResource.class, targetId, now);
verifyNoDisallowedStatuses(existingHost, DISALLOWED_STATUSES); verifyNoDisallowedStatuses(existingHost, DISALLOWED_STATUSES);
if (!isSuperuser) { if (!isSuperuser) {

View file

@ -18,9 +18,11 @@ import static google.registry.model.eppoutput.Result.Code.SUCCESS;
import static google.registry.model.eppoutput.Result.Code.SUCCESS_WITH_ACK_MESSAGE; import static google.registry.model.eppoutput.Result.Code.SUCCESS_WITH_ACK_MESSAGE;
import static google.registry.model.eppoutput.Result.Code.SUCCESS_WITH_ACTION_PENDING; import static google.registry.model.eppoutput.Result.Code.SUCCESS_WITH_ACTION_PENDING;
import static google.registry.model.eppoutput.Result.Code.SUCCESS_WITH_NO_MESSAGES; import static google.registry.model.eppoutput.Result.Code.SUCCESS_WITH_NO_MESSAGES;
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
import static google.registry.testing.EppMetricSubject.assertThat; import static google.registry.testing.EppMetricSubject.assertThat;
import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableMap;
import google.registry.model.eppoutput.Result;
import google.registry.testing.AppEngineExtension; import google.registry.testing.AppEngineExtension;
import google.registry.testing.DualDatabaseTest; import google.registry.testing.DualDatabaseTest;
import google.registry.testing.TestOfyAndSql; import google.registry.testing.TestOfyAndSql;
@ -63,14 +65,22 @@ class EppLifecycleContactTest extends EppTestCase {
.hasCommandName("ContactInfo") .hasCommandName("ContactInfo")
.and() .and()
.hasStatus(SUCCESS); .hasStatus(SUCCESS);
assertThatCommand("contact_delete_sh8013.xml") Result.Code resultCode;
.hasResponse("contact_delete_response_sh8013.xml"); if (tm().isOfy()) {
assertThatCommand("contact_delete_sh8013.xml")
.hasResponse("contact_delete_response_sh8013_pending.xml");
resultCode = SUCCESS_WITH_ACTION_PENDING;
} else {
assertThatCommand("contact_delete_sh8013.xml")
.hasResponse("contact_delete_response_sh8013.xml");
resultCode = SUCCESS;
}
assertThat(getRecordedEppMetric()) assertThat(getRecordedEppMetric())
.hasClientId("NewRegistrar") .hasClientId("NewRegistrar")
.and() .and()
.hasCommandName("ContactDelete") .hasCommandName("ContactDelete")
.and() .and()
.hasStatus(SUCCESS_WITH_ACTION_PENDING); .hasStatus(resultCode);
assertThatLogoutSucceeds(); assertThatLogoutSucceeds();
} }

View file

@ -14,19 +14,26 @@
package google.registry.flows.contact; package google.registry.flows.contact;
import static com.google.common.collect.MoreCollectors.onlyElement;
import static com.google.common.truth.Truth.assertThat; import static com.google.common.truth.Truth.assertThat;
import static google.registry.batch.AsyncTaskEnqueuer.QUEUE_ASYNC_DELETE;
import static google.registry.model.reporting.HistoryEntry.Type.CONTACT_PENDING_DELETE;
import static google.registry.testing.ContactResourceSubject.assertAboutContacts; import static google.registry.testing.ContactResourceSubject.assertAboutContacts;
import static google.registry.testing.DatabaseHelper.assertNoBillingEvents; import static google.registry.testing.DatabaseHelper.assertNoBillingEvents;
import static google.registry.testing.DatabaseHelper.createTld; import static google.registry.testing.DatabaseHelper.createTld;
import static google.registry.testing.DatabaseHelper.getPollMessages;
import static google.registry.testing.DatabaseHelper.newContactResource; import static google.registry.testing.DatabaseHelper.newContactResource;
import static google.registry.testing.DatabaseHelper.newDomainBase; import static google.registry.testing.DatabaseHelper.newDomainBase;
import static google.registry.testing.DatabaseHelper.persistActiveContact; import static google.registry.testing.DatabaseHelper.persistActiveContact;
import static google.registry.testing.DatabaseHelper.persistContactWithPendingTransfer;
import static google.registry.testing.DatabaseHelper.persistDeletedContact; import static google.registry.testing.DatabaseHelper.persistDeletedContact;
import static google.registry.testing.DatabaseHelper.persistResource; import static google.registry.testing.DatabaseHelper.persistResource;
import static google.registry.testing.EppExceptionSubject.assertAboutEppExceptions; import static google.registry.testing.EppExceptionSubject.assertAboutEppExceptions;
import static google.registry.testing.TaskQueueHelper.assertNoTasksEnqueued;
import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertThrows;
import com.google.common.collect.ImmutableSet; import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
import google.registry.flows.EppException; import google.registry.flows.EppException;
import google.registry.flows.ResourceFlowTestCase; import google.registry.flows.ResourceFlowTestCase;
import google.registry.flows.ResourceFlowUtils.ResourceDoesNotExistException; import google.registry.flows.ResourceFlowUtils.ResourceDoesNotExistException;
@ -36,10 +43,19 @@ import google.registry.flows.exceptions.ResourceToDeleteIsReferencedException;
import google.registry.model.contact.ContactResource; import google.registry.model.contact.ContactResource;
import google.registry.model.eppcommon.StatusValue; import google.registry.model.eppcommon.StatusValue;
import google.registry.model.eppcommon.Trid; import google.registry.model.eppcommon.Trid;
import google.registry.model.reporting.HistoryEntry; import google.registry.model.poll.PendingActionNotificationResponse;
import google.registry.model.poll.PollMessage;
import google.registry.model.registry.Registry;
import google.registry.model.reporting.HistoryEntry.Type;
import google.registry.model.transfer.TransferData;
import google.registry.model.transfer.TransferResponse;
import google.registry.model.transfer.TransferStatus;
import google.registry.testing.DualDatabaseTest; import google.registry.testing.DualDatabaseTest;
import google.registry.testing.ReplayExtension; import google.registry.testing.ReplayExtension;
import google.registry.testing.TestOfyAndSql; import google.registry.testing.TestOfyAndSql;
import google.registry.testing.TestOfyOnly;
import google.registry.testing.TestSqlOnly;
import org.joda.time.DateTime;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Order; import org.junit.jupiter.api.Order;
import org.junit.jupiter.api.extension.RegisterExtension; import org.junit.jupiter.api.extension.RegisterExtension;
@ -57,18 +73,24 @@ class ContactDeleteFlowTest extends ResourceFlowTestCase<ContactDeleteFlow, Cont
setEppInput("contact_delete.xml"); setEppInput("contact_delete.xml");
} }
@TestOfyAndSql @TestOfyOnly
void testDryRun() throws Exception { void testDryRun_ofy() throws Exception {
persistActiveContact(getUniqueIdFromCommand());
dryRunFlowAssertResponse(loadFile("contact_delete_response_pending.xml"));
}
@TestSqlOnly
void testDryRun_sql() throws Exception {
persistActiveContact(getUniqueIdFromCommand()); persistActiveContact(getUniqueIdFromCommand());
dryRunFlowAssertResponse(loadFile("contact_delete_response.xml")); dryRunFlowAssertResponse(loadFile("contact_delete_response.xml"));
} }
@TestOfyAndSql @TestOfyOnly
void testSuccess() throws Exception { void testSuccess_ofy() throws Exception {
persistActiveContact(getUniqueIdFromCommand()); persistActiveContact(getUniqueIdFromCommand());
clock.advanceOneMilli(); clock.advanceOneMilli();
assertTransactionalFlow(true); assertTransactionalFlow(true);
runFlowAssertResponse(loadFile("contact_delete_response.xml")); runFlowAssertResponse(loadFile("contact_delete_response_pending.xml"));
ContactResource deletedContact = reloadResourceByForeignKey(); ContactResource deletedContact = reloadResourceByForeignKey();
assertAboutContacts().that(deletedContact).hasStatusValue(StatusValue.PENDING_DELETE); assertAboutContacts().that(deletedContact).hasStatusValue(StatusValue.PENDING_DELETE);
assertAsyncDeletionTaskEnqueued( assertAsyncDeletionTaskEnqueued(
@ -76,18 +98,104 @@ class ContactDeleteFlowTest extends ResourceFlowTestCase<ContactDeleteFlow, Cont
assertAboutContacts() assertAboutContacts()
.that(deletedContact) .that(deletedContact)
.hasOnlyOneHistoryEntryWhich() .hasOnlyOneHistoryEntryWhich()
.hasType(HistoryEntry.Type.CONTACT_PENDING_DELETE); .hasType(Type.CONTACT_PENDING_DELETE);
assertNoBillingEvents(); assertNoBillingEvents();
assertLastHistoryContainsResource(deletedContact); assertLastHistoryContainsResource(deletedContact);
} }
@TestOfyAndSql @TestSqlOnly
void testSuccess_clTridNotSpecified() throws Exception { void testSuccess_sql() throws Exception {
persistActiveContact(getUniqueIdFromCommand());
clock.advanceOneMilli();
assertTransactionalFlow(true);
runFlowAssertResponse(loadFile("contact_delete_response.xml"));
assertThat(reloadResourceByForeignKey()).isNull();
assertAboutContacts()
.that(reloadResourceByForeignKey(clock.nowUtc().minusMillis(1)))
.isNotActiveAt(clock.nowUtc())
.and()
.hasNullLocalizedPostalInfo()
.and()
.hasNullInternationalizedPostalInfo()
.and()
.hasNullEmailAddress()
.and()
.hasNullVoiceNumber()
.and()
.hasNullFaxNumber()
.and()
.hasOnlyOneHistoryEntryWhich()
.hasType(Type.CONTACT_DELETE);
assertNoTasksEnqueued(QUEUE_ASYNC_DELETE);
assertNoBillingEvents();
}
@TestSqlOnly
void testSuccess_pendingTransfer_sql() throws Exception {
DateTime transferRequestTime = clock.nowUtc().minusDays(3);
TransferData oldTransferData =
persistContactWithPendingTransfer(
persistActiveContact(getUniqueIdFromCommand()),
transferRequestTime,
transferRequestTime.plus(Registry.DEFAULT_TRANSFER_GRACE_PERIOD),
clock.nowUtc())
.getTransferData();
clock.advanceOneMilli();
assertTransactionalFlow(true);
runFlowAssertResponse(loadFile("contact_delete_response.xml"));
assertThat(reloadResourceByForeignKey()).isNull();
ContactResource softDeletedContact = reloadResourceByForeignKey(clock.nowUtc().minusMillis(1));
assertAboutContacts()
.that(softDeletedContact)
.isNotActiveAt(clock.nowUtc())
.and()
.hasNullLocalizedPostalInfo()
.and()
.hasNullInternationalizedPostalInfo()
.and()
.hasNullEmailAddress()
.and()
.hasNullVoiceNumber()
.and()
.hasNullFaxNumber()
.and()
.hasOneHistoryEntryEachOfTypes(Type.CONTACT_DELETE, Type.CONTACT_TRANSFER_REQUEST);
assertThat(softDeletedContact.getTransferData())
.isEqualTo(
oldTransferData
.copyConstantFieldsToBuilder()
.setTransferStatus(TransferStatus.SERVER_CANCELLED)
.setPendingTransferExpirationTime(softDeletedContact.getDeletionTime())
.build());
PollMessage gainingPollMessage =
Iterables.getOnlyElement(getPollMessages("NewRegistrar", clock.nowUtc()));
assertThat(gainingPollMessage.getEventTime()).isEqualTo(clock.nowUtc());
assertThat(
gainingPollMessage.getResponseData().stream()
.filter(TransferResponse.class::isInstance)
.map(TransferResponse.class::cast)
.collect(onlyElement())
.getTransferStatus())
.isEqualTo(TransferStatus.SERVER_CANCELLED);
PendingActionNotificationResponse panData =
gainingPollMessage.getResponseData().stream()
.filter(PendingActionNotificationResponse.class::isInstance)
.map(PendingActionNotificationResponse.class::cast)
.collect(onlyElement());
assertThat(panData.getTrid())
.isEqualTo(Trid.create("transferClient-trid", "transferServer-trid"));
assertThat(panData.getActionResult()).isFalse();
assertNoTasksEnqueued(QUEUE_ASYNC_DELETE);
assertNoBillingEvents();
}
@TestOfyOnly
void testSuccess_clTridNotSpecified_ofy() throws Exception {
setEppInput("contact_delete_no_cltrid.xml"); setEppInput("contact_delete_no_cltrid.xml");
persistActiveContact(getUniqueIdFromCommand()); persistActiveContact(getUniqueIdFromCommand());
clock.advanceOneMilli(); clock.advanceOneMilli();
assertTransactionalFlow(true); assertTransactionalFlow(true);
runFlowAssertResponse(loadFile("contact_delete_response_no_cltrid.xml")); runFlowAssertResponse(loadFile("contact_delete_response_no_cltrid_pending.xml"));
ContactResource deletedContact = reloadResourceByForeignKey(); ContactResource deletedContact = reloadResourceByForeignKey();
assertAboutContacts().that(deletedContact).hasStatusValue(StatusValue.PENDING_DELETE); assertAboutContacts().that(deletedContact).hasStatusValue(StatusValue.PENDING_DELETE);
assertAsyncDeletionTaskEnqueued( assertAsyncDeletionTaskEnqueued(
@ -95,7 +203,35 @@ class ContactDeleteFlowTest extends ResourceFlowTestCase<ContactDeleteFlow, Cont
assertAboutContacts() assertAboutContacts()
.that(deletedContact) .that(deletedContact)
.hasOnlyOneHistoryEntryWhich() .hasOnlyOneHistoryEntryWhich()
.hasType(HistoryEntry.Type.CONTACT_PENDING_DELETE); .hasType(CONTACT_PENDING_DELETE);
assertNoBillingEvents();
}
@TestSqlOnly
void testSuccess_clTridNotSpecified_sql() throws Exception {
setEppInput("contact_delete_no_cltrid.xml");
persistActiveContact(getUniqueIdFromCommand());
clock.advanceOneMilli();
assertTransactionalFlow(true);
runFlowAssertResponse(loadFile("contact_delete_response_no_cltrid.xml"));
assertThat(reloadResourceByForeignKey()).isNull();
assertAboutContacts()
.that(reloadResourceByForeignKey(clock.nowUtc().minusMillis(1)))
.isNotActiveAt(clock.nowUtc())
.and()
.hasNullLocalizedPostalInfo()
.and()
.hasNullInternationalizedPostalInfo()
.and()
.hasNullEmailAddress()
.and()
.hasNullVoiceNumber()
.and()
.hasNullFaxNumber()
.and()
.hasOnlyOneHistoryEntryWhich()
.hasType(Type.CONTACT_DELETE);
assertNoTasksEnqueued(QUEUE_ASYNC_DELETE);
assertNoBillingEvents(); assertNoBillingEvents();
} }
@ -137,7 +273,8 @@ class ContactDeleteFlowTest extends ResourceFlowTestCase<ContactDeleteFlow, Cont
private void doFailingStatusTest(StatusValue statusValue, Class<? extends EppException> exception) private void doFailingStatusTest(StatusValue statusValue, Class<? extends EppException> exception)
throws Exception { throws Exception {
persistResource( persistResource(
newContactResource(getUniqueIdFromCommand()).asBuilder() newContactResource(getUniqueIdFromCommand())
.asBuilder()
.setStatusValues(ImmutableSet.of(statusValue)) .setStatusValues(ImmutableSet.of(statusValue))
.build()); .build());
EppException thrown = assertThrows(exception, this::runFlow); EppException thrown = assertThrows(exception, this::runFlow);
@ -153,13 +290,13 @@ class ContactDeleteFlowTest extends ResourceFlowTestCase<ContactDeleteFlow, Cont
assertAboutEppExceptions().that(thrown).marshalsToXml(); assertAboutEppExceptions().that(thrown).marshalsToXml();
} }
@TestOfyAndSql @TestOfyOnly
void testSuccess_superuserUnauthorizedClient() throws Exception { void testSuccess_superuserUnauthorizedClient_ofy() throws Exception {
sessionMetadata.setClientId("NewRegistrar"); sessionMetadata.setClientId("NewRegistrar");
persistActiveContact(getUniqueIdFromCommand()); persistActiveContact(getUniqueIdFromCommand());
clock.advanceOneMilli(); clock.advanceOneMilli();
runFlowAssertResponse( runFlowAssertResponse(
CommitMode.LIVE, UserPrivileges.SUPERUSER, loadFile("contact_delete_response.xml")); CommitMode.LIVE, UserPrivileges.SUPERUSER, loadFile("contact_delete_response_pending.xml"));
ContactResource deletedContact = reloadResourceByForeignKey(); ContactResource deletedContact = reloadResourceByForeignKey();
assertAboutContacts().that(deletedContact).hasStatusValue(StatusValue.PENDING_DELETE); assertAboutContacts().that(deletedContact).hasStatusValue(StatusValue.PENDING_DELETE);
assertAsyncDeletionTaskEnqueued( assertAsyncDeletionTaskEnqueued(
@ -167,15 +304,42 @@ class ContactDeleteFlowTest extends ResourceFlowTestCase<ContactDeleteFlow, Cont
assertAboutContacts() assertAboutContacts()
.that(deletedContact) .that(deletedContact)
.hasOnlyOneHistoryEntryWhich() .hasOnlyOneHistoryEntryWhich()
.hasType(HistoryEntry.Type.CONTACT_PENDING_DELETE); .hasType(CONTACT_PENDING_DELETE);
assertNoBillingEvents();
}
@TestSqlOnly
void testSuccess_superuserUnauthorizedClient_sql() throws Exception {
sessionMetadata.setClientId("NewRegistrar");
persistActiveContact(getUniqueIdFromCommand());
clock.advanceOneMilli();
runFlowAssertResponse(
CommitMode.LIVE, UserPrivileges.SUPERUSER, loadFile("contact_delete_response.xml"));
assertThat(reloadResourceByForeignKey()).isNull();
assertAboutContacts()
.that(reloadResourceByForeignKey(clock.nowUtc().minusMillis(1)))
.isNotActiveAt(clock.nowUtc())
.and()
.hasNullLocalizedPostalInfo()
.and()
.hasNullInternationalizedPostalInfo()
.and()
.hasNullEmailAddress()
.and()
.hasNullVoiceNumber()
.and()
.hasNullFaxNumber()
.and()
.hasOnlyOneHistoryEntryWhich()
.hasType(Type.CONTACT_DELETE);
assertNoTasksEnqueued(QUEUE_ASYNC_DELETE);
assertNoBillingEvents(); assertNoBillingEvents();
} }
@TestOfyAndSql @TestOfyAndSql
void testFailure_failfastWhenLinkedToDomain() throws Exception { void testFailure_failfastWhenLinkedToDomain() throws Exception {
createTld("tld"); createTld("tld");
persistResource( persistResource(newDomainBase("example.tld", persistActiveContact(getUniqueIdFromCommand())));
newDomainBase("example.tld", persistActiveContact(getUniqueIdFromCommand())));
EppException thrown = assertThrows(ResourceToDeleteIsReferencedException.class, this::runFlow); EppException thrown = assertThrows(ResourceToDeleteIsReferencedException.class, this::runFlow);
assertAboutEppExceptions().that(thrown).marshalsToXml(); assertAboutEppExceptions().that(thrown).marshalsToXml();
} }
@ -183,8 +347,7 @@ class ContactDeleteFlowTest extends ResourceFlowTestCase<ContactDeleteFlow, Cont
@TestOfyAndSql @TestOfyAndSql
void testFailure_failfastWhenLinkedToApplication() throws Exception { void testFailure_failfastWhenLinkedToApplication() throws Exception {
createTld("tld"); createTld("tld");
persistResource( persistResource(newDomainBase("example.tld", persistActiveContact(getUniqueIdFromCommand())));
newDomainBase("example.tld", persistActiveContact(getUniqueIdFromCommand())));
EppException thrown = assertThrows(ResourceToDeleteIsReferencedException.class, this::runFlow); EppException thrown = assertThrows(ResourceToDeleteIsReferencedException.class, this::runFlow);
assertAboutEppExceptions().that(thrown).marshalsToXml(); assertAboutEppExceptions().that(thrown).marshalsToXml();
} }

View file

@ -1,7 +1,7 @@
<epp xmlns="urn:ietf:params:xml:ns:epp-1.0"> <epp xmlns="urn:ietf:params:xml:ns:epp-1.0">
<response> <response>
<result code="1001"> <result code="1000">
<msg>Command completed successfully; action pending</msg> <msg>Command completed successfully</msg>
</result> </result>
<trID> <trID>
<clTRID>ABC-12345</clTRID> <clTRID>ABC-12345</clTRID>

View file

@ -1,7 +1,7 @@
<epp xmlns="urn:ietf:params:xml:ns:epp-1.0"> <epp xmlns="urn:ietf:params:xml:ns:epp-1.0">
<response> <response>
<result code="1001"> <result code="1000">
<msg>Command completed successfully; action pending</msg> <msg>Command completed successfully</msg>
</result> </result>
<trID> <trID>
<svTRID>server-trid</svTRID> <svTRID>server-trid</svTRID>

View file

@ -0,0 +1,10 @@
<epp xmlns="urn:ietf:params:xml:ns:epp-1.0">
<response>
<result code="1001">
<msg>Command completed successfully; action pending</msg>
</result>
<trID>
<svTRID>server-trid</svTRID>
</trID>
</response>
</epp>

View file

@ -0,0 +1,11 @@
<epp xmlns="urn:ietf:params:xml:ns:epp-1.0">
<response>
<result code="1001">
<msg>Command completed successfully; action pending</msg>
</result>
<trID>
<clTRID>ABC-12345</clTRID>
<svTRID>server-trid</svTRID>
</trID>
</response>
</epp>

View file

@ -1,7 +1,7 @@
<epp xmlns="urn:ietf:params:xml:ns:epp-1.0"> <epp xmlns="urn:ietf:params:xml:ns:epp-1.0">
<response> <response>
<result code="1001"> <result code="1000">
<msg>Command completed successfully; action pending</msg> <msg>Command completed successfully</msg>
</result> </result>
<trID> <trID>
<clTRID>ABC-12345</clTRID> <clTRID>ABC-12345</clTRID>

View file

@ -0,0 +1,11 @@
<epp xmlns="urn:ietf:params:xml:ns:epp-1.0">
<response>
<result code="1001">
<msg>Command completed successfully; action pending</msg>
</result>
<trID>
<clTRID>ABC-12345</clTRID>
<svTRID>server-trid</svTRID>
</trID>
</response>
</epp>

View file

@ -0,0 +1,11 @@
<epp xmlns="urn:ietf:params:xml:ns:epp-1.0">
<response>
<result code="1001">
<msg>Command completed successfully; action pending</msg>
</result>
<trID>
<clTRID>ABC-12345</clTRID>
<svTRID>server-trid</svTRID>
</trID>
</response>
</epp>