diff --git a/core/build.gradle b/core/build.gradle index 60ffbba89..53161af4f 100644 --- a/core/build.gradle +++ b/core/build.gradle @@ -37,7 +37,6 @@ def jsDir = "${project.projectDir}/src/main/javascript" // TODO(weiminyu): identify cause and fix offending tests. def outcastTestPatterns = [ // Problem seems to lie with AppEngine TaskQueue for test. - "google/registry/batch/DeleteContactsAndHostsActionTest.*", "google/registry/batch/RefreshDnsOnHostRenameActionTest.*", "google/registry/flows/CheckApiActionTest.*", "google/registry/flows/EppLifecycleHostTest.*", @@ -714,10 +713,6 @@ createToolTask( createToolTask( 'jpaDemoPipeline', 'google.registry.beam.common.JpaDemoPipeline') -createToolTask( - 'createSyntheticHistoryEntries', - 'google.registry.tools.javascrap.CreateSyntheticHistoryEntriesPipeline') - // Caller must provide projectId, GCP region, runner, and the kinds to delete // (comma-separated kind names or '*' for all). E.g.: // nom_build :core:bulkDeleteDatastore --args="--project=domain-registry-crash \ diff --git a/core/src/main/java/google/registry/batch/DeleteContactsAndHostsAction.java b/core/src/main/java/google/registry/batch/DeleteContactsAndHostsAction.java deleted file mode 100644 index 1496b29c2..000000000 --- a/core/src/main/java/google/registry/batch/DeleteContactsAndHostsAction.java +++ /dev/null @@ -1,621 +0,0 @@ -// Copyright 2017 The Nomulus Authors. All Rights Reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package google.registry.batch; - -import static com.google.appengine.api.taskqueue.QueueConstants.maxLeaseCount; -import static com.google.appengine.api.taskqueue.QueueFactory.getQueue; -import static com.google.common.base.Preconditions.checkNotNull; -import static com.google.common.base.Preconditions.checkState; -import static com.google.common.collect.ImmutableList.toImmutableList; -import static com.google.common.math.IntMath.divide; -import static com.googlecode.objectify.Key.getKind; -import static google.registry.batch.AsyncTaskEnqueuer.PARAM_CLIENT_TRANSACTION_ID; -import static google.registry.batch.AsyncTaskEnqueuer.PARAM_IS_SUPERUSER; -import static google.registry.batch.AsyncTaskEnqueuer.PARAM_REQUESTED_TIME; -import static google.registry.batch.AsyncTaskEnqueuer.PARAM_REQUESTING_CLIENT_ID; -import static google.registry.batch.AsyncTaskEnqueuer.PARAM_RESOURCE_KEY; -import static google.registry.batch.AsyncTaskEnqueuer.PARAM_SERVER_TRANSACTION_ID; -import static google.registry.batch.AsyncTaskEnqueuer.QUEUE_ASYNC_DELETE; -import static google.registry.model.EppResourceUtils.isActive; -import static google.registry.model.EppResourceUtils.isDeleted; -import static google.registry.model.ResourceTransferUtils.denyPendingTransfer; -import static google.registry.model.ResourceTransferUtils.handlePendingTransferOnDelete; -import static google.registry.model.ResourceTransferUtils.updateForeignKeyIndexDeletionTime; -import static google.registry.model.eppcommon.StatusValue.PENDING_DELETE; -import static google.registry.model.ofy.ObjectifyService.auditedOfy; -import static google.registry.model.reporting.HistoryEntry.Type.CONTACT_DELETE; -import static google.registry.model.reporting.HistoryEntry.Type.CONTACT_DELETE_FAILURE; -import static google.registry.model.reporting.HistoryEntry.Type.HOST_DELETE; -import static google.registry.model.reporting.HistoryEntry.Type.HOST_DELETE_FAILURE; -import static google.registry.model.transfer.TransferStatus.SERVER_CANCELLED; -import static google.registry.persistence.transaction.TransactionManagerFactory.tm; -import static java.math.RoundingMode.CEILING; -import static java.util.concurrent.TimeUnit.DAYS; -import static java.util.concurrent.TimeUnit.SECONDS; -import static java.util.logging.Level.INFO; -import static java.util.logging.Level.SEVERE; -import static org.joda.time.Duration.standardHours; - -import com.google.appengine.api.taskqueue.LeaseOptions; -import com.google.appengine.api.taskqueue.Queue; -import com.google.appengine.api.taskqueue.TaskHandle; -import com.google.appengine.api.taskqueue.TransientFailureException; -import com.google.appengine.tools.mapreduce.Mapper; -import com.google.appengine.tools.mapreduce.Reducer; -import com.google.appengine.tools.mapreduce.ReducerInput; -import com.google.auto.value.AutoValue; -import com.google.common.collect.HashMultiset; -import com.google.common.collect.ImmutableList; -import com.google.common.collect.ImmutableMap; -import com.google.common.collect.Iterators; -import com.google.common.collect.Multiset; -import com.google.common.flogger.FluentLogger; -import com.googlecode.objectify.Key; -import google.registry.batch.AsyncTaskMetrics.OperationResult; -import google.registry.batch.AsyncTaskMetrics.OperationType; -import google.registry.batch.DeleteContactsAndHostsAction.DeletionResult.Type; -import google.registry.dns.DnsQueue; -import google.registry.mapreduce.MapreduceRunner; -import google.registry.mapreduce.UnlockerOutput; -import google.registry.mapreduce.inputs.EppResourceInputs; -import google.registry.mapreduce.inputs.NullInput; -import google.registry.model.EppResource; -import google.registry.model.ImmutableObject; -import google.registry.model.annotations.DeleteAfterMigration; -import google.registry.model.annotations.ExternalMessagingName; -import google.registry.model.contact.ContactResource; -import google.registry.model.domain.DomainBase; -import google.registry.model.eppcommon.StatusValue; -import google.registry.model.eppcommon.Trid; -import google.registry.model.eppoutput.EppResponse.ResponseData; -import google.registry.model.host.HostResource; -import google.registry.model.poll.PendingActionNotificationResponse.ContactPendingActionNotificationResponse; -import google.registry.model.poll.PendingActionNotificationResponse.HostPendingActionNotificationResponse; -import google.registry.model.poll.PollMessage; -import google.registry.model.reporting.HistoryEntry; -import google.registry.model.server.Lock; -import google.registry.persistence.VKey; -import google.registry.request.Action; -import google.registry.request.Response; -import google.registry.request.auth.Auth; -import google.registry.util.Clock; -import google.registry.util.NonFinalForTesting; -import google.registry.util.RequestStatusChecker; -import google.registry.util.Retrier; -import google.registry.util.SystemClock; -import java.io.Serializable; -import java.util.ArrayList; -import java.util.List; -import java.util.Optional; -import java.util.logging.Level; -import javax.annotation.Nullable; -import javax.inject.Inject; -import javax.inject.Named; -import org.joda.time.DateTime; -import org.joda.time.Duration; - -/** - * 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. - */ -@Deprecated -@Action( - service = Action.Service.BACKEND, - path = "/_dr/task/deleteContactsAndHosts", - auth = Auth.AUTH_INTERNAL_OR_ADMIN) -@DeleteAfterMigration -public class DeleteContactsAndHostsAction implements Runnable { - - static final String KIND_CONTACT = getKind(ContactResource.class); - static final String KIND_HOST = getKind(HostResource.class); - - private static final Duration LEASE_LENGTH = standardHours(4); - private static final FluentLogger logger = FluentLogger.forEnclosingClass(); - private static final int MAX_REDUCE_SHARDS = 50; - private static final int DELETES_PER_SHARD = 5; - - @Inject AsyncTaskMetrics asyncTaskMetrics; - @Inject Clock clock; - @Inject MapreduceRunner mrRunner; - @Inject @Named(QUEUE_ASYNC_DELETE) Queue queue; - @Inject RequestStatusChecker requestStatusChecker; - @Inject Response response; - @Inject Retrier retrier; - @Inject DeleteContactsAndHostsAction() {} - - @Override - public void run() { - // Check if the lock can be acquired, and if not, a previous run of this mapreduce is still - // executing, so return early. - Optional lock = - Lock.acquire( - DeleteContactsAndHostsAction.class.getSimpleName(), - null, - LEASE_LENGTH, - requestStatusChecker, - false); - if (!lock.isPresent()) { - logRespondAndUnlock(INFO, "Can't acquire lock; aborting.", lock); - return; - } - - // Lease the async tasks to process. - LeaseOptions options = - LeaseOptions.Builder.withCountLimit(maxLeaseCount()) - .leasePeriod(LEASE_LENGTH.getStandardSeconds(), SECONDS); - List tasks = queue.leaseTasks(options); - asyncTaskMetrics.recordContactHostDeletionBatchSize(tasks.size()); - - // Check if there are no tasks to process, and if so, return early. - if (tasks.isEmpty()) { - logRespondAndUnlock(INFO, "No contact/host deletion tasks in pull queue; finishing.", lock); - return; - } - - Multiset kindCounts = HashMultiset.create(2); - ImmutableList.Builder builder = new ImmutableList.Builder<>(); - ImmutableList.Builder> resourceKeys = new ImmutableList.Builder<>(); - final List requestsToDelete = new ArrayList<>(); - for (TaskHandle task : tasks) { - try { - DeletionRequest deletionRequest = DeletionRequest.createFromTask(task, clock.nowUtc()); - if (deletionRequest.isDeletionAllowed()) { - builder.add(deletionRequest); - resourceKeys.add(deletionRequest.key()); - kindCounts.add(deletionRequest.key().getKind()); - } else { - requestsToDelete.add(deletionRequest); - } - } catch (Exception e) { - logger.atSevere().withCause(e).log( - "Could not parse async deletion request, delaying task for a day: %s", task); - // Grab the lease for a whole day, so that it won't continue throwing errors every five - // minutes. - queue.modifyTaskLease(task, 1L, DAYS); - } - } - deleteStaleTasksWithRetry(requestsToDelete); - ImmutableList deletionRequests = builder.build(); - if (deletionRequests.isEmpty()) { - logRespondAndUnlock( - INFO, "No async deletions to process because all were already handled.", lock); - } else { - logger.atInfo().log( - "Processing asynchronous deletion of %d contacts and %d hosts: %s", - kindCounts.count(KIND_CONTACT), kindCounts.count(KIND_HOST), resourceKeys.build()); - runMapreduce(deletionRequests, lock); - } - } - - /** - * Deletes a list of tasks associated with deletion requests from the async delete queue using a - * retrier. - */ - private void deleteStaleTasksWithRetry(final List deletionRequests) { - if (deletionRequests.isEmpty()) { - return; - } - final List tasks = - deletionRequests.stream().map(DeletionRequest::task).collect(toImmutableList()); - retrier.callWithRetry(() -> queue.deleteTask(tasks), TransientFailureException.class); - deletionRequests.forEach( - deletionRequest -> - asyncTaskMetrics.recordAsyncFlowResult( - deletionRequest.getMetricOperationType(), - OperationResult.STALE, - deletionRequest.requestedTime())); - } - - private void runMapreduce(ImmutableList deletionRequests, Optional lock) { - try { - int numReducers = - Math.min(MAX_REDUCE_SHARDS, divide(deletionRequests.size(), DELETES_PER_SHARD, CEILING)); - mrRunner - .setJobName("Check for EPP resource references and then delete") - .setModuleName("backend") - .setDefaultReduceShards(numReducers) - .runMapreduce( - new DeleteContactsAndHostsMapper(deletionRequests), - new DeleteEppResourceReducer(), - ImmutableList.of( - // Add an extra shard that maps over a null domain. See the mapper code for why. - new NullInput<>(), EppResourceInputs.createEntityInput(DomainBase.class)), - new UnlockerOutput(lock.get())) - .sendLinkToMapreduceConsole(response); - } catch (Throwable t) { - logRespondAndUnlock(SEVERE, "Error starting mapreduce to delete contacts/hosts.", lock); - } - } - - private void logRespondAndUnlock(Level level, String message, Optional lock) { - logger.at(level).log(message); - response.setPayload(message); - lock.ifPresent(Lock::release); - } - - /** - * A mapper that iterates over all {@link DomainBase} entities. - * - *

It emits the target key and {@code true} for domains referencing the target resource. For - * the special input of {@code null} it emits the target key and {@code false}. - */ - public static class DeleteContactsAndHostsMapper - extends Mapper { - - private static final long serialVersionUID = -253652818502690537L; - - private final ImmutableList deletionRequests; - - DeleteContactsAndHostsMapper(ImmutableList resourcesToDelete) { - this.deletionRequests = resourcesToDelete; - } - - @Override - public void map(DomainBase domain) { - for (DeletionRequest deletionRequest : deletionRequests) { - if (domain == null) { - // The reducer only runs if at least one value is emitted. We add a null input to the - // mapreduce and emit one 'false' for each deletion request so that the reducer always - // runs for each requested deletion (so that it can finish up tasks if nothing else). - emit(deletionRequest, false); - } else if (isActive(domain, deletionRequest.lastUpdateTime()) - && isLinked(domain, deletionRequest.key())) { - emit(deletionRequest, true); - getContext() - .incrementCounter( - String.format("active Domain-%s links found", deletionRequest.key().getKind())); - } - } - if (domain != null) { - getContext().incrementCounter("domains processed"); - } - } - - /** Determine whether the target resource is a linked resource on the domain. */ - private boolean isLinked(DomainBase domain, Key resourceKey) { - if (resourceKey.getKind().equals(KIND_CONTACT)) { - return domain - .getReferencedContacts() - .contains(VKey.from((Key) resourceKey)); - } else if (resourceKey.getKind().equals(KIND_HOST)) { - return domain.getNameservers().contains(VKey.from((Key) resourceKey)); - } else { - throw new IllegalStateException("EPP resource key of unknown type: " + resourceKey); - } - } - } - - /** - * A reducer that checks if the EPP resource to be deleted is referenced anywhere, and then - * deletes it if not and unmarks it for deletion if so. - */ - public static class DeleteEppResourceReducer - extends Reducer { - - private static final long serialVersionUID = 6569363449285506326L; - private static final DnsQueue dnsQueue = DnsQueue.create(); - - @NonFinalForTesting - private static AsyncTaskMetrics asyncTaskMetrics = new AsyncTaskMetrics(new SystemClock()); - - @Override - public void reduce(final DeletionRequest deletionRequest, ReducerInput values) { - final boolean hasNoActiveReferences = !Iterators.contains(values, true); - logger.atInfo().log("Processing async deletion request for %s.", deletionRequest.key()); - DeletionResult result = - tm() - .transactNew( - () -> { - DeletionResult deletionResult = - attemptToDeleteResource(deletionRequest, hasNoActiveReferences); - getQueue(QUEUE_ASYNC_DELETE).deleteTask(deletionRequest.task()); - return deletionResult; - }); - asyncTaskMetrics.recordAsyncFlowResult( - deletionRequest.getMetricOperationType(), - result.getMetricOperationResult(), - deletionRequest.requestedTime()); - String resourceNamePlural = deletionRequest.key().getKind() + "s"; - getContext().incrementCounter(result.type().renderCounterText(resourceNamePlural)); - logger.atInfo().log( - "Result of async deletion for resource %s: %s", - deletionRequest.key(), result.pollMessageText()); - } - - private DeletionResult attemptToDeleteResource( - DeletionRequest deletionRequest, boolean hasNoActiveReferences) { - DateTime now = tm().getTransactionTime(); - EppResource resource = - auditedOfy().load().key(deletionRequest.key()).now().cloneProjectedAtTime(now); - // Double-check transactionally that the resource is still active and in PENDING_DELETE. - if (!doesResourceStateAllowDeletion(resource, now)) { - return DeletionResult.create(Type.ERRORED, ""); - } - // Contacts and external hosts have a direct client id. For subordinate hosts it needs to be - // read off of the superordinate domain. - String resourceRegistrarId = resource.getPersistedCurrentSponsorRegistrarId(); - if (resource instanceof HostResource && ((HostResource) resource).isSubordinate()) { - resourceRegistrarId = - tm().loadByKey(((HostResource) resource).getSuperordinateDomain()) - .cloneProjectedAtTime(now) - .getCurrentSponsorRegistrarId(); - } - boolean requestedByCurrentOwner = - resourceRegistrarId.equals(deletionRequest.requestingClientId()); - - boolean deleteAllowed = - hasNoActiveReferences && (requestedByCurrentOwner || deletionRequest.isSuperuser()); - - String resourceTypeName = - resource.getClass().getAnnotation(ExternalMessagingName.class).value(); - String pollMessageText = - deleteAllowed - ? String.format("Deleted %s %s.", resourceTypeName, resource.getForeignKey()) - : String.format( - "Can't delete %s %s because %s.", - resourceTypeName, - resource.getForeignKey(), - requestedByCurrentOwner - ? "it is referenced by a domain" - : "it was transferred prior to deletion"); - - HistoryEntry historyEntry = - HistoryEntry.createBuilderForResource(resource) - .setRegistrarId(deletionRequest.requestingClientId()) - .setModificationTime(now) - .setType(getHistoryEntryType(resource, deleteAllowed)) - .build(); - - PollMessage.OneTime pollMessage = - new PollMessage.OneTime.Builder() - .setRegistrarId(deletionRequest.requestingClientId()) - .setMsg(pollMessageText) - .setParent(historyEntry) - .setEventTime(now) - .setResponseData( - getPollMessageResponseData(deletionRequest, resource, deleteAllowed, now)) - .build(); - - EppResource resourceToSave; - if (deleteAllowed) { - EppResource.Builder resourceToSaveBuilder; - if (resource instanceof ContactResource) { - ContactResource contact = (ContactResource) resource; - // Handle pending transfers on contact deletion. - if (contact.getStatusValues().contains(StatusValue.PENDING_TRANSFER)) { - contact = - denyPendingTransfer( - contact, SERVER_CANCELLED, now, deletionRequest.requestingClientId()); - } - // Wipe out PII on contact deletion. - resourceToSaveBuilder = contact.asBuilder().wipeOut(); - } else { - resourceToSaveBuilder = resource.asBuilder(); - } - resourceToSave = resourceToSaveBuilder.setDeletionTime(now).setStatusValues(null).build(); - performDeleteTasks(resource, resourceToSave, now, historyEntry); - updateForeignKeyIndexDeletionTime(resourceToSave); - } else { - resourceToSave = resource.asBuilder().removeStatusValue(PENDING_DELETE).build(); - } - auditedOfy() - .save() - .entities(resourceToSave, historyEntry.asHistoryEntry(), pollMessage); - return DeletionResult.create( - deleteAllowed ? Type.DELETED : Type.NOT_DELETED, pollMessageText); - } - - private static ImmutableList getPollMessageResponseData( - DeletionRequest deletionRequest, - EppResource resource, - boolean deleteAllowed, - DateTime now) { - @Nullable String clientTransactionId = deletionRequest.clientTransactionId(); - String serverTransactionId = deletionRequest.serverTransactionId(); - Trid trid = Trid.create(clientTransactionId, serverTransactionId); - if (resource instanceof HostResource) { - return ImmutableList.of( - HostPendingActionNotificationResponse.create( - ((HostResource) resource).getHostName(), deleteAllowed, trid, now)); - } else if (resource instanceof ContactResource) { - return ImmutableList.of( - ContactPendingActionNotificationResponse.create( - ((ContactResource) resource).getContactId(), deleteAllowed, trid, now)); - } else { - throw new IllegalStateException("EPP resource of unknown type " + Key.create(resource)); - } - } - - /** - * Determine the proper history entry type for the delete operation, as a function of - * whether or not the delete was successful. - */ - private HistoryEntry.Type getHistoryEntryType(EppResource resource, boolean successfulDelete) { - if (resource instanceof ContactResource) { - return successfulDelete ? CONTACT_DELETE : CONTACT_DELETE_FAILURE; - } else if (resource instanceof HostResource) { - return successfulDelete ? HOST_DELETE : HOST_DELETE_FAILURE; - } else { - throw new IllegalStateException("EPP resource of unknown type: " + Key.create(resource)); - } - } - - /** Perform any type-specific tasks on the resource to be deleted (and/or its dependencies). */ - private void performDeleteTasks( - EppResource existingResource, - EppResource deletedResource, - DateTime deletionTime, - HistoryEntry historyEntryForDelete) { - if (existingResource instanceof ContactResource) { - handlePendingTransferOnDelete( - (ContactResource) existingResource, - (ContactResource) deletedResource, - deletionTime, - historyEntryForDelete); - } else if (existingResource instanceof HostResource) { - HostResource host = (HostResource) existingResource; - if (host.isSubordinate()) { - dnsQueue.addHostRefreshTask(host.getHostName()); - tm().put( - tm().loadByKey(host.getSuperordinateDomain()) - .asBuilder() - .removeSubordinateHost(host.getHostName()) - .build()); - } - } else { - throw new IllegalStateException( - "EPP resource of unknown type: " + Key.create(existingResource)); - } - } - } - - /** A class that encapsulates the values of a request to delete a contact or host. */ - @AutoValue - abstract static class DeletionRequest implements Serializable { - - private static final long serialVersionUID = -4612618525760839240L; - - abstract Key key(); - abstract DateTime lastUpdateTime(); - - /** - * The client id of the registrar that requested this deletion (which might NOT be the same as - * the actual current owner of the resource). - */ - abstract String requestingClientId(); - - /** First half of TRID for the original request, split for serializability. */ - @Nullable - abstract String clientTransactionId(); - - /** Second half of TRID for the original request, split for serializability. */ - abstract String serverTransactionId(); - - abstract boolean isSuperuser(); - abstract DateTime requestedTime(); - abstract boolean isDeletionAllowed(); - abstract TaskHandle task(); - - @AutoValue.Builder - abstract static class Builder { - abstract Builder setKey(Key key); - abstract Builder setLastUpdateTime(DateTime lastUpdateTime); - abstract Builder setRequestingClientId(String requestingClientId); - abstract Builder setClientTransactionId(@Nullable String clientTransactionId); - abstract Builder setServerTransactionId(String serverTransactionId); - abstract Builder setIsSuperuser(boolean isSuperuser); - abstract Builder setRequestedTime(DateTime requestedTime); - abstract Builder setIsDeletionAllowed(boolean isDeletionAllowed); - abstract Builder setTask(TaskHandle task); - abstract DeletionRequest build(); - } - - static DeletionRequest createFromTask(TaskHandle task, DateTime now) - throws Exception { - ImmutableMap params = ImmutableMap.copyOf(task.extractParams()); - VKey resourceKey = - VKey.create( - checkNotNull(params.get(PARAM_RESOURCE_KEY), "Resource to delete not specified")); - EppResource resource = - checkNotNull( - auditedOfy().load().key(resourceKey.getOfyKey()).now(), - "Resource to delete doesn't exist"); - checkState( - resource instanceof ContactResource || resource instanceof HostResource, - "Cannot delete a %s via this action", - resource.getClass().getSimpleName()); - return new AutoValue_DeleteContactsAndHostsAction_DeletionRequest.Builder() - .setKey(resourceKey.getOfyKey()) - .setLastUpdateTime(resource.getUpdateTimestamp().getTimestamp()) - .setRequestingClientId( - checkNotNull( - params.get(PARAM_REQUESTING_CLIENT_ID), "Requesting client id not specified")) - // Note that client transaction ID is optional, in which case this sets it to null. - .setClientTransactionId(params.get(PARAM_CLIENT_TRANSACTION_ID)) - .setServerTransactionId( - checkNotNull( - params.get(PARAM_SERVER_TRANSACTION_ID), "Server transaction id not specified")) - .setIsSuperuser( - Boolean.parseBoolean( - checkNotNull(params.get(PARAM_IS_SUPERUSER), "Is superuser not specified"))) - .setRequestedTime( - DateTime.parse( - checkNotNull(params.get(PARAM_REQUESTED_TIME), "Requested time not specified"))) - .setIsDeletionAllowed(doesResourceStateAllowDeletion(resource, now)) - .setTask(task) - .build(); - } - - OperationType getMetricOperationType() { - if (key().getKind().equals(KIND_CONTACT)) { - return OperationType.CONTACT_DELETE; - } else if (key().getKind().equals(KIND_HOST)) { - return OperationType.HOST_DELETE; - } else { - throw new IllegalStateException( - String.format( - "Cannot determine async operation type for metric for resource %s", key())); - } - } - } - - /** A class that encapsulates the values resulting from attempted contact/host deletion. */ - @AutoValue - abstract static class DeletionResult { - - enum Type { - DELETED("%s deleted", OperationResult.SUCCESS), - NOT_DELETED("%s not deleted", OperationResult.FAILURE), - ERRORED("%s errored out during deletion", OperationResult.ERROR); - - private final String counterFormat; - private final OperationResult operationResult; - - Type(String counterFormat, OperationResult operationResult) { - this.counterFormat = counterFormat; - this.operationResult = operationResult; - } - - String renderCounterText(String resourceName) { - return String.format(counterFormat, resourceName); - } - } - - abstract Type type(); - abstract String pollMessageText(); - - static DeletionResult create(Type type, String pollMessageText) { - return new AutoValue_DeleteContactsAndHostsAction_DeletionResult(type, pollMessageText); - } - - OperationResult getMetricOperationResult() { - return type().operationResult; - } - } - - static boolean doesResourceStateAllowDeletion(EppResource resource, DateTime now) { - Key key = Key.create(resource); - if (isDeleted(resource, now)) { - logger.atWarning().log("Cannot asynchronously delete %s because it is already deleted.", key); - return false; - } - if (!resource.getStatusValues().contains(PENDING_DELETE)) { - logger.atWarning().log( - "Cannot asynchronously delete %s because it is not in PENDING_DELETE.", key); - return false; - } - return true; - } -} diff --git a/core/src/main/java/google/registry/env/common/backend/WEB-INF/web.xml b/core/src/main/java/google/registry/env/common/backend/WEB-INF/web.xml index a381b00ec..2c125b6ee 100644 --- a/core/src/main/java/google/registry/env/common/backend/WEB-INF/web.xml +++ b/core/src/main/java/google/registry/env/common/backend/WEB-INF/web.xml @@ -334,15 +334,6 @@ /_dr/task/resaveEntity - - - backend-servlet - /_dr/task/deleteContactsAndHosts - - backend-servlet @@ -422,12 +413,6 @@ have been in the database for a certain period of time. --> /_dr/task/wipeOutDatastore - - - backend-servlet - /_dr/task/createSyntheticHistoryEntries - - backend-servlet diff --git a/core/src/main/java/google/registry/flows/ResourceFlowUtils.java b/core/src/main/java/google/registry/flows/ResourceFlowUtils.java index 6364df7dc..4d24d21b3 100644 --- a/core/src/main/java/google/registry/flows/ResourceFlowUtils.java +++ b/core/src/main/java/google/registry/flows/ResourceFlowUtils.java @@ -15,7 +15,6 @@ package google.registry.flows; import static com.google.common.collect.Sets.intersection; -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.index.ForeignKeyIndex.loadAndGetKey; @@ -54,8 +53,6 @@ import java.util.List; import java.util.Objects; import java.util.Optional; import java.util.Set; -import java.util.function.Function; -import java.util.function.Predicate; import org.joda.time.DateTime; /** Static utility functions for resource flows. */ @@ -63,12 +60,6 @@ public final class ResourceFlowUtils { private ResourceFlowUtils() {} - /** - * In {@link #checkLinkedDomains(String, DateTime, Class, Function)}, check this (arbitrary) - * number of query results. - */ - private static final int FAILFAST_CHECK_COUNT = 5; - /** Check that the given registrarId corresponds to the owner of given resource. */ public static void verifyResourceOwnership(String myRegistrarId, EppResource resource) throws EppException { @@ -85,46 +76,18 @@ public final class ResourceFlowUtils { * consistent, so we only check a few domains to fail fast. */ public static void checkLinkedDomains( - final String targetId, - final DateTime now, - final Class resourceClass, - final Function> getPotentialReferences) - throws EppException { + final String targetId, final DateTime now, final Class resourceClass) throws EppException { EppException failfastException = - tm().isOfy() - ? tm().doTransactionless( - () -> { - final ForeignKeyIndex fki = - ForeignKeyIndex.load(resourceClass, targetId, now); - if (fki == null) { - return new ResourceDoesNotExistException(resourceClass, targetId); - } - // Query for the first few linked domains, and if found, actually load them. - // The 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 actual reference then we can reliably fail. If we don't find any, - // we can't trust the query and need to do the full mapreduce. - Iterable> keys = - getLinkedDomainKeys(fki.getResourceKey(), now, FAILFAST_CHECK_COUNT); - - VKey resourceVKey = fki.getResourceKey(); - Predicate predicate = - domain -> getPotentialReferences.apply(domain).contains(resourceVKey); - return tm().loadByKeys(keys).values().stream().anyMatch(predicate) - ? new ResourceToDeleteIsReferencedException() - : null; - }) - : tm().transact( - () -> { - final ForeignKeyIndex fki = - ForeignKeyIndex.load(resourceClass, targetId, now); - if (fki == null) { - return new ResourceDoesNotExistException(resourceClass, targetId); - } - return isLinked(fki.getResourceKey(), now) - ? new ResourceToDeleteIsReferencedException() - : null; - }); + tm().transact( + () -> { + final ForeignKeyIndex 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) { throw failfastException; } diff --git a/core/src/main/java/google/registry/flows/contact/ContactDeleteFlow.java b/core/src/main/java/google/registry/flows/contact/ContactDeleteFlow.java index f79146f08..f8f636277 100644 --- a/core/src/main/java/google/registry/flows/contact/ContactDeleteFlow.java +++ b/core/src/main/java/google/registry/flows/contact/ContactDeleteFlow.java @@ -23,7 +23,6 @@ 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.transfer.TransferStatus.SERVER_CANCELLED; import static google.registry.persistence.transaction.TransactionManagerFactory.assertAsyncActionsAreAllowed; import static google.registry.persistence.transaction.TransactionManagerFactory.tm; @@ -39,13 +38,11 @@ import google.registry.flows.TransactionalFlow; import google.registry.flows.annotations.ReportingSpec; import google.registry.model.contact.ContactHistory; import google.registry.model.contact.ContactResource; -import google.registry.model.domain.DomainBase; import google.registry.model.domain.metadata.MetadataExtension; import google.registry.model.eppcommon.AuthInfo; import google.registry.model.eppcommon.StatusValue; import google.registry.model.eppcommon.Trid; import google.registry.model.eppoutput.EppResponse; -import google.registry.model.eppoutput.Result.Code; import google.registry.model.reporting.HistoryEntry.Type; import google.registry.model.reporting.IcannReportingTypes.ActivityReportField; import java.util.Optional; @@ -97,41 +94,26 @@ public final class ContactDeleteFlow implements TransactionalFlow { extensionManager.validate(); assertAsyncActionsAreAllowed(); DateTime now = tm().getTransactionTime(); - checkLinkedDomains(targetId, now, ContactResource.class, DomainBase::getReferencedContacts); + checkLinkedDomains(targetId, now, ContactResource.class); ContactResource existingContact = loadAndVerifyExistence(ContactResource.class, targetId, now); verifyNoDisallowedStatuses(existingContact, DISALLOWED_STATUSES); verifyOptionalAuthInfo(authInfo, existingContact); if (!isSuperuser) { verifyResourceOwnership(registrarId, existingContact); } - Type historyEntryType; - Code resultCode; - ContactResource newContact; - if (tm().isOfy()) { - asyncTaskEnqueuer.enqueueAsyncDelete( - existingContact, tm().getTransactionTime(), registrarId, trid, isSuperuser); - newContact = existingContact.asBuilder().addStatusValue(StatusValue.PENDING_DELETE).build(); - historyEntryType = Type.CONTACT_PENDING_DELETE; - resultCode = SUCCESS_WITH_ACTION_PENDING; - } else { - // Handle pending transfers on contact deletion. - newContact = - existingContact.getStatusValues().contains(StatusValue.PENDING_TRANSFER) - ? denyPendingTransfer(existingContact, SERVER_CANCELLED, now, registrarId) - : existingContact; - // Wipe out PII on contact deletion. - newContact = - newContact.asBuilder().wipeOut().setStatusValues(null).setDeletionTime(now).build(); - historyEntryType = Type.CONTACT_DELETE; - resultCode = SUCCESS; - } + // Handle pending transfers on contact deletion. + ContactResource newContact = + existingContact.getStatusValues().contains(StatusValue.PENDING_TRANSFER) + ? denyPendingTransfer(existingContact, SERVER_CANCELLED, now, registrarId) + : existingContact; + // Wipe out PII on contact deletion. + newContact = + newContact.asBuilder().wipeOut().setStatusValues(null).setDeletionTime(now).build(); ContactHistory contactHistory = - historyBuilder.setType(historyEntryType).setContact(newContact).build(); - if (!tm().isOfy()) { - handlePendingTransferOnDelete(existingContact, newContact, now, contactHistory); - } + historyBuilder.setType(Type.CONTACT_DELETE).setContact(newContact).build(); + handlePendingTransferOnDelete(existingContact, newContact, now, contactHistory); tm().insert(contactHistory); tm().update(newContact); - return responseBuilder.setResultFromCode(resultCode).build(); + return responseBuilder.setResultFromCode(SUCCESS).build(); } } diff --git a/core/src/main/java/google/registry/flows/domain/DomainFlowUtils.java b/core/src/main/java/google/registry/flows/domain/DomainFlowUtils.java index 1e298adfd..f24ce5e0f 100644 --- a/core/src/main/java/google/registry/flows/domain/DomainFlowUtils.java +++ b/core/src/main/java/google/registry/flows/domain/DomainFlowUtils.java @@ -26,7 +26,6 @@ import static com.google.common.collect.Sets.difference; import static com.google.common.collect.Sets.intersection; import static com.google.common.collect.Sets.union; import static google.registry.model.domain.DomainBase.MAX_REGISTRATION_YEARS; -import static google.registry.model.ofy.ObjectifyService.auditedOfy; import static google.registry.model.tld.Registries.findTldForName; import static google.registry.model.tld.Registries.getTlds; import static google.registry.model.tld.Registry.TldState.GENERAL_AVAILABILITY; @@ -1159,24 +1158,14 @@ public class DomainFlowUtils { private static List findRecentHistoryEntries( DomainBase domainBase, DateTime now, Duration maxSearchPeriod) { - if (tm().isOfy()) { - return auditedOfy() - .load() - .type(HistoryEntry.class) - .ancestor(domainBase) - .filter("modificationTime >=", now.minus(maxSearchPeriod)) - .order("modificationTime") - .list(); - } else { - return jpaTm() - .query( - "FROM DomainHistory WHERE modificationTime >= :beginning AND domainRepoId = " - + ":repoId ORDER BY modificationTime ASC", - DomainHistory.class) - .setParameter("beginning", now.minus(maxSearchPeriod)) - .setParameter("repoId", domainBase.getRepoId()) - .getResultList(); - } + return jpaTm() + .query( + "FROM DomainHistory WHERE modificationTime >= :beginning AND domainRepoId = " + + ":repoId ORDER BY modificationTime ASC", + DomainHistory.class) + .setParameter("beginning", now.minus(maxSearchPeriod)) + .setParameter("repoId", domainBase.getRepoId()) + .getResultList(); } /** Resource linked to this domain does not exist. */ diff --git a/core/src/main/java/google/registry/flows/domain/DomainInfoFlow.java b/core/src/main/java/google/registry/flows/domain/DomainInfoFlow.java index 0d4d61489..0ff20743c 100644 --- a/core/src/main/java/google/registry/flows/domain/DomainInfoFlow.java +++ b/core/src/main/java/google/registry/flows/domain/DomainInfoFlow.java @@ -102,11 +102,6 @@ public final class DomainInfoFlow implements Flow { verifyOptionalAuthInfo(authInfo, domain); flowCustomLogic.afterValidation( AfterValidationParameters.newBuilder().setDomain(domain).build()); - // In ofy, refetch all referenced resources. - if (tm().isOfy()) { - tm().loadByKeys(domain.getNameservers()); - tm().loadByKeys(domain.getReferencedContacts()); - } // Registrars can only see a few fields on unauthorized domains. // This is a policy decision that is left up to us by the rfcs. DomainInfoData.Builder infoBuilder = diff --git a/core/src/main/java/google/registry/flows/host/HostDeleteFlow.java b/core/src/main/java/google/registry/flows/host/HostDeleteFlow.java index d00125cea..babdc068b 100644 --- a/core/src/main/java/google/registry/flows/host/HostDeleteFlow.java +++ b/core/src/main/java/google/registry/flows/host/HostDeleteFlow.java @@ -21,7 +21,6 @@ import static google.registry.flows.ResourceFlowUtils.verifyNoDisallowedStatuses import static google.registry.flows.ResourceFlowUtils.verifyResourceOwnership; import static google.registry.flows.host.HostFlowUtils.validateHostName; 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.persistence.transaction.TransactionManagerFactory.assertAsyncActionsAreAllowed; import static google.registry.persistence.transaction.TransactionManagerFactory.tm; @@ -36,15 +35,12 @@ import google.registry.flows.FlowModule.TargetId; import google.registry.flows.TransactionalFlow; import google.registry.flows.annotations.ReportingSpec; import google.registry.model.EppResource; -import google.registry.model.domain.DomainBase; import google.registry.model.domain.metadata.MetadataExtension; import google.registry.model.eppcommon.StatusValue; import google.registry.model.eppcommon.Trid; import google.registry.model.eppoutput.EppResponse; -import google.registry.model.eppoutput.Result; import google.registry.model.host.HostHistory; import google.registry.model.host.HostResource; -import google.registry.model.reporting.HistoryEntry; import google.registry.model.reporting.HistoryEntry.Type; import google.registry.model.reporting.IcannReportingTypes.ActivityReportField; import javax.inject.Inject; @@ -100,7 +96,7 @@ public final class HostDeleteFlow implements TransactionalFlow { assertAsyncActionsAreAllowed(); DateTime now = tm().getTransactionTime(); validateHostName(targetId); - checkLinkedDomains(targetId, now, HostResource.class, DomainBase::getNameservers); + checkLinkedDomains(targetId, now, HostResource.class); HostResource existingHost = loadAndVerifyExistence(HostResource.class, targetId, now); verifyNoDisallowedStatuses(existingHost, DISALLOWED_STATUSES); if (!isSuperuser) { @@ -112,31 +108,19 @@ public final class HostDeleteFlow implements TransactionalFlow { : existingHost; verifyResourceOwnership(registrarId, owningResource); } - HistoryEntry.Type historyEntryType; - Result.Code resultCode; - HostResource newHost; - if (tm().isOfy()) { - asyncTaskEnqueuer.enqueueAsyncDelete( - existingHost, tm().getTransactionTime(), registrarId, trid, isSuperuser); - newHost = existingHost.asBuilder().addStatusValue(StatusValue.PENDING_DELETE).build(); - historyEntryType = Type.HOST_PENDING_DELETE; - resultCode = SUCCESS_WITH_ACTION_PENDING; - } else { - newHost = existingHost.asBuilder().setStatusValues(null).setDeletionTime(now).build(); - if (existingHost.isSubordinate()) { - dnsQueue.addHostRefreshTask(existingHost.getHostName()); - tm().update( - tm().loadByKey(existingHost.getSuperordinateDomain()) - .asBuilder() - .removeSubordinateHost(existingHost.getHostName()) - .build()); - } - historyEntryType = Type.HOST_DELETE; - resultCode = SUCCESS; + HostResource newHost = + existingHost.asBuilder().setStatusValues(null).setDeletionTime(now).build(); + if (existingHost.isSubordinate()) { + dnsQueue.addHostRefreshTask(existingHost.getHostName()); + tm().update( + tm().loadByKey(existingHost.getSuperordinateDomain()) + .asBuilder() + .removeSubordinateHost(existingHost.getHostName()) + .build()); } - historyBuilder.setType(historyEntryType).setHost(newHost); + historyBuilder.setType(Type.HOST_DELETE).setHost(newHost); tm().insert(historyBuilder.build()); tm().update(newHost); - return responseBuilder.setResultFromCode(resultCode).build(); + return responseBuilder.setResultFromCode(SUCCESS).build(); } } diff --git a/core/src/main/java/google/registry/flows/poll/PollAckFlow.java b/core/src/main/java/google/registry/flows/poll/PollAckFlow.java index b797bdb70..6a2b15d65 100644 --- a/core/src/main/java/google/registry/flows/poll/PollAckFlow.java +++ b/core/src/main/java/google/registry/flows/poll/PollAckFlow.java @@ -102,19 +102,13 @@ public final class PollAckFlow implements TransactionalFlow { // This keeps track of whether we should include the current acked message in the updated // message count that's returned to the user. The only case where we do so is if an autorenew // poll message is acked, but its next event is already ready to be delivered. - boolean includeAckedMessageInCount = ackPollMessage(pollMessage); + ackPollMessage(pollMessage); // We need to return the new queue length. If this was the last message in the queue being // acked, then we return a special status code indicating that. Note that the query will // include the message being acked. int messageCount = tm().doTransactionless(() -> getPollMessageCount(registrarId, now)); - // Within the same transaction, Datastore will not reflect the updated count (potentially - // reduced by one thanks to the acked poll message). SQL will, however, so we shouldn't reduce - // the count in the SQL case. - if (!includeAckedMessageInCount && tm().isOfy()) { - messageCount--; - } if (messageCount <= 0) { return responseBuilder.setResultFromCode(SUCCESS_WITH_NO_MESSAGES).build(); } diff --git a/core/src/main/java/google/registry/flows/poll/PollFlowUtils.java b/core/src/main/java/google/registry/flows/poll/PollFlowUtils.java index 16a19a573..6ba489966 100644 --- a/core/src/main/java/google/registry/flows/poll/PollFlowUtils.java +++ b/core/src/main/java/google/registry/flows/poll/PollFlowUtils.java @@ -47,13 +47,12 @@ public final class PollFlowUtils { *

The only case where we do so is if an autorenew poll message is acked, but its next event is * already ready to be delivered. */ - public static boolean ackPollMessage(PollMessage pollMessage) { + public static void ackPollMessage(PollMessage pollMessage) { checkArgument( isBeforeOrAt(pollMessage.getEventTime(), tm().getTransactionTime()), "Cannot ACK poll message with ID %s because its event time is in the future: %s", pollMessage.getId(), pollMessage.getEventTime()); - boolean includeAckedMessageInCount = false; if (pollMessage instanceof PollMessage.OneTime) { // One-time poll messages are deleted once acked. tm().delete(pollMessage.createVKey()); @@ -68,14 +67,12 @@ public final class PollFlowUtils { // autorenew poll message has no more events to deliver and should be deleted. if (nextEventTime.isBefore(autorenewPollMessage.getAutorenewEndTime())) { tm().put(autorenewPollMessage.asBuilder().setEventTime(nextEventTime).build()); - includeAckedMessageInCount = isBeforeOrAt(nextEventTime, tm().getTransactionTime()); } else { tm().delete(autorenewPollMessage.createVKey()); } } else { throw new IllegalArgumentException("Unknown poll message type: " + pollMessage.getClass()); } - return includeAckedMessageInCount; } /** diff --git a/core/src/main/java/google/registry/model/reporting/HistoryEntryDao.java b/core/src/main/java/google/registry/model/reporting/HistoryEntryDao.java index d61f3ac78..1fcdebffc 100644 --- a/core/src/main/java/google/registry/model/reporting/HistoryEntryDao.java +++ b/core/src/main/java/google/registry/model/reporting/HistoryEntryDao.java @@ -16,9 +16,7 @@ package google.registry.model.reporting; import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.collect.ImmutableList.toImmutableList; -import static google.registry.model.ofy.ObjectifyService.auditedOfy; import static google.registry.persistence.transaction.TransactionManagerFactory.jpaTm; -import static google.registry.persistence.transaction.TransactionManagerFactory.tm; import static google.registry.util.DateTimeUtils.END_OF_TIME; import static google.registry.util.DateTimeUtils.START_OF_TIME; @@ -72,29 +70,16 @@ public class HistoryEntryDao { /** Loads all history objects in the times specified, including all types. */ public static ImmutableList loadAllHistoryObjects( DateTime afterTime, DateTime beforeTime) { - if (tm().isOfy()) { - return Streams.stream( - auditedOfy() - .load() - .type(HistoryEntry.class) - .order("modificationTime") - .filter("modificationTime >=", afterTime) - .filter("modificationTime <=", beforeTime)) - .map(HistoryEntry::toChildHistoryEntity) - .collect(toImmutableList()); - } else { - return jpaTm() - .transact( - () -> - new ImmutableList.Builder() - .addAll( - loadAllHistoryObjectsFromSql(ContactHistory.class, afterTime, beforeTime)) - .addAll( - loadAllHistoryObjectsFromSql(DomainHistory.class, afterTime, beforeTime)) - .addAll( - loadAllHistoryObjectsFromSql(HostHistory.class, afterTime, beforeTime)) - .build()); - } + return jpaTm() + .transact( + () -> + new ImmutableList.Builder() + .addAll( + loadAllHistoryObjectsFromSql(ContactHistory.class, afterTime, beforeTime)) + .addAll( + loadAllHistoryObjectsFromSql(DomainHistory.class, afterTime, beforeTime)) + .addAll(loadAllHistoryObjectsFromSql(HostHistory.class, afterTime, beforeTime)) + .build()); } /** Loads all history objects corresponding to the given {@link EppResource}. */ @@ -115,21 +100,8 @@ public class HistoryEntryDao { /** Loads all history objects in the time period specified for the given {@link EppResource}. */ public static ImmutableList loadHistoryObjectsForResource( VKey parentKey, DateTime afterTime, DateTime beforeTime) { - if (tm().isOfy()) { - return Streams.stream( - auditedOfy() - .load() - .type(HistoryEntry.class) - .ancestor(parentKey.getOfyKey()) - .order("modificationTime") - .filter("modificationTime >=", afterTime) - .filter("modificationTime <=", beforeTime)) - .map(HistoryEntry::toChildHistoryEntity) - .collect(toImmutableList()); - } else { - return jpaTm() - .transact(() -> loadHistoryObjectsForResourceFromSql(parentKey, afterTime, beforeTime)); - } + return jpaTm() + .transact(() -> loadHistoryObjectsForResourceFromSql(parentKey, afterTime, beforeTime)); } /** @@ -162,23 +134,15 @@ public class HistoryEntryDao { /** Loads all history objects from all time from the given registrars. */ public static Iterable loadHistoryObjectsByRegistrars( ImmutableCollection registrarIds) { - if (tm().isOfy()) { - return auditedOfy() - .load() - .type(HistoryEntry.class) - .filter("clientId in", registrarIds) - .order("modificationTime"); - } else { - return jpaTm() - .transact( - () -> - Streams.concat( - loadHistoryObjectFromSqlByRegistrars(ContactHistory.class, registrarIds), - loadHistoryObjectFromSqlByRegistrars(DomainHistory.class, registrarIds), - loadHistoryObjectFromSqlByRegistrars(HostHistory.class, registrarIds)) - .sorted(Comparator.comparing(HistoryEntry::getModificationTime)) - .collect(toImmutableList())); - } + return jpaTm() + .transact( + () -> + Streams.concat( + loadHistoryObjectFromSqlByRegistrars(ContactHistory.class, registrarIds), + loadHistoryObjectFromSqlByRegistrars(DomainHistory.class, registrarIds), + loadHistoryObjectFromSqlByRegistrars(HostHistory.class, registrarIds)) + .sorted(Comparator.comparing(HistoryEntry::getModificationTime)) + .collect(toImmutableList())); } private static Stream loadHistoryObjectFromSqlByRegistrars( diff --git a/core/src/main/java/google/registry/module/backend/BackendRequestComponent.java b/core/src/main/java/google/registry/module/backend/BackendRequestComponent.java index fb06d0360..5fa160d14 100644 --- a/core/src/main/java/google/registry/module/backend/BackendRequestComponent.java +++ b/core/src/main/java/google/registry/module/backend/BackendRequestComponent.java @@ -23,7 +23,6 @@ import google.registry.backup.ExportCommitLogDiffAction; import google.registry.backup.ReplayCommitLogsToSqlAction; import google.registry.backup.SyncDatastoreToSqlSnapshotAction; import google.registry.batch.BatchModule; -import google.registry.batch.DeleteContactsAndHostsAction; import google.registry.batch.DeleteExpiredDomainsAction; import google.registry.batch.DeleteLoadTestDataAction; import google.registry.batch.DeleteProberDataAction; @@ -93,7 +92,6 @@ import google.registry.tmch.TmchCrlAction; import google.registry.tmch.TmchDnlAction; import google.registry.tmch.TmchModule; import google.registry.tmch.TmchSmdrlAction; -import google.registry.tools.javascrap.CreateSyntheticHistoryEntriesAction; /** Dagger component with per-request lifetime for "backend" App Engine module. */ @RequestScope @@ -137,10 +135,6 @@ interface BackendRequestComponent { CopyDetailReportsAction copyDetailReportAction(); - CreateSyntheticHistoryEntriesAction createSyntheticHistoryEntriesAction(); - - DeleteContactsAndHostsAction deleteContactsAndHostsAction(); - DeleteExpiredDomainsAction deleteExpiredDomainsAction(); DeleteLoadTestDataAction deleteLoadTestDataAction(); diff --git a/core/src/main/java/google/registry/tools/javascrap/CreateSyntheticHistoryEntriesAction.java b/core/src/main/java/google/registry/tools/javascrap/CreateSyntheticHistoryEntriesAction.java deleted file mode 100644 index 2941a5e0c..000000000 --- a/core/src/main/java/google/registry/tools/javascrap/CreateSyntheticHistoryEntriesAction.java +++ /dev/null @@ -1,198 +0,0 @@ -// Copyright 2021 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.javascrap; - -import static google.registry.model.ofy.ObjectifyService.auditedOfy; -import static google.registry.model.reporting.HistoryEntryDao.REPO_ID_FIELD_NAMES; -import static google.registry.model.reporting.HistoryEntryDao.RESOURCE_TYPES_TO_HISTORY_TYPES; -import static google.registry.persistence.transaction.TransactionManagerFactory.jpaTm; -import static google.registry.persistence.transaction.TransactionManagerFactory.ofyTm; - -import com.google.appengine.tools.mapreduce.Mapper; -import com.google.common.base.CaseFormat; -import com.google.common.collect.ImmutableList; -import com.googlecode.objectify.Key; -import google.registry.config.RegistryConfig.Config; -import google.registry.mapreduce.MapreduceRunner; -import google.registry.mapreduce.inputs.EppResourceInputs; -import google.registry.model.EppResource; -import google.registry.model.domain.DomainHistory; -import google.registry.model.reporting.HistoryEntry; -import google.registry.rde.RdeStagingAction; -import google.registry.request.Action; -import google.registry.request.Response; -import google.registry.request.auth.Auth; -import google.registry.tools.server.GenerateZoneFilesAction; -import javax.inject.Inject; - -/** - * A mapreduce that creates synthetic history objects in SQL for all {@link EppResource} objects. - * - *

Certain operations, e.g. {@link RdeStagingAction} or {@link GenerateZoneFilesAction}, require - * that we are able to answer the question of "What did this EPP resource look like at a point in - * time?" In the Datastore world, we are able to answer this question using the commit logs, however - * this is no longer possible in the SQL world. Instead, we will use the history objects, e.g. - * {@link DomainHistory} to see what a particular resource looked like at that point in time, since - * history objects store a snapshot of that resource. - * - *

This command creates a synthetic history object at the current point in time for every single - * EPP resource to guarantee that later on, when examining in-the-past resources, we have some - * history object for which the EppResource field is filled. This synthetic history object contains - * basically nothing and its only purpose is to create a populated history object in SQL through - * asynchronous replication. - * - *

NB: This class operates entirely in Datastore, which may be counterintuitive at first glance. - * However, since this is meant to be run during the Datastore-primary, SQL-secondary stage of the - * migration, we want to make sure that we are using the most up-to-date version of the data. The - * resource field of the history objects will be populated during asynchronous migration, e.g. in - * {@link DomainHistory#beforeSqlSaveOnReplay}. - */ -@Action( - service = Action.Service.BACKEND, - path = "/_dr/task/createSyntheticHistoryEntries", - auth = Auth.AUTH_INTERNAL_OR_ADMIN) -public class CreateSyntheticHistoryEntriesAction implements Runnable { - - private static final String HISTORY_REASON = - "Backfill EppResource history objects during Cloud SQL migration"; - - private final MapreduceRunner mrRunner; - private final Response response; - private final String registryAdminRegistrarId; - - @Inject - CreateSyntheticHistoryEntriesAction( - MapreduceRunner mrRunner, - Response response, - @Config("registryAdminClientId") String registryAdminRegistrarId) { - this.mrRunner = mrRunner; - this.response = response; - this.registryAdminRegistrarId = registryAdminRegistrarId; - } - - /** - * The default number of shards to run the map-only mapreduce on. - * - *

This is much lower than the default of 100 because we can afford it being slow and we want - * to avoid overloading SQL. - */ - private static final int NUM_SHARDS = 10; - - @Override - public void run() { - mrRunner - .setJobName("Create a synthetic HistoryEntry for each EPP resource") - .setModuleName("backend") - .setDefaultMapShards(NUM_SHARDS) - .runMapOnly( - new CreateSyntheticHistoryEntriesMapper(registryAdminRegistrarId), - ImmutableList.of(EppResourceInputs.createKeyInput(EppResource.class))) - .sendLinkToMapreduceConsole(response); - } - - // Returns true iff any of the *History objects in SQL contain a representation of this resource - // at the point in time that the *History object was created. - private static boolean hasHistoryContainingResource(EppResource resource) { - return jpaTm() - .transact( - () -> { - // Use READ COMMITTED isolation level so that any long-living queries don't cause - // collection of predicate locks to spiral out of control (as would happen with a - // SERIALIZABLE isolation level) - // - // NB: setting the isolation level inside the transaction only works for Postgres and - // will be reverted to the default once the transaction is committed. - jpaTm() - .getEntityManager() - .createNativeQuery("SET TRANSACTION ISOLATION LEVEL READ COMMITTED") - .executeUpdate(); - // The class we're searching from is based on which parent type (e.g. Domain) we have - Class historyClass = - getHistoryClassFromParent(resource.getClass()); - // The field representing repo ID unfortunately varies by history class - String repoIdFieldName = - CaseFormat.LOWER_CAMEL.to( - CaseFormat.LOWER_UNDERSCORE, - getRepoIdFieldNameFromHistoryClass(historyClass)); - // The "history" fields in the *History objects are all prefixed with "history_". If - // any of the non-"history_" fields are non-null, that means that that row contains - // a representation of that EppResource at that point in time. We use creation_time as - // a marker since it's the simplest field and all EppResources will have it. - return (boolean) - jpaTm() - .getEntityManager() - .createNativeQuery( - String.format( - "SELECT EXISTS (SELECT 1 FROM \"%s\" WHERE %s = :repoId AND" - + " creation_time IS NOT NULL)", - historyClass.getSimpleName(), repoIdFieldName)) - .setParameter("repoId", resource.getRepoId()) - .getSingleResult(); - }); - } - - // Lifted from HistoryEntryDao - private static Class getHistoryClassFromParent( - Class parent) { - if (!RESOURCE_TYPES_TO_HISTORY_TYPES.containsKey(parent)) { - throw new IllegalArgumentException( - String.format("Unknown history type for parent %s", parent.getName())); - } - return RESOURCE_TYPES_TO_HISTORY_TYPES.get(parent); - } - - // Lifted from HistoryEntryDao - private static String getRepoIdFieldNameFromHistoryClass( - Class historyClass) { - if (!REPO_ID_FIELD_NAMES.containsKey(historyClass)) { - throw new IllegalArgumentException( - String.format("Unknown history type %s", historyClass.getName())); - } - return REPO_ID_FIELD_NAMES.get(historyClass); - } - - /** Mapper to re-save all EPP resources. */ - public static class CreateSyntheticHistoryEntriesMapper - extends Mapper, Void, Void> { - - private final String registryAdminRegistrarId; - - public CreateSyntheticHistoryEntriesMapper(String registryAdminRegistrarId) { - this.registryAdminRegistrarId = registryAdminRegistrarId; - } - - @Override - public final void map(final Key resourceKey) { - EppResource eppResource = auditedOfy().load().key(resourceKey).now(); - // Only save new history entries if the most recent history for this object in SQL does not - // have the resource at that point in time already - if (!hasHistoryContainingResource(eppResource)) { - ofyTm() - .transact( - () -> - ofyTm() - .put( - HistoryEntry.createBuilderForResource(eppResource) - .setRegistrarId(registryAdminRegistrarId) - .setBySuperuser(true) - .setRequestedByRegistrar(false) - .setModificationTime(ofyTm().getTransactionTime()) - .setReason(HISTORY_REASON) - .setType(HistoryEntry.Type.SYNTHETIC) - .build())); - } - } - } -} diff --git a/core/src/main/java/google/registry/tools/javascrap/CreateSyntheticHistoryEntriesPipeline.java b/core/src/main/java/google/registry/tools/javascrap/CreateSyntheticHistoryEntriesPipeline.java deleted file mode 100644 index 19319a13b..000000000 --- a/core/src/main/java/google/registry/tools/javascrap/CreateSyntheticHistoryEntriesPipeline.java +++ /dev/null @@ -1,135 +0,0 @@ -// Copyright 2021 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.javascrap; - -import static google.registry.persistence.transaction.TransactionManagerFactory.jpaTm; - -import com.google.common.collect.ImmutableList; -import dagger.Component; -import google.registry.beam.common.RegistryJpaIO; -import google.registry.beam.common.RegistryPipelineOptions; -import google.registry.config.RegistryConfig.Config; -import google.registry.config.RegistryConfig.ConfigModule; -import google.registry.model.EppResource; -import google.registry.model.UpdateAutoTimestamp; -import google.registry.model.UpdateAutoTimestamp.DisableAutoUpdateResource; -import google.registry.model.contact.ContactResource; -import google.registry.model.domain.DomainBase; -import google.registry.model.host.HostResource; -import google.registry.model.reporting.HistoryEntry; -import google.registry.persistence.PersistenceModule.TransactionIsolationLevel; -import google.registry.persistence.VKey; -import java.io.Serializable; -import javax.inject.Singleton; -import javax.persistence.Entity; -import org.apache.beam.sdk.Pipeline; -import org.apache.beam.sdk.options.PipelineOptionsFactory; -import org.apache.beam.sdk.transforms.MapElements; -import org.apache.beam.sdk.values.TypeDescriptor; - -/** - * Pipeline that creates a synthetic history entry for every {@link EppResource} in SQL at the - * current time. - * - *

The history entries in Datastore does not have the EPP resource embedded in them. Therefore - * after {@link google.registry.beam.initsql.InitSqlPipeline} runs, these fields will all be empty. - * This pipeline loads all EPP resources and for each of them creates a synthetic history entry that - * contains the resource and saves them back to SQL, so that they can be used in the RDE pipeline. - * - *

Note that this pipeline should only be run in a test environment right after the init SQL - * pipeline finishes, and no EPP update is being made to the system, otherwise there is no garuantee - * that the latest history entry for a given EPP resource does not already have the resource - * embedded within it. - * - *

To run the pipeline: - * - *

- * $ ./nom_build :core:cSHE --args="--region=us-central1 - * --runner=DataflowRunner - * --registryEnvironment=CRASH - * --project={project-id} - * --workerMachineType=n2-standard-4" - * - * - * @see google.registry.tools.javascrap.CreateSyntheticHistoryEntriesAction - */ -public class CreateSyntheticHistoryEntriesPipeline implements Serializable { - - private static final ImmutableList> EPP_RESOURCE_CLASSES = - ImmutableList.of(DomainBase.class, ContactResource.class, HostResource.class); - - private static final String HISTORY_REASON = - "Backfill EppResource history objects after initial backup to SQL"; - - static void setup(Pipeline pipeline, String registryAdminRegistrarId) { - for (Class clazz : EPP_RESOURCE_CLASSES) { - pipeline - .apply( - String.format("Read all %s", clazz.getSimpleName()), - RegistryJpaIO.read( - "SELECT id FROM %entity%" - .replace("%entity%", clazz.getAnnotation(Entity.class).name()), - String.class, - repoId -> VKey.createSql(clazz, repoId))) - .apply( - String.format("Save a synthetic HistoryEntry for each %s", clazz), - MapElements.into(TypeDescriptor.of(Void.class)) - .via( - (VKey key) -> { - jpaTm() - .transact( - () -> { - EppResource eppResource = jpaTm().loadByKey(key); - try (DisableAutoUpdateResource disable = - UpdateAutoTimestamp.disableAutoUpdate()) { - jpaTm() - .put( - HistoryEntry.createBuilderForResource(eppResource) - .setRegistrarId(registryAdminRegistrarId) - .setBySuperuser(true) - .setRequestedByRegistrar(false) - .setModificationTime(jpaTm().getTransactionTime()) - .setReason(HISTORY_REASON) - .setType(HistoryEntry.Type.SYNTHETIC) - .build()); - } - }); - return null; - })); - } - } - - public static void main(String[] args) { - RegistryPipelineOptions options = - PipelineOptionsFactory.fromArgs(args).withValidation().as(RegistryPipelineOptions.class); - RegistryPipelineOptions.validateRegistryPipelineOptions(options); - options.setIsolationOverride(TransactionIsolationLevel.TRANSACTION_READ_COMMITTED); - String registryAdminRegistrarId = - DaggerCreateSyntheticHistoryEntriesPipeline_ConfigComponent.create() - .getRegistryAdminRegistrarId(); - - Pipeline pipeline = Pipeline.create(options); - setup(pipeline, registryAdminRegistrarId); - pipeline.run(); - } - - @Singleton - @Component(modules = ConfigModule.class) - interface ConfigComponent { - - @Config("registryAdminClientId") - String getRegistryAdminRegistrarId(); - } -} diff --git a/core/src/test/java/google/registry/batch/DeleteContactsAndHostsActionTest.java b/core/src/test/java/google/registry/batch/DeleteContactsAndHostsActionTest.java deleted file mode 100644 index f2170b83b..000000000 --- a/core/src/test/java/google/registry/batch/DeleteContactsAndHostsActionTest.java +++ /dev/null @@ -1,950 +0,0 @@ -// Copyright 2017 The Nomulus Authors. All Rights Reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package google.registry.batch; - -import static com.google.appengine.api.taskqueue.QueueFactory.getQueue; -import static com.google.common.collect.MoreCollectors.onlyElement; -import static com.google.common.truth.Truth.assertThat; -import static com.google.common.truth.Truth8.assertThat; -import static google.registry.batch.AsyncTaskEnqueuer.PARAM_RESOURCE_KEY; -import static google.registry.batch.AsyncTaskEnqueuer.QUEUE_ASYNC_DELETE; -import static google.registry.batch.AsyncTaskMetrics.OperationResult.STALE; -import static google.registry.model.EppResourceUtils.loadByForeignKey; -import static google.registry.model.eppcommon.StatusValue.PENDING_DELETE; -import static google.registry.model.ofy.ObjectifyService.auditedOfy; -import static google.registry.model.reporting.HistoryEntry.Type.CONTACT_DELETE; -import static google.registry.model.reporting.HistoryEntry.Type.CONTACT_DELETE_FAILURE; -import static google.registry.model.reporting.HistoryEntry.Type.CONTACT_TRANSFER_REQUEST; -import static google.registry.model.reporting.HistoryEntry.Type.HOST_DELETE; -import static google.registry.model.reporting.HistoryEntry.Type.HOST_DELETE_FAILURE; -import static google.registry.testing.ContactResourceSubject.assertAboutContacts; -import static google.registry.testing.DatabaseHelper.assertNoBillingEvents; -import static google.registry.testing.DatabaseHelper.createTld; -import static google.registry.testing.DatabaseHelper.getOnlyHistoryEntryOfType; -import static google.registry.testing.DatabaseHelper.getOnlyPollMessageForHistoryEntry; -import static google.registry.testing.DatabaseHelper.getPollMessages; -import static google.registry.testing.DatabaseHelper.newContactResource; -import static google.registry.testing.DatabaseHelper.newDomainBase; -import static google.registry.testing.DatabaseHelper.newHostResource; -import static google.registry.testing.DatabaseHelper.persistActiveContact; -import static google.registry.testing.DatabaseHelper.persistActiveHost; -import static google.registry.testing.DatabaseHelper.persistContactWithPendingTransfer; -import static google.registry.testing.DatabaseHelper.persistDeletedContact; -import static google.registry.testing.DatabaseHelper.persistDeletedHost; -import static google.registry.testing.DatabaseHelper.persistResource; -import static google.registry.testing.HostResourceSubject.assertAboutHosts; -import static google.registry.testing.TaskQueueHelper.assertDnsTasksEnqueued; -import static google.registry.testing.TaskQueueHelper.assertNoTasksEnqueued; -import static google.registry.testing.TaskQueueHelper.assertTasksEnqueued; -import static google.registry.util.DateTimeUtils.END_OF_TIME; -import static org.joda.time.DateTimeZone.UTC; -import static org.joda.time.Duration.millis; -import static org.joda.time.Duration.standardDays; -import static org.joda.time.Duration.standardHours; -import static org.joda.time.Duration.standardSeconds; -import static org.mockito.ArgumentMatchers.anyString; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.verifyNoMoreInteractions; -import static org.mockito.Mockito.when; - -import com.google.appengine.api.taskqueue.TaskOptions; -import com.google.appengine.api.taskqueue.TaskOptions.Method; -import com.google.common.collect.ImmutableList; -import com.google.common.collect.ImmutableSet; -import com.google.common.collect.Iterables; -import google.registry.batch.AsyncTaskMetrics.OperationResult; -import google.registry.batch.AsyncTaskMetrics.OperationType; -import google.registry.batch.DeleteContactsAndHostsAction.DeleteEppResourceReducer; -import google.registry.model.EppResource; -import google.registry.model.contact.ContactAddress; -import google.registry.model.contact.ContactPhoneNumber; -import google.registry.model.contact.ContactResource; -import google.registry.model.contact.PostalInfo; -import google.registry.model.domain.DomainBase; -import google.registry.model.eppcommon.StatusValue; -import google.registry.model.eppcommon.Trid; -import google.registry.model.eppoutput.EppResponse.ResponseData; -import google.registry.model.host.HostResource; -import google.registry.model.ofy.Ofy; -import google.registry.model.poll.PendingActionNotificationResponse; -import google.registry.model.poll.PendingActionNotificationResponse.ContactPendingActionNotificationResponse; -import google.registry.model.poll.PendingActionNotificationResponse.HostPendingActionNotificationResponse; -import google.registry.model.poll.PollMessage; -import google.registry.model.poll.PollMessage.OneTime; -import google.registry.model.reporting.HistoryEntry; -import google.registry.model.server.Lock; -import google.registry.model.tld.Registry; -import google.registry.model.transfer.ContactTransferData; -import google.registry.model.transfer.TransferData; -import google.registry.model.transfer.TransferResponse; -import google.registry.model.transfer.TransferStatus; -import google.registry.testing.CloudTasksHelper; -import google.registry.testing.FakeClock; -import google.registry.testing.FakeResponse; -import google.registry.testing.FakeSleeper; -import google.registry.testing.InjectExtension; -import google.registry.testing.TaskQueueHelper.TaskMatcher; -import google.registry.testing.mapreduce.MapreduceTestCase; -import google.registry.util.RequestStatusChecker; -import google.registry.util.Retrier; -import google.registry.util.Sleeper; -import google.registry.util.SystemSleeper; -import java.util.Optional; -import org.joda.time.DateTime; -import org.joda.time.Duration; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.RegisterExtension; -import org.mockito.Mock; - -/** Unit tests for {@link DeleteContactsAndHostsAction}. */ -public class DeleteContactsAndHostsActionTest - extends MapreduceTestCase { - - @RegisterExtension public final InjectExtension inject = new InjectExtension(); - - private AsyncTaskEnqueuer enqueuer; - private final FakeClock clock = new FakeClock(DateTime.parse("2015-01-15T11:22:33Z")); - private final FakeResponse fakeResponse = new FakeResponse(); - @Mock private RequestStatusChecker requestStatusChecker; - - private void runMapreduce() throws Exception { - clock.advanceBy(standardSeconds(5)); - // Apologies for the hard sleeps. Without them, the tests can be flaky because the tasks aren't - // quite fully enqueued by the time the tests attempt to lease from the queue. - Sleeper sleeper = new SystemSleeper(); - sleeper.sleep(millis(50)); - action.run(); - sleeper.sleep(millis(50)); - executeTasksUntilEmpty("mapreduce", clock); - sleeper.sleep(millis(50)); - clock.advanceBy(standardSeconds(5)); - auditedOfy().clearSessionCache(); - } - - /** Kicks off, but does not run, the mapreduce tasks. Useful for testing validation/setup. */ - private void enqueueMapreduceOnly() { - clock.advanceBy(standardSeconds(5)); - action.run(); - clock.advanceBy(standardSeconds(5)); - auditedOfy().clearSessionCache(); - } - - @BeforeEach - void beforeEach() { - inject.setStaticField(Ofy.class, "clock", clock); - enqueuer = - AsyncTaskEnqueuerTest.createForTesting( - new CloudTasksHelper(clock).getTestCloudTasksUtils(), clock, Duration.ZERO); - AsyncTaskMetrics asyncTaskMetricsMock = mock(AsyncTaskMetrics.class); - action = new DeleteContactsAndHostsAction(); - action.asyncTaskMetrics = asyncTaskMetricsMock; - inject.setStaticField(DeleteEppResourceReducer.class, "asyncTaskMetrics", asyncTaskMetricsMock); - action.clock = clock; - action.mrRunner = makeDefaultRunner(); - action.requestStatusChecker = requestStatusChecker; - action.response = fakeResponse; - action.retrier = new Retrier(new FakeSleeper(clock), 1); - action.queue = getQueue(QUEUE_ASYNC_DELETE); - when(requestStatusChecker.getLogId()).thenReturn("requestId"); - when(requestStatusChecker.isRunning(anyString())) - .thenThrow(new AssertionError("Should not be called")); - - createTld("tld"); - clock.advanceOneMilli(); - } - - @Test - void testSuccess_contact_referencedByActiveDomain_doesNotGetDeleted() throws Exception { - ContactResource contact = persistContactPendingDelete("blah8221"); - persistResource(newDomainBase("example.tld", contact)); - DateTime timeEnqueued = clock.nowUtc(); - enqueuer.enqueueAsyncDelete( - contact, - timeEnqueued, - "TheRegistrar", - Trid.create("fakeClientTrid", "fakeServerTrid"), - false); - runMapreduce(); - ContactResource contactUpdated = - loadByForeignKey(ContactResource.class, "blah8221", clock.nowUtc()).get(); - assertAboutContacts() - .that(contactUpdated) - .doesNotHaveStatusValue(PENDING_DELETE) - .and() - .hasDeletionTime(END_OF_TIME); - DomainBase domainReloaded = - loadByForeignKey(DomainBase.class, "example.tld", clock.nowUtc()).get(); - // We have to check for the objectify key here, specifically, as the SQL side of the key does - // not get persisted. - assertThat(domainReloaded.getReferencedContacts()).contains(contactUpdated.createVKey()); - HistoryEntry historyEntry = - getOnlyHistoryEntryOfType(contactUpdated, HistoryEntry.Type.CONTACT_DELETE_FAILURE); - assertPollMessageFor( - historyEntry, - "TheRegistrar", - "Can't delete contact blah8221 because it is referenced by a domain.", - false, - contact, - Optional.of("fakeClientTrid")); - assertNoTasksEnqueued(QUEUE_ASYNC_DELETE); - verify(action.asyncTaskMetrics).recordContactHostDeletionBatchSize(1L); - verify(action.asyncTaskMetrics) - .recordAsyncFlowResult(OperationType.CONTACT_DELETE, OperationResult.FAILURE, timeEnqueued); - verifyNoMoreInteractions(action.asyncTaskMetrics); - } - - @Test - void testSuccess_contact_notReferenced_getsDeleted_andPiiWipedOut() throws Exception { - runSuccessfulContactDeletionTest(Optional.of("fakeClientTrid")); - } - - @Test - void testSuccess_contact_andNoClientTrid_deletesSuccessfully() throws Exception { - runSuccessfulContactDeletionTest(Optional.empty()); - } - - @Test - void test_cannotAcquireLock() { - // Make lock acquisition fail. - acquireLock(); - enqueueMapreduceOnly(); - assertThat(fakeResponse.getPayload()).isEqualTo("Can't acquire lock; aborting."); - } - - @Test - void test_mapreduceHasWorkToDo_lockIsAcquired() { - ContactResource contact = persistContactPendingDelete("blah8221"); - persistResource(newDomainBase("example.tld", contact)); - DateTime timeEnqueued = clock.nowUtc(); - enqueuer.enqueueAsyncDelete( - contact, - timeEnqueued, - "TheRegistrar", - Trid.create("fakeClientTrid", "fakeServerTrid"), - false); - enqueueMapreduceOnly(); - assertThat(acquireLock()).isEmpty(); - } - - @Test - void test_noTasksToLease_releasesLockImmediately() { - enqueueMapreduceOnly(); - // If the Lock was correctly released, then we can acquire it now. - assertThat(acquireLock()).isPresent(); - } - - private void runSuccessfulContactDeletionTest(Optional clientTrid) throws Exception { - ContactResource contact = persistContactWithPii("jim919"); - DateTime timeEnqueued = clock.nowUtc(); - enqueuer.enqueueAsyncDelete( - contact, - timeEnqueued, - "TheRegistrar", - Trid.create(clientTrid.orElse(null), "fakeServerTrid"), - false); - runMapreduce(); - assertThat(loadByForeignKey(ContactResource.class, "jim919", clock.nowUtc())).isEmpty(); - ContactResource contactAfterDeletion = auditedOfy().load().entity(contact).now(); - assertAboutContacts() - .that(contactAfterDeletion) - .isNotActiveAt(clock.nowUtc()) - // Note that there will be another history entry of CONTACT_PENDING_DELETE, but this is - // added by the flow and not the mapreduce itself. - .and() - .hasOnlyOneHistoryEntryWhich() - .hasType(CONTACT_DELETE); - assertAboutContacts() - .that(contactAfterDeletion) - .hasNullLocalizedPostalInfo() - .and() - .hasNullInternationalizedPostalInfo() - .and() - .hasNullEmailAddress() - .and() - .hasNullVoiceNumber() - .and() - .hasNullFaxNumber(); - HistoryEntry historyEntry = getOnlyHistoryEntryOfType(contactAfterDeletion, CONTACT_DELETE); - assertPollMessageFor( - historyEntry, "TheRegistrar", "Deleted contact jim919.", true, contact, clientTrid); - assertNoTasksEnqueued(QUEUE_ASYNC_DELETE); - verify(action.asyncTaskMetrics).recordContactHostDeletionBatchSize(1L); - verify(action.asyncTaskMetrics) - .recordAsyncFlowResult(OperationType.CONTACT_DELETE, OperationResult.SUCCESS, timeEnqueued); - verifyNoMoreInteractions(action.asyncTaskMetrics); - - } - - @Test - void testSuccess_contactWithoutPendingTransfer_isDeletedAndHasNoTransferData() throws Exception { - ContactResource contact = persistContactPendingDelete("blah8221"); - enqueuer.enqueueAsyncDelete( - contact, - clock.nowUtc(), - "TheRegistrar", - Trid.create("fakeClientTrid", "fakeServerTrid"), - false); - runMapreduce(); - ContactResource contactAfterDeletion = auditedOfy().load().entity(contact).now(); - assertThat(contactAfterDeletion.getTransferData()).isEqualTo(ContactTransferData.EMPTY); - } - - @Test - void testSuccess_contactWithPendingTransfer_getsDeleted() throws Exception { - DateTime transferRequestTime = clock.nowUtc().minusDays(3); - ContactResource contact = - persistContactWithPendingTransfer( - newContactResource("sh8013").asBuilder().addStatusValue(PENDING_DELETE).build(), - transferRequestTime, - transferRequestTime.plus(Registry.DEFAULT_TRANSFER_GRACE_PERIOD), - clock.nowUtc()); - enqueuer.enqueueAsyncDelete( - contact, - clock.nowUtc(), - "TheRegistrar", - Trid.create("fakeClientTrid", "fakeServerTrid"), - false); - runMapreduce(); - // Check that the contact is deleted as of now. - assertThat(loadByForeignKey(ContactResource.class, "sh8013", clock.nowUtc())).isEmpty(); - // Check that it's still there (it wasn't deleted yesterday) and that it has history. - ContactResource softDeletedContact = - loadByForeignKey(ContactResource.class, "sh8013", clock.nowUtc().minusDays(1)).get(); - assertAboutContacts() - .that(softDeletedContact) - .hasOneHistoryEntryEachOfTypes(CONTACT_TRANSFER_REQUEST, CONTACT_DELETE); - // Check that the transfer data reflects the cancelled transfer as we expect. - TransferData oldTransferData = contact.getTransferData(); - assertThat(softDeletedContact.getTransferData()) - .isEqualTo( - oldTransferData.copyConstantFieldsToBuilder() - .setTransferStatus(TransferStatus.SERVER_CANCELLED) - .setPendingTransferExpirationTime(softDeletedContact.getDeletionTime()) - .build()); - assertNoBillingEvents(); - PollMessage deletePollMessage = - Iterables.getOnlyElement(getPollMessages("TheRegistrar", clock.nowUtc().plusMonths(1))); - assertThat(deletePollMessage.getMsg()).isEqualTo("Deleted contact sh8013."); - // The poll message in the future to the gaining registrar should be gone too, but there - // should be one at the current time to the gaining registrar. - PollMessage gainingPollMessage = - Iterables.getOnlyElement(getPollMessages("NewRegistrar", clock.nowUtc())); - assertThat(gainingPollMessage.getEventTime()).isLessThan(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); - } - - @Test - void testSuccess_contact_referencedByDeletedDomain_getsDeleted() throws Exception { - ContactResource contactUsed = persistContactPendingDelete("blah1234"); - persistResource( - newDomainBase("example.tld", contactUsed) - .asBuilder() - .setDeletionTime(clock.nowUtc().minusDays(3)) - .build()); - enqueuer.enqueueAsyncDelete( - contactUsed, - clock.nowUtc(), - "TheRegistrar", - Trid.create("fakeClientTrid", "fakeServerTrid"), - false); - runMapreduce(); - assertThat(loadByForeignKey(ContactResource.class, "blah1234", clock.nowUtc())).isEmpty(); - ContactResource contactBeforeDeletion = - loadByForeignKey(ContactResource.class, "blah1234", clock.nowUtc().minusDays(1)).get(); - assertAboutContacts() - .that(contactBeforeDeletion) - .isNotActiveAt(clock.nowUtc()) - .and() - .hasExactlyStatusValues(StatusValue.OK) - // Note that there will be another history entry of CONTACT_PENDING_DELETE, but this is - // added by the flow and not the mapreduce itself. - .and() - .hasOnlyOneHistoryEntryWhich() - .hasType(CONTACT_DELETE); - HistoryEntry historyEntry = getOnlyHistoryEntryOfType(contactBeforeDeletion, CONTACT_DELETE); - assertPollMessageFor( - historyEntry, - "TheRegistrar", - "Deleted contact blah1234.", - true, - contactUsed, - Optional.of("fakeClientTrid")); - assertNoTasksEnqueued(QUEUE_ASYNC_DELETE); - } - - @Test - void testSuccess_contact_notRequestedByOwner_doesNotGetDeleted() throws Exception { - ContactResource contact = persistContactPendingDelete("jane0991"); - enqueuer.enqueueAsyncDelete( - contact, - clock.nowUtc(), - "OtherRegistrar", - Trid.create("fakeClientTrid", "fakeServerTrid"), - false); - runMapreduce(); - ContactResource contactAfter = - loadByForeignKey(ContactResource.class, "jane0991", clock.nowUtc()).get(); - assertAboutContacts() - .that(contactAfter) - .doesNotHaveStatusValue(PENDING_DELETE) - .and() - .hasDeletionTime(END_OF_TIME); - HistoryEntry historyEntry = getOnlyHistoryEntryOfType(contactAfter, CONTACT_DELETE_FAILURE); - assertPollMessageFor( - historyEntry, - "OtherRegistrar", - "Can't delete contact jane0991 because it was transferred prior to deletion.", - false, - contact, - Optional.of("fakeClientTrid")); - assertNoTasksEnqueued(QUEUE_ASYNC_DELETE); - } - - @Test - void testSuccess_contact_notRequestedByOwner_isSuperuser_getsDeleted() throws Exception { - ContactResource contact = persistContactWithPii("nate007"); - enqueuer.enqueueAsyncDelete( - contact, - clock.nowUtc(), - "OtherRegistrar", - Trid.create("fakeClientTrid", "fakeServerTrid"), - true); - runMapreduce(); - assertThat(loadByForeignKey(ContactResource.class, "nate007", clock.nowUtc())).isEmpty(); - ContactResource contactAfterDeletion = auditedOfy().load().entity(contact).now(); - assertAboutContacts() - .that(contactAfterDeletion) - .isNotActiveAt(clock.nowUtc()) - // Note that there will be another history entry of CONTACT_PENDING_DELETE, but this is - // added by the flow and not the mapreduce itself. - .and() - .hasOnlyOneHistoryEntryWhich() - .hasType(CONTACT_DELETE); - assertAboutContacts() - .that(contactAfterDeletion) - .hasNullLocalizedPostalInfo() - .and() - .hasNullInternationalizedPostalInfo() - .and() - .hasNullEmailAddress() - .and() - .hasNullVoiceNumber() - .and() - .hasNullFaxNumber(); - HistoryEntry historyEntry = getOnlyHistoryEntryOfType(contactAfterDeletion, CONTACT_DELETE); - assertPollMessageFor( - historyEntry, - "OtherRegistrar", - "Deleted contact nate007.", - true, - contact, - Optional.of("fakeClientTrid")); - assertNoTasksEnqueued(QUEUE_ASYNC_DELETE); - } - - @Test - void testSuccess_targetResourcesDontExist_areDelayedForADay() { - ContactResource contactNotSaved = newContactResource("somecontact"); - HostResource hostNotSaved = newHostResource("a11.blah.foo"); - DateTime timeBeforeRun = clock.nowUtc(); - enqueuer.enqueueAsyncDelete( - contactNotSaved, - timeBeforeRun, - "TheRegistrar", - Trid.create("fakeClientTrid", "fakeServerTrid"), - false); - enqueuer.enqueueAsyncDelete( - hostNotSaved, - timeBeforeRun, - "TheRegistrar", - Trid.create("fakeClientTrid", "fakeServerTrid"), - false); - enqueueMapreduceOnly(); - assertTasksEnqueued( - QUEUE_ASYNC_DELETE, - new TaskMatcher() - .etaDelta(standardHours(23), standardHours(25)) - .param(PARAM_RESOURCE_KEY, contactNotSaved.createVKey().stringify()) - .param("requestingClientId", "TheRegistrar") - .param("clientTransactionId", "fakeClientTrid") - .param("serverTransactionId", "fakeServerTrid") - .param("isSuperuser", "false") - .param("requestedTime", timeBeforeRun.toString()), - new TaskMatcher() - .etaDelta(standardHours(23), standardHours(25)) - .param(PARAM_RESOURCE_KEY, hostNotSaved.createVKey().stringify()) - .param("requestingClientId", "TheRegistrar") - .param("clientTransactionId", "fakeClientTrid") - .param("serverTransactionId", "fakeServerTrid") - .param("isSuperuser", "false") - .param("requestedTime", timeBeforeRun.toString())); - assertThat(acquireLock()).isPresent(); - } - - @Test - void testSuccess_unparseableTasks_areDelayedForADay() { - TaskOptions task = - TaskOptions.Builder.withMethod(Method.PULL).param("gobbledygook", "kljhadfgsd9f7gsdfh"); - getQueue(QUEUE_ASYNC_DELETE).add(task); - enqueueMapreduceOnly(); - assertTasksEnqueued( - QUEUE_ASYNC_DELETE, - new TaskMatcher() - .payload("gobbledygook=kljhadfgsd9f7gsdfh") - .etaDelta(standardHours(23), standardHours(25))); - verify(action.asyncTaskMetrics).recordContactHostDeletionBatchSize(1L); - verifyNoMoreInteractions(action.asyncTaskMetrics); - assertThat(acquireLock()).isPresent(); - } - - @Test - void testSuccess_resourcesNotInPendingDelete_areSkipped() { - ContactResource contact = persistActiveContact("blah2222"); - HostResource host = persistActiveHost("rustles.your.jimmies"); - DateTime timeEnqueued = clock.nowUtc(); - enqueuer.enqueueAsyncDelete( - contact, - timeEnqueued, - "TheRegistrar", - Trid.create("fakeClientTrid", "fakeServerTrid"), - false); - enqueuer.enqueueAsyncDelete( - host, - timeEnqueued, - "TheRegistrar", - Trid.create("fakeClientTrid", "fakeServerTrid"), - false); - enqueueMapreduceOnly(); - assertThat(loadByForeignKey(ContactResource.class, "blah2222", clock.nowUtc())) - .hasValue(contact); - assertThat(loadByForeignKey(HostResource.class, "rustles.your.jimmies", clock.nowUtc())) - .hasValue(host); - assertNoTasksEnqueued(QUEUE_ASYNC_DELETE); - verify(action.asyncTaskMetrics).recordContactHostDeletionBatchSize(2L); - verify(action.asyncTaskMetrics) - .recordAsyncFlowResult(OperationType.CONTACT_DELETE, STALE, timeEnqueued); - verify(action.asyncTaskMetrics) - .recordAsyncFlowResult(OperationType.HOST_DELETE, STALE, timeEnqueued); - verifyNoMoreInteractions(action.asyncTaskMetrics); - assertThat(acquireLock()).isPresent(); - } - - @Test - void testSuccess_alreadyDeletedResources_areSkipped() { - ContactResource contactDeleted = persistDeletedContact("blah1236", clock.nowUtc().minusDays(2)); - HostResource hostDeleted = persistDeletedHost("a.lim.lop", clock.nowUtc().minusDays(3)); - enqueuer.enqueueAsyncDelete( - contactDeleted, - clock.nowUtc(), - "TheRegistrar", - Trid.create("fakeClientTrid", "fakeServerTrid"), - false); - enqueuer.enqueueAsyncDelete( - hostDeleted, - clock.nowUtc(), - "TheRegistrar", - Trid.create("fakeClientTrid", "fakeServerTrid"), - false); - enqueueMapreduceOnly(); - assertThat(auditedOfy().load().entity(contactDeleted).now()).isEqualTo(contactDeleted); - assertThat(auditedOfy().load().entity(hostDeleted).now()).isEqualTo(hostDeleted); - assertNoTasksEnqueued(QUEUE_ASYNC_DELETE); - assertThat(acquireLock()).isPresent(); - } - - @Test - void testSuccess_host_referencedByActiveDomain_doesNotGetDeleted() throws Exception { - HostResource host = persistHostPendingDelete("ns1.example.tld"); - persistUsedDomain("example.tld", persistActiveContact("abc456"), host); - DateTime timeEnqueued = clock.nowUtc(); - enqueuer.enqueueAsyncDelete( - host, - timeEnqueued, - "TheRegistrar", - Trid.create("fakeClientTrid", "fakeServerTrid"), - false); - runMapreduce(); - HostResource hostAfter = - loadByForeignKey(HostResource.class, "ns1.example.tld", clock.nowUtc()).get(); - assertAboutHosts() - .that(hostAfter) - .doesNotHaveStatusValue(PENDING_DELETE) - .and() - .hasDeletionTime(END_OF_TIME); - DomainBase domain = - loadByForeignKey(DomainBase.class, "example.tld", clock.nowUtc()).get(); - assertThat(domain.getNameservers()).contains(hostAfter.createVKey()); - HistoryEntry historyEntry = getOnlyHistoryEntryOfType(hostAfter, HOST_DELETE_FAILURE); - assertPollMessageFor( - historyEntry, - "TheRegistrar", - "Can't delete host ns1.example.tld because it is referenced by a domain.", - false, - host, - Optional.of("fakeClientTrid")); - assertNoTasksEnqueued(QUEUE_ASYNC_DELETE); - verify(action.asyncTaskMetrics).recordContactHostDeletionBatchSize(1L); - verify(action.asyncTaskMetrics) - .recordAsyncFlowResult(OperationType.HOST_DELETE, OperationResult.FAILURE, timeEnqueued); - verifyNoMoreInteractions(action.asyncTaskMetrics); - } - - @Test - void testSuccess_host_notReferenced_getsDeleted() throws Exception { - runSuccessfulHostDeletionTest(Optional.of("fakeClientTrid")); - } - - @Test - void testSuccess_host_andNoClientTrid_deletesSuccessfully() throws Exception { - runSuccessfulHostDeletionTest(Optional.empty()); - } - - private void runSuccessfulHostDeletionTest(Optional clientTrid) throws Exception { - HostResource host = persistHostPendingDelete("ns2.example.tld"); - DateTime timeEnqueued = clock.nowUtc(); - enqueuer.enqueueAsyncDelete( - host, - timeEnqueued, - "TheRegistrar", - Trid.create(clientTrid.orElse(null), "fakeServerTrid"), - false); - runMapreduce(); - assertThat(loadByForeignKey(HostResource.class, "ns2.example.tld", clock.nowUtc())).isEmpty(); - HostResource hostBeforeDeletion = - loadByForeignKey(HostResource.class, "ns2.example.tld", clock.nowUtc().minusDays(1)).get(); - assertAboutHosts() - .that(hostBeforeDeletion) - .isNotActiveAt(clock.nowUtc()) - .and() - .hasExactlyStatusValues(StatusValue.OK) - // Note that there will be another history entry of HOST_PENDING_DELETE, but this is - // added by the flow and not the mapreduce itself. - .and() - .hasOnlyOneHistoryEntryWhich() - .hasType(HOST_DELETE); - HistoryEntry historyEntry = getOnlyHistoryEntryOfType(hostBeforeDeletion, HOST_DELETE); - assertPollMessageFor( - historyEntry, - "TheRegistrar", - "Deleted host ns2.example.tld.", - true, - host, - clientTrid); - assertNoTasksEnqueued(QUEUE_ASYNC_DELETE); - verify(action.asyncTaskMetrics).recordContactHostDeletionBatchSize(1L); - verify(action.asyncTaskMetrics) - .recordAsyncFlowResult(OperationType.HOST_DELETE, OperationResult.SUCCESS, timeEnqueued); - verifyNoMoreInteractions(action.asyncTaskMetrics); - } - - @Test - void testSuccess_host_referencedByDeletedDomain_getsDeleted() throws Exception { - HostResource host = persistHostPendingDelete("ns1.example.tld"); - persistResource( - newDomainBase("example.tld") - .asBuilder() - .setNameservers(ImmutableSet.of(host.createVKey())) - .setDeletionTime(clock.nowUtc().minusDays(5)) - .build()); - enqueuer.enqueueAsyncDelete( - host, - clock.nowUtc(), - "TheRegistrar", - Trid.create("fakeClientTrid", "fakeServerTrid"), - false); - runMapreduce(); - assertThat(loadByForeignKey(HostResource.class, "ns1.example.tld", clock.nowUtc())).isEmpty(); - HostResource hostBeforeDeletion = - loadByForeignKey(HostResource.class, "ns1.example.tld", clock.nowUtc().minusDays(1)).get(); - assertAboutHosts() - .that(hostBeforeDeletion) - .isNotActiveAt(clock.nowUtc()) - .and() - .hasExactlyStatusValues(StatusValue.OK) - // Note that there will be another history entry of HOST_PENDING_DELETE, but this is - // added by the flow and not the mapreduce itself. - .and() - .hasOnlyOneHistoryEntryWhich() - .hasType(HOST_DELETE); - HistoryEntry historyEntry = getOnlyHistoryEntryOfType(hostBeforeDeletion, HOST_DELETE); - assertPollMessageFor( - historyEntry, - "TheRegistrar", - "Deleted host ns1.example.tld.", - true, - host, - Optional.of("fakeClientTrid")); - assertNoTasksEnqueued(QUEUE_ASYNC_DELETE); - } - - @Test - void testSuccess_subordinateHost_getsDeleted() throws Exception { - DomainBase domain = - persistResource( - newDomainBase("example.tld") - .asBuilder() - .setSubordinateHosts(ImmutableSet.of("ns2.example.tld")) - .build()); - HostResource host = - persistResource( - persistHostPendingDelete("ns2.example.tld") - .asBuilder() - .setSuperordinateDomain(domain.createVKey()) - .build()); - enqueuer.enqueueAsyncDelete( - host, - clock.nowUtc(), - "TheRegistrar", - Trid.create("fakeClientTrid", "fakeServerTrid"), - false); - runMapreduce(); - // Check that the host is deleted as of now. - assertThat(loadByForeignKey(HostResource.class, "ns2.example.tld", clock.nowUtc())).isEmpty(); - assertNoBillingEvents(); - assertThat( - loadByForeignKey(DomainBase.class, "example.tld", clock.nowUtc()) - .get() - .getSubordinateHosts()) - .isEmpty(); - assertDnsTasksEnqueued("ns2.example.tld"); - HostResource hostBeforeDeletion = - loadByForeignKey(HostResource.class, "ns2.example.tld", clock.nowUtc().minusDays(1)).get(); - assertAboutHosts() - .that(hostBeforeDeletion) - .isNotActiveAt(clock.nowUtc()) - .and() - .hasExactlyStatusValues(StatusValue.OK) - .and() - .hasOnlyOneHistoryEntryWhich() - .hasType(HOST_DELETE); - HistoryEntry historyEntry = getOnlyHistoryEntryOfType(hostBeforeDeletion, HOST_DELETE); - assertPollMessageFor( - historyEntry, - "TheRegistrar", - "Deleted host ns2.example.tld.", - true, - host, - Optional.of("fakeClientTrid")); - assertNoTasksEnqueued(QUEUE_ASYNC_DELETE); - } - - @Test - void testSuccess_host_notRequestedByOwner_doesNotGetDeleted() throws Exception { - HostResource host = persistHostPendingDelete("ns2.example.tld"); - enqueuer.enqueueAsyncDelete( - host, - clock.nowUtc(), - "OtherRegistrar", - Trid.create("fakeClientTrid", "fakeServerTrid"), - false); - runMapreduce(); - HostResource hostAfter = - loadByForeignKey(HostResource.class, "ns2.example.tld", clock.nowUtc()).get(); - assertAboutHosts() - .that(hostAfter) - .doesNotHaveStatusValue(PENDING_DELETE) - .and() - .hasDeletionTime(END_OF_TIME); - HistoryEntry historyEntry = getOnlyHistoryEntryOfType(host, HOST_DELETE_FAILURE); - assertPollMessageFor( - historyEntry, - "OtherRegistrar", - "Can't delete host ns2.example.tld because it was transferred prior to deletion.", - false, - host, - Optional.of("fakeClientTrid")); - assertNoTasksEnqueued(QUEUE_ASYNC_DELETE); - } - - @Test - void testSuccess_host_notRequestedByOwner_isSuperuser_getsDeleted() throws Exception { - HostResource host = persistHostPendingDelete("ns66.example.tld"); - enqueuer.enqueueAsyncDelete( - host, - clock.nowUtc(), - "OtherRegistrar", - Trid.create("fakeClientTrid", "fakeServerTrid"), - true); - runMapreduce(); - assertThat(loadByForeignKey(HostResource.class, "ns66.example.tld", clock.nowUtc())).isEmpty(); - HostResource hostBeforeDeletion = - loadByForeignKey(HostResource.class, "ns66.example.tld", clock.nowUtc().minusDays(1)).get(); - assertAboutHosts() - .that(hostBeforeDeletion) - .isNotActiveAt(clock.nowUtc()) - .and() - .hasExactlyStatusValues(StatusValue.OK) - // Note that there will be another history entry of HOST_PENDING_DELETE, but this is - // added by the flow and not the mapreduce itself. - .and() - .hasOnlyOneHistoryEntryWhich() - .hasType(HOST_DELETE); - HistoryEntry historyEntry = getOnlyHistoryEntryOfType(hostBeforeDeletion, HOST_DELETE); - assertPollMessageFor( - historyEntry, - "OtherRegistrar", - "Deleted host ns66.example.tld.", - true, - host, - Optional.of("fakeClientTrid")); - assertNoTasksEnqueued(QUEUE_ASYNC_DELETE); - } - - @Test - void testSuccess_deleteABunchOfContactsAndHosts_butNotSome() throws Exception { - ContactResource c1 = persistContactPendingDelete("nsaid54"); - ContactResource c2 = persistContactPendingDelete("nsaid55"); - ContactResource c3 = persistContactPendingDelete("nsaid57"); - HostResource h1 = persistHostPendingDelete("nn5.example.tld"); - HostResource h2 = persistHostPendingDelete("no.foos.ball"); - HostResource h3 = persistHostPendingDelete("slime.wars.fun"); - ContactResource c4 = persistContactPendingDelete("iaminuse6"); - HostResource h4 = persistHostPendingDelete("used.host.com"); - persistUsedDomain("usescontactandhost.tld", c4, h4); - for (EppResource resource : ImmutableList.of(c1, c2, c3, c4, h1, h2, h3, h4)) { - enqueuer.enqueueAsyncDelete( - resource, - clock.nowUtc(), - "TheRegistrar", - Trid.create("fakeClientTrid", "fakeServerTrid"), - false); - } - runMapreduce(); - for (EppResource resource : ImmutableList.of(c1, c2, c3, h1, h2, h3)) { - EppResource loaded = auditedOfy().load().entity(resource).now(); - assertThat(loaded.getDeletionTime()).isLessThan(DateTime.now(UTC)); - assertThat(loaded.getStatusValues()).doesNotContain(PENDING_DELETE); - } - for (EppResource resource : ImmutableList.of(c4, h4)) { - EppResource loaded = auditedOfy().load().entity(resource).now(); - assertThat(loaded.getDeletionTime()).isEqualTo(END_OF_TIME); - assertThat(loaded.getStatusValues()).doesNotContain(PENDING_DELETE); - } - assertNoTasksEnqueued(QUEUE_ASYNC_DELETE); - } - - private static ContactResource persistContactWithPii(String contactId) { - return persistResource( - newContactResource(contactId) - .asBuilder() - .setLocalizedPostalInfo( - new PostalInfo.Builder() - .setType(PostalInfo.Type.LOCALIZED) - .setAddress( - new ContactAddress.Builder() - .setStreet(ImmutableList.of("123 Grand Ave")) - .build()) - .build()) - .setInternationalizedPostalInfo( - new PostalInfo.Builder() - .setType(PostalInfo.Type.INTERNATIONALIZED) - .setAddress( - new ContactAddress.Builder() - .setStreet(ImmutableList.of("123 Avenida Grande")) - .build()) - .build()) - .setEmailAddress("bob@bob.com") - .setVoiceNumber(new ContactPhoneNumber.Builder().setPhoneNumber("555-1212").build()) - .setFaxNumber(new ContactPhoneNumber.Builder().setPhoneNumber("555-1212").build()) - .addStatusValue(PENDING_DELETE) - .build()); - } - - /** - * Helper method to check that one poll message exists with a given history entry, resource, - * client id, and message. Also checks that the only resulting async response matches the resource - * type, and has the appropriate actionResult, nameOrId, and Trid. - */ - private static void assertPollMessageFor( - HistoryEntry historyEntry, - String registrarId, - String msg, - boolean expectedActionResult, - EppResource resource, - Optional clientTrid) { - PollMessage.OneTime pollMessage = (OneTime) getOnlyPollMessageForHistoryEntry(historyEntry); - assertThat(pollMessage.getMsg()).isEqualTo(msg); - assertThat(pollMessage.getRegistrarId()).isEqualTo(registrarId); - - ImmutableList pollResponses = pollMessage.getResponseData(); - assertThat(pollResponses).hasSize(1); - ResponseData responseData = pollMessage.getResponseData().get(0); - - String expectedResourceName; - if (resource instanceof HostResource) { - assertThat(responseData).isInstanceOf(HostPendingActionNotificationResponse.class); - expectedResourceName = ((HostResource) resource).getHostName(); - } else { - assertThat(responseData).isInstanceOf(ContactPendingActionNotificationResponse.class); - expectedResourceName = ((ContactResource) resource).getContactId(); - } - PendingActionNotificationResponse pendingResponse = - (PendingActionNotificationResponse) responseData; - assertThat(pendingResponse.getActionResult()).isEqualTo(expectedActionResult); - assertThat(pendingResponse.getNameAsString()).isEqualTo(expectedResourceName); - Trid trid = pendingResponse.getTrid(); - assertThat(trid.getClientTransactionId()).isEqualTo(clientTrid); - assertThat(trid.getServerTransactionId()).isEqualTo("fakeServerTrid"); - } - - private static ContactResource persistContactPendingDelete(String contactId) { - return persistResource( - newContactResource(contactId).asBuilder().addStatusValue(PENDING_DELETE).build()); - } - - private static HostResource persistHostPendingDelete(String hostName) { - return persistResource( - newHostResource(hostName).asBuilder().addStatusValue(PENDING_DELETE).build()); - } - - private static DomainBase persistUsedDomain( - String domainName, ContactResource contact, HostResource host) { - return persistResource( - newDomainBase(domainName, contact) - .asBuilder() - .setNameservers(ImmutableSet.of(host.createVKey())) - .build()); - } - - private Optional acquireLock() { - return Lock.acquire( - DeleteContactsAndHostsAction.class.getSimpleName(), - null, - standardDays(30), - requestStatusChecker, - false); - } -} diff --git a/core/src/test/java/google/registry/flows/EppLifecycleContactTest.java b/core/src/test/java/google/registry/flows/EppLifecycleContactTest.java index 93346e74e..ee2340c37 100644 --- a/core/src/test/java/google/registry/flows/EppLifecycleContactTest.java +++ b/core/src/test/java/google/registry/flows/EppLifecycleContactTest.java @@ -16,13 +16,10 @@ package google.registry.flows; 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_ACTION_PENDING; 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 com.google.common.collect.ImmutableMap; -import google.registry.model.eppoutput.Result; import google.registry.testing.AppEngineExtension; import google.registry.testing.DualDatabaseTest; import google.registry.testing.TestOfyAndSql; @@ -65,22 +62,14 @@ class EppLifecycleContactTest extends EppTestCase { .hasCommandName("ContactInfo") .and() .hasStatus(SUCCESS); - Result.Code deleteResultCode; - if (tm().isOfy()) { - assertThatCommand("contact_delete_sh8013.xml") - .hasResponse("contact_delete_response_sh8013_pending.xml"); - deleteResultCode = SUCCESS_WITH_ACTION_PENDING; - } else { - assertThatCommand("contact_delete_sh8013.xml") - .hasResponse("contact_delete_response_sh8013.xml"); - deleteResultCode = SUCCESS; - } + assertThatCommand("contact_delete_sh8013.xml") + .hasResponse("contact_delete_response_sh8013.xml"); assertThat(getRecordedEppMetric()) .hasClientId("NewRegistrar") .and() .hasCommandName("ContactDelete") .and() - .hasStatus(deleteResultCode); + .hasStatus(SUCCESS); assertThatLogoutSucceeds(); } diff --git a/core/src/test/java/google/registry/flows/EppLifecycleHostTest.java b/core/src/test/java/google/registry/flows/EppLifecycleHostTest.java index 42987cf9a..e03e33ae8 100644 --- a/core/src/test/java/google/registry/flows/EppLifecycleHostTest.java +++ b/core/src/test/java/google/registry/flows/EppLifecycleHostTest.java @@ -17,8 +17,6 @@ package google.registry.flows; import static com.google.common.truth.Truth.assertThat; import static google.registry.model.EppResourceUtils.loadByForeignKey; 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.persistence.transaction.TransactionManagerFactory.tm; import static google.registry.testing.DatabaseHelper.createTld; import static google.registry.testing.DatabaseHelper.createTlds; import static google.registry.testing.EppMetricSubject.assertThat; @@ -26,7 +24,6 @@ import static google.registry.testing.HostResourceSubject.assertAboutHosts; import com.google.common.collect.ImmutableMap; import google.registry.model.domain.DomainBase; -import google.registry.model.eppoutput.Result; import google.registry.model.host.HostResource; import google.registry.testing.AppEngineExtension; import google.registry.testing.DualDatabaseTest; @@ -82,24 +79,15 @@ class EppLifecycleHostTest extends EppTestCase { .hasCommandName("HostInfo") .and() .hasStatus(SUCCESS); - Result.Code deleteResultCode; - if (tm().isOfy()) { - assertThatCommand("host_delete.xml", ImmutableMap.of("HOSTNAME", "ns1.example.tld")) - .atTime("2000-06-02T00:03:00Z") - .hasResponse("generic_success_action_pending_response.xml"); - deleteResultCode = SUCCESS_WITH_ACTION_PENDING; - } else { - assertThatCommand("host_delete.xml", ImmutableMap.of("HOSTNAME", "ns1.example.tld")) - .atTime("2000-06-02T00:03:00Z") - .hasResponse("generic_success_response.xml"); - deleteResultCode = SUCCESS; - } + assertThatCommand("host_delete.xml", ImmutableMap.of("HOSTNAME", "ns1.example.tld")) + .atTime("2000-06-02T00:03:00Z") + .hasResponse("generic_success_response.xml"); assertThat(getRecordedEppMetric()) .hasClientId("NewRegistrar") .and() .hasCommandName("HostDelete") .and() - .hasStatus(deleteResultCode); + .hasStatus(SUCCESS); assertThatLogoutSucceeds(); } diff --git a/core/src/test/java/google/registry/flows/EppPointInTimeTest.java b/core/src/test/java/google/registry/flows/EppPointInTimeTest.java index bc536334c..21eca5321 100644 --- a/core/src/test/java/google/registry/flows/EppPointInTimeTest.java +++ b/core/src/test/java/google/registry/flows/EppPointInTimeTest.java @@ -148,19 +148,12 @@ class EppPointInTimeTest { .isEqualExceptFields(domainAfterCreate, "updateTimestamp"); tm().clearSessionCache(); - if (tm().isOfy()) { - // Both updates happened on the same day. Since the revisions field has day granularity in - // Datastore, the key to the first update should have been overwritten by the second, and its - // timestamp rolled forward. So we have to fall back to the last revision before midnight. - assertThat(loadAtPointInTime(latest, timeAtFirstUpdate)).isEqualTo(domainAfterCreate); - } else { - // In SQL, however, we are not limited by the day granularity, so when we request the object - // at timeAtFirstUpdate we should receive the object at that first update, even though the - // second update occurred one millisecond later. - assertAboutImmutableObjects() - .that(loadAtPointInTime(latest, timeAtFirstUpdate)) - .isEqualExceptFields(domainAfterFirstUpdate, "updateTimestamp"); - } + // In SQL, we are not limited by the day granularity, so when we request the object + // at timeAtFirstUpdate we should receive the object at that first update, even though the + // second update occurred one millisecond later. + assertAboutImmutableObjects() + .that(loadAtPointInTime(latest, timeAtFirstUpdate)) + .isEqualExceptFields(domainAfterFirstUpdate, "updateTimestamp"); tm().clearSessionCache(); assertAboutImmutableObjects() diff --git a/core/src/test/java/google/registry/flows/ResourceFlowTestCase.java b/core/src/test/java/google/registry/flows/ResourceFlowTestCase.java index 44d628641..6ba415659 100644 --- a/core/src/test/java/google/registry/flows/ResourceFlowTestCase.java +++ b/core/src/test/java/google/registry/flows/ResourceFlowTestCase.java @@ -14,12 +14,9 @@ package google.registry.flows; -import static com.google.common.collect.ImmutableList.toImmutableList; -import static com.google.common.truth.Truth.assertThat; import static google.registry.batch.AsyncTaskEnqueuer.PARAM_RESOURCE_KEY; import static google.registry.model.EppResourceUtils.loadByForeignKey; import static google.registry.model.ImmutableObjectSubject.assertAboutImmutableObjects; -import static google.registry.model.ofy.ObjectifyService.auditedOfy; import static google.registry.persistence.transaction.TransactionManagerFactory.tm; import static google.registry.persistence.transaction.TransactionManagerUtil.transactIfJpaTm; import static google.registry.testing.LogsSubject.assertAboutLogs; @@ -28,9 +25,7 @@ import static google.registry.testing.TaskQueueHelper.assertTasksEnqueued; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.collect.Iterables; -import com.google.common.collect.Streams; import com.google.common.testing.TestLogHandler; -import com.googlecode.objectify.Key; import google.registry.model.EppResource; import google.registry.model.contact.ContactBase; import google.registry.model.contact.ContactHistory; @@ -41,8 +36,6 @@ import google.registry.model.eppinput.EppInput.ResourceCommandWrapper; import google.registry.model.eppinput.ResourceCommand; import google.registry.model.host.HostBase; import google.registry.model.host.HostHistory; -import google.registry.model.index.EppResourceIndex; -import google.registry.model.index.EppResourceIndexBucket; import google.registry.model.reporting.HistoryEntry; import google.registry.model.tmch.ClaimsList; import google.registry.model.tmch.ClaimsListDao; @@ -117,30 +110,6 @@ public abstract class ResourceFlowTestCase void assertEppResourceIndexEntityFor(final T resource) { - if (!tm().isOfy()) { - // Indices aren't explicitly stored as objects in SQL - return; - } - ImmutableList indices = - Streams.stream( - auditedOfy() - .load() - .type(EppResourceIndex.class) - .filter("kind", Key.getKind(resource.getClass()))) - .filter( - index -> - Key.create(resource).equals(index.getKey()) - && auditedOfy().load().key(index.getKey()).now().equals(resource)) - .collect(toImmutableList()); - assertThat(indices).hasSize(1); - assertThat(indices.get(0).getBucket()) - .isEqualTo(EppResourceIndexBucket.getBucketKey(Key.create(resource))); - } - /** Asserts the presence of a single enqueued async contact or host deletion */ protected void assertAsyncDeletionTaskEnqueued( T resource, String requestingClientId, Trid trid, boolean isSuperuser) { @@ -180,26 +149,24 @@ public abstract class ResourceFlowTestCase entity in SQL database(it is - // reconstructed by reading the information from Host table), we can not reconstruct the entity - // for the old host name because of the rename so we just skip the check for SQL database. - if (tm().isOfy()) { - // The old ForeignKeyIndex is invalidated at the time we did the rename. - ForeignKeyIndex oldFkiBeforeRename = - ForeignKeyIndex.load(HostResource.class, oldHostName(), clock.nowUtc().minusMillis(1)); - assertThat(oldFkiBeforeRename.getResourceKey()).isEqualTo(renamedHost.createVKey()); - assertThat(oldFkiBeforeRename.getDeletionTime()).isEqualTo(clock.nowUtc()); - } ForeignKeyIndex oldFkiAfterRename = ForeignKeyIndex.load(HostResource.class, oldHostName(), clock.nowUtc()); assertThat(oldFkiAfterRename).isNull(); diff --git a/core/src/test/java/google/registry/rdap/RdapDomainActionTest.java b/core/src/test/java/google/registry/rdap/RdapDomainActionTest.java index d6b402853..5f3b104be 100644 --- a/core/src/test/java/google/registry/rdap/RdapDomainActionTest.java +++ b/core/src/test/java/google/registry/rdap/RdapDomainActionTest.java @@ -40,10 +40,11 @@ import google.registry.rdap.RdapMetrics.SearchType; import google.registry.rdap.RdapMetrics.WildcardType; import google.registry.rdap.RdapSearchResults.IncompletenessWarningType; import google.registry.request.Action; +import google.registry.testing.DualDatabaseTest; +import google.registry.testing.TestOfyAndSql; import java.util.Optional; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Disabled; -import org.junit.jupiter.api.Test; /** * Unit tests for {@link RdapDomainAction}. @@ -51,6 +52,7 @@ import org.junit.jupiter.api.Test; *

TODO(b/26872828): The next time we do any work on RDAP, consider adding the APNIC RDAP * conformance checker to the unit test suite. */ +@DualDatabaseTest class RdapDomainActionTest extends RdapActionBaseTestCase { RdapDomainActionTest() { @@ -247,7 +249,7 @@ class RdapDomainActionTest extends RdapActionBaseTestCase { assertThat(response.getStatus()).isEqualTo(200); } - @Test + @TestOfyAndSql void testInvalidDomain_returns400() { assertThat(generateActualJson("invalid/domain/name")) .isEqualTo( @@ -258,7 +260,7 @@ class RdapDomainActionTest extends RdapActionBaseTestCase { assertThat(response.getStatus()).isEqualTo(400); } - @Test + @TestOfyAndSql void testUnknownDomain_returns400() { assertThat(generateActualJson("missingdomain.com")) .isEqualTo( @@ -269,45 +271,45 @@ class RdapDomainActionTest extends RdapActionBaseTestCase { assertThat(response.getStatus()).isEqualTo(400); } - @Test + @TestOfyAndSql void testValidDomain_works() { login("evilregistrar"); assertProperResponseForCatLol("cat.lol", "rdap_domain.json"); } - @Test + @TestOfyAndSql void testValidDomain_asAdministrator_works() { loginAsAdmin(); assertProperResponseForCatLol("cat.lol", "rdap_domain.json"); } - @Test + @TestOfyAndSql void testValidDomain_notLoggedIn_noContacts() { assertProperResponseForCatLol("cat.lol", "rdap_domain_no_contacts_with_remark.json"); } - @Test + @TestOfyAndSql void testValidDomain_loggedInAsOtherRegistrar_noContacts() { login("idnregistrar"); assertProperResponseForCatLol("cat.lol", "rdap_domain_no_contacts_with_remark.json"); } - @Test + @TestOfyAndSql void testUpperCase_ignored() { assertProperResponseForCatLol("CaT.lOl", "rdap_domain_no_contacts_with_remark.json"); } - @Test + @TestOfyAndSql void testTrailingDot_ignored() { assertProperResponseForCatLol("cat.lol.", "rdap_domain_no_contacts_with_remark.json"); } - @Test + @TestOfyAndSql void testQueryParameter_ignored() { assertProperResponseForCatLol("cat.lol?key=value", "rdap_domain_no_contacts_with_remark.json"); } - @Test + @TestOfyAndSql void testIdnDomain_works() { login("idnregistrar"); assertThat(generateActualJson("cat.みんな")) @@ -325,7 +327,7 @@ class RdapDomainActionTest extends RdapActionBaseTestCase { assertThat(response.getStatus()).isEqualTo(200); } - @Test + @TestOfyAndSql void testIdnDomainWithPercentEncoding_works() { login("idnregistrar"); assertThat(generateActualJson("cat.%E3%81%BF%E3%82%93%E3%81%AA")) @@ -343,7 +345,7 @@ class RdapDomainActionTest extends RdapActionBaseTestCase { assertThat(response.getStatus()).isEqualTo(200); } - @Test + @TestOfyAndSql void testPunycodeDomain_works() { login("idnregistrar"); assertThat(generateActualJson("cat.xn--q9jyb4c")) @@ -361,7 +363,7 @@ class RdapDomainActionTest extends RdapActionBaseTestCase { assertThat(response.getStatus()).isEqualTo(200); } - @Test + @TestOfyAndSql void testMultilevelDomain_works() { login("1tldregistrar"); assertThat(generateActualJson("cat.1.tld")) @@ -381,35 +383,35 @@ class RdapDomainActionTest extends RdapActionBaseTestCase { // todo (b/27378695): reenable or delete this test @Disabled - @Test + @TestOfyAndSql void testDomainInTestTld_notFound() { persistResource(Registry.get("lol").asBuilder().setTldType(Registry.TldType.TEST).build()); generateActualJson("cat.lol"); assertThat(response.getStatus()).isEqualTo(404); } - @Test + @TestOfyAndSql void testDeletedDomain_notFound() { assertThat(generateActualJson("dodo.lol")) .isEqualTo(generateExpectedJsonError("dodo.lol not found", 404)); assertThat(response.getStatus()).isEqualTo(404); } - @Test + @TestOfyAndSql void testDeletedDomain_notFound_includeDeletedSetFalse() { action.includeDeletedParam = Optional.of(true); generateActualJson("dodo.lol"); assertThat(response.getStatus()).isEqualTo(404); } - @Test + @TestOfyAndSql void testDeletedDomain_notFound_notLoggedIn() { action.includeDeletedParam = Optional.of(true); generateActualJson("dodo.lol"); assertThat(response.getStatus()).isEqualTo(404); } - @Test + @TestOfyAndSql void testDeletedDomain_notFound_loggedInAsDifferentRegistrar() { login("1tldregistrar"); action.includeDeletedParam = Optional.of(true); @@ -417,7 +419,7 @@ class RdapDomainActionTest extends RdapActionBaseTestCase { assertThat(response.getStatus()).isEqualTo(404); } - @Test + @TestOfyAndSql void testDeletedDomain_works_loggedInAsCorrectRegistrar() { login("evilregistrar"); action.includeDeletedParam = Optional.of(true); @@ -436,7 +438,7 @@ class RdapDomainActionTest extends RdapActionBaseTestCase { assertThat(response.getStatus()).isEqualTo(200); } - @Test + @TestOfyAndSql void testDeletedDomain_works_loggedInAsAdmin() { loginAsAdmin(); action.includeDeletedParam = Optional.of(true); @@ -455,7 +457,7 @@ class RdapDomainActionTest extends RdapActionBaseTestCase { assertThat(response.getStatus()).isEqualTo(200); } - @Test + @TestOfyAndSql void testMetrics() { generateActualJson("cat.lol"); verify(rdapMetrics) diff --git a/core/src/test/java/google/registry/tools/EppLifecycleToolsTest.java b/core/src/test/java/google/registry/tools/EppLifecycleToolsTest.java index 142b4c44e..bfc79537f 100644 --- a/core/src/test/java/google/registry/tools/EppLifecycleToolsTest.java +++ b/core/src/test/java/google/registry/tools/EppLifecycleToolsTest.java @@ -29,27 +29,33 @@ import google.registry.model.domain.DomainBase; import google.registry.model.domain.DomainHistory; import google.registry.model.reporting.HistoryEntry.Type; import google.registry.testing.AppEngineExtension; +import google.registry.testing.DualDatabaseTest; +import google.registry.testing.TestOfyAndSql; import google.registry.util.Clock; import java.util.List; import org.joda.money.Money; import org.joda.time.DateTime; import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; /** Tests for tools that affect EPP lifecycle. */ +@DualDatabaseTest class EppLifecycleToolsTest extends EppTestCase { @RegisterExtension final AppEngineExtension appEngine = - AppEngineExtension.builder().withDatastoreAndCloudSql().withTaskQueue().build(); + AppEngineExtension.builder() + .withClock(clock) + .withDatastoreAndCloudSql() + .withTaskQueue() + .build(); @BeforeEach void beforeEach() { createTlds("example", "tld"); } - @Test + @TestOfyAndSql void test_renewDomainThenUnrenew() throws Exception { assertThatLoginSucceeds("NewRegistrar", "foo-BAR2"); createContacts(DateTime.parse("2000-06-01T00:00:00Z")); diff --git a/core/src/test/java/google/registry/tools/javascrap/CreateSyntheticHistoryEntriesActionTest.java b/core/src/test/java/google/registry/tools/javascrap/CreateSyntheticHistoryEntriesActionTest.java deleted file mode 100644 index 0c6833829..000000000 --- a/core/src/test/java/google/registry/tools/javascrap/CreateSyntheticHistoryEntriesActionTest.java +++ /dev/null @@ -1,152 +0,0 @@ -// Copyright 2021 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.javascrap; - -import static com.google.common.truth.Truth.assertThat; -import static google.registry.persistence.transaction.TransactionManagerFactory.jpaTm; -import static google.registry.testing.DatabaseHelper.createTld; -import static google.registry.testing.DatabaseHelper.loadByKey; -import static google.registry.testing.DatabaseHelper.loadRegistrar; -import static google.registry.testing.DatabaseHelper.persistActiveDomain; -import static google.registry.testing.DatabaseHelper.persistActiveHost; -import static google.registry.testing.DatabaseHelper.persistDomainAsDeleted; -import static google.registry.testing.DatabaseHelper.persistDomainWithDependentResources; -import static google.registry.testing.DatabaseHelper.persistResource; -import static google.registry.util.DateTimeUtils.END_OF_TIME; -import static google.registry.util.DateTimeUtils.START_OF_TIME; - -import com.google.common.collect.ImmutableList; -import com.google.common.collect.Iterables; -import com.googlecode.objectify.Key; -import google.registry.model.EppResource; -import google.registry.model.contact.ContactResource; -import google.registry.model.domain.DomainBase; -import google.registry.model.domain.DomainHistory; -import google.registry.model.host.HostResource; -import google.registry.model.reporting.HistoryEntry; -import google.registry.model.reporting.HistoryEntryDao; -import google.registry.model.tld.Registry; -import google.registry.testing.FakeResponse; -import google.registry.testing.mapreduce.MapreduceTestCase; -import org.joda.time.DateTime; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; - -/** Tests for {@link CreateSyntheticHistoryEntriesAction}. */ -public class CreateSyntheticHistoryEntriesActionTest - extends MapreduceTestCase { - - private DomainBase domain; - private ContactResource contact; - - @BeforeEach - void beforeEach() { - action = - new CreateSyntheticHistoryEntriesAction( - makeDefaultRunner(), new FakeResponse(), "adminRegistrarId"); - - createTld("tld"); - domain = persistActiveDomain("example.tld"); - contact = loadByKey(domain.getAdminContact()); - } - - @Test - void testCreation_forAllTypes() throws Exception { - DomainBase domain2 = persistActiveDomain("exampletwo.tld"); - ContactResource contact2 = loadByKey(domain2.getAdminContact()); - HostResource host = persistActiveHost("ns1.foobar.tld"); - HostResource host2 = persistActiveHost("ns1.baz.tld"); - - assertThat(HistoryEntryDao.loadAllHistoryObjects(START_OF_TIME, END_OF_TIME)).isEmpty(); - runMapreduce(); - - for (EppResource resource : ImmutableList.of(contact, contact2, domain, domain2, host, host2)) { - HistoryEntry historyEntry = - Iterables.getOnlyElement( - HistoryEntryDao.loadHistoryObjectsForResource(resource.createVKey())); - assertThat(historyEntry.getParent()).isEqualTo(Key.create(resource)); - assertThat(historyEntry.getType()).isEqualTo(HistoryEntry.Type.SYNTHETIC); - } - assertThat(HistoryEntryDao.loadAllHistoryObjects(START_OF_TIME, END_OF_TIME)).hasSize(6); - } - - @Test - void testCreation_withPreviousHistoryEntry() throws Exception { - DateTime now = DateTime.parse("1999-04-03T22:00:00.0Z"); - DomainBase withHistoryEntry = - persistDomainWithDependentResources("foobar", "tld", contact, now, now, now.plusYears(1)); - assertThat( - Iterables.getOnlyElement( - HistoryEntryDao.loadHistoryObjectsForResource(withHistoryEntry.createVKey())) - .getType()) - .isEqualTo(HistoryEntry.Type.DOMAIN_CREATE); - - runMapreduce(); - - Iterable historyEntries = - HistoryEntryDao.loadHistoryObjectsForResource(withHistoryEntry.createVKey()); - assertThat(historyEntries).hasSize(2); - assertThat(Iterables.getLast(historyEntries).getType()).isEqualTo(HistoryEntry.Type.SYNTHETIC); - } - - @Test - void testDoesntSave_ifAlreadyReplayed() throws Exception { - DateTime now = DateTime.parse("1999-04-03T22:00:00.0Z"); - DomainHistory domainHistoryWithoutDomain = - persistResource( - new DomainHistory.Builder() - .setType(HistoryEntry.Type.DOMAIN_CREATE) - .setModificationTime(now) - .setDomain(domain) - .setRegistrarId(domain.getCreationRegistrarId()) - .build()); - DomainHistory domainHistoryWithDomain = - domainHistoryWithoutDomain.asBuilder().setDomain(domain).build(); - // Simulate having replayed the domain and history to SQL - jpaTm() - .transact( - () -> - jpaTm() - .putAll( - Registry.get("tld"), - loadRegistrar("TheRegistrar"), - contact, - domain, - domainHistoryWithDomain)); - runMapreduce(); - - // Since we already had a DomainHistory with the domain in SQL, we shouldn't create a synthetic - // history entry - assertThat(HistoryEntryDao.loadHistoryObjectsForResource(domain.createVKey())) - .containsExactly(domainHistoryWithoutDomain); - } - - @Test - void testCreation_forDeletedResource() throws Exception { - persistDomainAsDeleted(domain, domain.getCreationTime().plusMonths(6)); - runMapreduce(); - - assertThat( - Iterables.getOnlyElement( - HistoryEntryDao.loadHistoryObjectsForResource(domain.createVKey())) - .getType()) - .isEqualTo(HistoryEntry.Type.SYNTHETIC); - } - - private void runMapreduce() throws Exception { - action.run(); - executeTasksUntilEmpty("mapreduce"); - } -} diff --git a/core/src/test/java/google/registry/tools/javascrap/CreateSyntheticHistoryEntriesPipelineTest.java b/core/src/test/java/google/registry/tools/javascrap/CreateSyntheticHistoryEntriesPipelineTest.java deleted file mode 100644 index 432ca0ff0..000000000 --- a/core/src/test/java/google/registry/tools/javascrap/CreateSyntheticHistoryEntriesPipelineTest.java +++ /dev/null @@ -1,115 +0,0 @@ -// Copyright 2021 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.javascrap; - -import static com.google.common.truth.Truth.assertThat; -import static google.registry.model.ImmutableObjectSubject.assertAboutImmutableObjects; -import static google.registry.persistence.transaction.TransactionManagerFactory.jpaTm; -import static google.registry.persistence.transaction.TransactionManagerFactory.removeTmOverrideForTest; -import static google.registry.persistence.transaction.TransactionManagerFactory.setTmOverrideForTest; -import static google.registry.testing.DatabaseHelper.createTld; -import static google.registry.testing.DatabaseHelper.newDomainBase; -import static google.registry.testing.DatabaseHelper.persistActiveHost; -import static google.registry.testing.DatabaseHelper.persistNewRegistrar; -import static google.registry.testing.DatabaseHelper.persistSimpleResource; - -import com.google.common.collect.ImmutableList; -import google.registry.beam.TestPipelineExtension; -import google.registry.model.EppResource; -import google.registry.model.contact.ContactHistory; -import google.registry.model.contact.ContactResource; -import google.registry.model.domain.DomainBase; -import google.registry.model.domain.DomainHistory; -import google.registry.model.host.HostHistory; -import google.registry.model.host.HostResource; -import google.registry.model.reporting.HistoryEntry; -import google.registry.persistence.transaction.JpaTestExtensions; -import google.registry.persistence.transaction.JpaTestExtensions.JpaIntegrationTestExtension; -import google.registry.testing.DatastoreEntityExtension; -import google.registry.testing.FakeClock; -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.RegisterExtension; - -/** Unit tests for {@link CreateSyntheticHistoryEntriesPipeline}. */ -public class CreateSyntheticHistoryEntriesPipelineTest { - - FakeClock clock = new FakeClock(); - - @RegisterExtension - JpaIntegrationTestExtension jpaEextension = - new JpaTestExtensions.Builder().withClock(clock).buildIntegrationTestExtension(); - - @RegisterExtension - DatastoreEntityExtension datastoreEntityExtension = - new DatastoreEntityExtension().allThreads(true); - - @RegisterExtension TestPipelineExtension pipeline = TestPipelineExtension.create(); - - DomainBase domain; - ContactResource contact; - HostResource host; - - @BeforeEach - void beforeEach() { - setTmOverrideForTest(jpaTm()); - persistNewRegistrar("TheRegistrar"); - persistNewRegistrar("NewRegistrar"); - createTld("tld"); - host = persistActiveHost("external.com"); - domain = - persistSimpleResource( - newDomainBase("example.tld").asBuilder().setNameservers(host.createVKey()).build()); - contact = jpaTm().transact(() -> jpaTm().loadByKey(domain.getRegistrant())); - clock.advanceOneMilli(); - } - - @AfterEach - void afterEach() { - removeTmOverrideForTest(); - } - - @Test - void testSuccess() { - assertThat(jpaTm().transact(() -> jpaTm().loadAllOf(DomainHistory.class))).isEmpty(); - assertThat(jpaTm().transact(() -> jpaTm().loadAllOf(ContactHistory.class))).isEmpty(); - assertThat(jpaTm().transact(() -> jpaTm().loadAllOf(HostHistory.class))).isEmpty(); - CreateSyntheticHistoryEntriesPipeline.setup(pipeline, "NewRegistrar"); - pipeline.run().waitUntilFinish(); - validateHistoryEntry(DomainHistory.class, domain); - validateHistoryEntry(ContactHistory.class, contact); - validateHistoryEntry(HostHistory.class, host); - } - - private static void validateHistoryEntry( - Class historyClazz, T resource) { - ImmutableList historyEntries = - jpaTm().transact(() -> jpaTm().loadAllOf(historyClazz)); - assertThat(historyEntries).hasSize(1); - HistoryEntry historyEntry = historyEntries.get(0); - assertThat(historyEntry.getType()).isEqualTo(HistoryEntry.Type.SYNTHETIC); - assertThat(historyEntry.getRegistrarId()).isEqualTo("NewRegistrar"); - EppResource embeddedResource; - if (historyEntry instanceof DomainHistory) { - embeddedResource = ((DomainHistory) historyEntry).getDomainContent().get(); - } else if (historyEntry instanceof ContactHistory) { - embeddedResource = ((ContactHistory) historyEntry).getContactBase().get(); - } else { - embeddedResource = ((HostHistory) historyEntry).getHostBase().get(); - } - assertAboutImmutableObjects().that(embeddedResource).hasFieldsEqualTo(resource); - } -} diff --git a/core/src/test/java/google/registry/tools/server/VerifyOteActionTest.java b/core/src/test/java/google/registry/tools/server/VerifyOteActionTest.java index c49995601..b194f5f52 100644 --- a/core/src/test/java/google/registry/tools/server/VerifyOteActionTest.java +++ b/core/src/test/java/google/registry/tools/server/VerifyOteActionTest.java @@ -20,12 +20,14 @@ import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import google.registry.model.OteStatsTestHelper; import google.registry.testing.AppEngineExtension; +import google.registry.testing.DualDatabaseTest; +import google.registry.testing.TestOfyAndSql; import java.util.Map; import java.util.regex.Pattern; -import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; /** Unit tests for {@link VerifyOteAction}. */ +@DualDatabaseTest class VerifyOteActionTest { @RegisterExtension @@ -34,21 +36,21 @@ class VerifyOteActionTest { private final VerifyOteAction action = new VerifyOteAction(); - @Test + @TestOfyAndSql void testSuccess_summarize_allPass() throws Exception { OteStatsTestHelper.setupCompleteOte("blobio"); assertThat(getResponse(true)) .isEqualTo("# actions: 30 - Reqs: [----------------] 16/16 - Overall: PASS"); } - @Test + @TestOfyAndSql void testFailure_summarize_someFailures() throws Exception { OteStatsTestHelper.setupIncompleteOte("blobio"); assertThat(getResponse(true)) .isEqualTo("# actions: 34 - Reqs: [-.-----.------.-] 13/16 - Overall: FAIL"); } - @Test + @TestOfyAndSql void testSuccess_passNotSummarized() throws Exception { OteStatsTestHelper.setupCompleteOte("blobio"); String expectedOteStatus = @@ -78,7 +80,7 @@ class VerifyOteActionTest { assertThat(getResponse(false)).containsMatch(expectedOteStatusPattern); } - @Test + @TestOfyAndSql void testFailure_incomplete() throws Exception { OteStatsTestHelper.setupIncompleteOte("blobio"); String expectedOteStatus = diff --git a/core/src/test/java/google/registry/ui/server/registrar/OteStatusActionTest.java b/core/src/test/java/google/registry/ui/server/registrar/OteStatusActionTest.java index ea512cfff..f8afdbe3b 100644 --- a/core/src/test/java/google/registry/ui/server/registrar/OteStatusActionTest.java +++ b/core/src/test/java/google/registry/ui/server/registrar/OteStatusActionTest.java @@ -30,14 +30,16 @@ import google.registry.model.registrar.Registrar.Type; import google.registry.request.auth.AuthenticatedRegistrarAccessor; import google.registry.request.auth.AuthenticatedRegistrarAccessor.Role; import google.registry.testing.AppEngineExtension; +import google.registry.testing.DualDatabaseTest; +import google.registry.testing.TestOfyAndSql; import java.util.List; import java.util.Map; import java.util.function.Function; import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; /** Unit tests for {@link OteStatusAction} */ +@DualDatabaseTest public final class OteStatusActionTest { private static final String CLIENT_ID = "blobio-1"; @@ -57,7 +59,7 @@ public final class OteStatusActionTest { action.registrarAccessor = AuthenticatedRegistrarAccessor.createForTesting(authValues); } - @Test + @TestOfyAndSql @SuppressWarnings("unchecked") void testSuccess_finishedOte() throws Exception { OteStatsTestHelper.setupCompleteOte(BASE_CLIENT_ID); @@ -72,7 +74,7 @@ public final class OteStatusActionTest { assertThat(getFailingResultDetails(results)).isEmpty(); } - @Test + @TestOfyAndSql @SuppressWarnings("unchecked") void testSuccess_incomplete() throws Exception { OteStatsTestHelper.setupIncompleteOte(BASE_CLIENT_ID); @@ -103,7 +105,7 @@ public final class OteStatusActionTest { "completed", false)); } - @Test + @TestOfyAndSql void testFailure_malformedInput() { assertThat(action.handleJsonRequest(null)) .containsExactlyEntriesIn(errorResultWithMessage("Malformed JSON")); @@ -111,14 +113,14 @@ public final class OteStatusActionTest { .containsExactlyEntriesIn(errorResultWithMessage("Missing key for OT&E client: clientId")); } - @Test + @TestOfyAndSql void testFailure_registrarDoesntExist() { assertThat(action.handleJsonRequest(ImmutableMap.of("clientId", "nonexistent-3"))) .containsExactlyEntriesIn( errorResultWithMessage("Registrar nonexistent-3 does not exist")); } - @Test + @TestOfyAndSql void testFailure_notAuthorized() { persistNewRegistrar(CLIENT_ID, "blobio-1", Type.REAL, 1L); action.registrarAccessor = @@ -128,7 +130,7 @@ public final class OteStatusActionTest { errorResultWithMessage("TestUserId doesn't have access to registrar blobio-1")); } - @Test + @TestOfyAndSql void testFailure_malformedRegistrarName() { assertThat(action.handleJsonRequest(ImmutableMap.of("clientId", "badclient-id"))) .containsExactlyEntriesIn( @@ -136,7 +138,7 @@ public final class OteStatusActionTest { "ID badclient-id is not one of the OT&E registrar IDs for base badclient")); } - @Test + @TestOfyAndSql void testFailure_nonOteRegistrar() { persistNewRegistrar(CLIENT_ID, "SomeRegistrar", Type.REAL, 1L); assertThat(action.handleJsonRequest(ImmutableMap.of("clientId", CLIENT_ID))) diff --git a/core/src/test/java/google/registry/ui/server/registrar/RegistryLockVerifyActionTest.java b/core/src/test/java/google/registry/ui/server/registrar/RegistryLockVerifyActionTest.java index cfb8b6a69..83d56338e 100644 --- a/core/src/test/java/google/registry/ui/server/registrar/RegistryLockVerifyActionTest.java +++ b/core/src/test/java/google/registry/ui/server/registrar/RegistryLockVerifyActionTest.java @@ -49,8 +49,10 @@ import google.registry.testing.AppEngineExtension; import google.registry.testing.CloudTasksHelper; import google.registry.testing.DatabaseHelper; import google.registry.testing.DeterministicStringGenerator; +import google.registry.testing.DualDatabaseTest; import google.registry.testing.FakeClock; import google.registry.testing.FakeResponse; +import google.registry.testing.TestOfyAndSql; import google.registry.testing.UserInfo; import google.registry.tools.DomainLockUtils; import google.registry.util.StringGenerator; @@ -58,10 +60,10 @@ import google.registry.util.StringGenerator.Alphabets; import javax.servlet.http.HttpServletRequest; import org.joda.time.Duration; import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; /** Unit tests for {@link RegistryLockVerifyAction}. */ +@DualDatabaseTest final class RegistryLockVerifyActionTest { private final FakeClock fakeClock = new FakeClock(); @@ -96,7 +98,7 @@ final class RegistryLockVerifyActionTest { action = createAction(lockId, true); } - @Test + @TestOfyAndSql void testSuccess_lockDomain() { saveRegistryLock(createLock()); action.run(); @@ -112,7 +114,7 @@ final class RegistryLockVerifyActionTest { assertBillingEvent(historyEntry); } - @Test + @TestOfyAndSql void testSuccess_unlockDomain() { action = createAction(lockId, false); domain = persistResource(domain.asBuilder().setStatusValues(REGISTRY_LOCK_STATUSES).build()); @@ -130,7 +132,7 @@ final class RegistryLockVerifyActionTest { assertBillingEvent(historyEntry); } - @Test + @TestOfyAndSql void testSuccess_adminLock_createsOnlyHistoryEntry() { action.authResult = AuthResult.create(AuthLevel.USER, UserAuthInfo.create(user, true)); saveRegistryLock(createLock().asBuilder().isSuperuser(true).build()); @@ -142,7 +144,7 @@ final class RegistryLockVerifyActionTest { DatabaseHelper.assertNoBillingEvents(); } - @Test + @TestOfyAndSql void testFailure_badVerificationCode() { saveRegistryLock( createLock().asBuilder().setVerificationCode("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA").build()); @@ -151,7 +153,7 @@ final class RegistryLockVerifyActionTest { assertNoDomainChanges(); } - @Test + @TestOfyAndSql void testFailure_alreadyVerified() { saveRegistryLock(createLock().asBuilder().setLockCompletionTime(fakeClock.nowUtc()).build()); action.run(); @@ -159,7 +161,7 @@ final class RegistryLockVerifyActionTest { assertNoDomainChanges(); } - @Test + @TestOfyAndSql void testFailure_expired() { saveRegistryLock(createLock()); fakeClock.advanceBy(Duration.standardHours(2)); @@ -169,7 +171,7 @@ final class RegistryLockVerifyActionTest { assertNoDomainChanges(); } - @Test + @TestOfyAndSql void testFailure_nonAdmin_verifyingAdminLock() { saveRegistryLock(createLock().asBuilder().isSuperuser(true).build()); action.run(); @@ -177,7 +179,7 @@ final class RegistryLockVerifyActionTest { assertNoDomainChanges(); } - @Test + @TestOfyAndSql void testFailure_alreadyUnlocked() { action = createAction(lockId, false); saveRegistryLock( @@ -192,7 +194,7 @@ final class RegistryLockVerifyActionTest { assertNoDomainChanges(); } - @Test + @TestOfyAndSql void testFailure_alreadyLocked() { saveRegistryLock(createLock()); domain = persistResource(domain.asBuilder().setStatusValues(REGISTRY_LOCK_STATUSES).build()); @@ -201,7 +203,7 @@ final class RegistryLockVerifyActionTest { assertNoDomainChanges(); } - @Test + @TestOfyAndSql void testFailure_notLoggedIn() { action.authResult = AuthResult.NOT_AUTHENTICATED; action.run(); @@ -210,7 +212,7 @@ final class RegistryLockVerifyActionTest { assertNoDomainChanges(); } - @Test + @TestOfyAndSql void testFailure_doesNotChangeLockObject() { // A failure when performing Datastore actions means that no actions should be taken in the // Cloud SQL RegistryLock object @@ -229,7 +231,7 @@ final class RegistryLockVerifyActionTest { assertThat(afterAction).isEqualTo(lock); } - @Test + @TestOfyAndSql void testFailure_isLockTrue_shouldBeFalse() { domain = persistResource(domain.asBuilder().setStatusValues(REGISTRY_LOCK_STATUSES).build()); saveRegistryLock( @@ -242,7 +244,7 @@ final class RegistryLockVerifyActionTest { assertThat(response.getPayload()).contains("Failed: Domain example.tld is already locked"); } - @Test + @TestOfyAndSql void testFailure_isLockFalse_shouldBeTrue() { action = createAction(lockId, false); saveRegistryLock(createLock()); @@ -250,7 +252,7 @@ final class RegistryLockVerifyActionTest { assertThat(response.getPayload()).contains("Failed: Domain example.tld is already unlocked"); } - @Test + @TestOfyAndSql void testFailure_lock_unlock_lockAgain() { RegistryLock lock = saveRegistryLock(createLock()); action.run(); @@ -269,7 +271,7 @@ final class RegistryLockVerifyActionTest { assertThat(response.getPayload()).contains("Failed: Invalid verification code"); } - @Test + @TestOfyAndSql void testFailure_lock_lockAgain() { saveRegistryLock(createLock()); action.run(); @@ -279,7 +281,7 @@ final class RegistryLockVerifyActionTest { assertThat(response.getPayload()).contains("Failed: Domain example.tld is already locked"); } - @Test + @TestOfyAndSql void testFailure_unlock_unlockAgain() { action = createAction(lockId, false); domain = persistResource(domain.asBuilder().setStatusValues(REGISTRY_LOCK_STATUSES).build()); diff --git a/core/src/test/java/google/registry/webdriver/TestServerExtension.java b/core/src/test/java/google/registry/webdriver/TestServerExtension.java index 3851b2a22..80b51d0df 100644 --- a/core/src/test/java/google/registry/webdriver/TestServerExtension.java +++ b/core/src/test/java/google/registry/webdriver/TestServerExtension.java @@ -24,11 +24,14 @@ import static google.registry.util.PreconditionsUtils.checkArgumentNotNull; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.net.HostAndPort; +import google.registry.persistence.transaction.JpaTestExtensions; import google.registry.request.auth.AuthenticatedRegistrarAccessor; import google.registry.server.Fixture; import google.registry.server.Route; import google.registry.server.TestServer; import google.registry.testing.AppEngineExtension; +import google.registry.testing.DatabaseHelper; +import google.registry.testing.FakeClock; import google.registry.testing.UserInfo; import java.net.URL; import java.net.UnknownHostException; @@ -40,6 +43,8 @@ import java.util.concurrent.Callable; import java.util.concurrent.FutureTask; import java.util.concurrent.LinkedBlockingDeque; import javax.servlet.Filter; +import org.joda.time.DateTime; +import org.joda.time.DateTimeZone; import org.junit.jupiter.api.extension.AfterEachCallback; import org.junit.jupiter.api.extension.BeforeEachCallback; import org.junit.jupiter.api.extension.ExtensionContext; @@ -202,7 +207,15 @@ public final class TestServerExtension implements BeforeEachCallback, AfterEachC } } - void runInner() throws InterruptedException { + void runInner() throws Exception { + // Clear the SQL database and set it as primary (we have to do this out of band because the + // AppEngineExtension can't natively do it for us yet due to remaining ofy dependencies) + new JpaTestExtensions.Builder().buildIntegrationTestExtension().beforeEach(null); + DatabaseHelper.setMigrationScheduleToSqlPrimary( + new FakeClock(DateTime.now(DateTimeZone.UTC))); + // sleep a few millis to make sure we get to SQL-primary mode + Thread.sleep(4); + AppEngineExtension.loadInitialData(); for (Fixture fixture : fixtures) { fixture.load(); } diff --git a/core/src/test/resources/google/registry/module/backend/backend_routing.txt b/core/src/test/resources/google/registry/module/backend/backend_routing.txt index 41b3ce86d..ef6f90fa9 100644 --- a/core/src/test/resources/google/registry/module/backend/backend_routing.txt +++ b/core/src/test/resources/google/registry/module/backend/backend_routing.txt @@ -9,8 +9,6 @@ PATH CLASS /_dr/task/brdaCopy BrdaCopyAction POST y INTERNAL,API APP ADMIN /_dr/task/checkDatastoreBackup CheckBackupAction POST,GET y INTERNAL,API APP ADMIN /_dr/task/copyDetailReports CopyDetailReportsAction POST n INTERNAL,API APP ADMIN -/_dr/task/createSyntheticHistoryEntries CreateSyntheticHistoryEntriesAction GET n INTERNAL,API APP ADMIN -/_dr/task/deleteContactsAndHosts DeleteContactsAndHostsAction GET n INTERNAL,API APP ADMIN /_dr/task/deleteExpiredDomains DeleteExpiredDomainsAction GET n INTERNAL,API APP ADMIN /_dr/task/deleteLoadTestData DeleteLoadTestDataAction POST n INTERNAL,API APP ADMIN /_dr/task/deleteOldCommitLogs DeleteOldCommitLogsAction GET n INTERNAL,API APP ADMIN