Remove Ofy code from various flow-related classes (#1653)

This included removing ofy-specific code from various tests. Also, some
of the other tests (e.g. RdapDomainActionTest) had to be configured to
use only SQL -- otherwise, as it currently stands, they were trying to
use ofy.

We also delete the CreateSyntheticHistoryEntriesAction and pipeline
because they're no longer relevant, and impossible to test (the goal of
the actions were to create objects in ofy which doesn't happen any
more).
This commit is contained in:
gbrodman 2022-06-07 11:43:33 -04:00 committed by GitHub
parent fad5d19110
commit 4c61df65b2
34 changed files with 200 additions and 2680 deletions

View file

@ -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 \

View file

@ -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 =
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<TaskHandle> 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<String> kindCounts = HashMultiset.create(2);
ImmutableList.Builder<DeletionRequest> builder = new ImmutableList.Builder<>();
ImmutableList.Builder<Key<? extends EppResource>> resourceKeys = new ImmutableList.Builder<>();
final List<DeletionRequest> 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<DeletionRequest> 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<DeletionRequest> deletionRequests) {
if (deletionRequests.isEmpty()) {
return;
}
final List<TaskHandle> 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<DeletionRequest> deletionRequests, Optional<Lock> 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<Void>(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> lock) {
logger.at(level).log(message);
response.setPayload(message);
lock.ifPresent(Lock::release);
}
/**
* A mapper that iterates over all {@link DomainBase} entities.
*
* <p>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<DomainBase, DeletionRequest, Boolean> {
private static final long serialVersionUID = -253652818502690537L;
private final ImmutableList<DeletionRequest> deletionRequests;
DeleteContactsAndHostsMapper(ImmutableList<DeletionRequest> 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<? extends EppResource> resourceKey) {
if (resourceKey.getKind().equals(KIND_CONTACT)) {
return domain
.getReferencedContacts()
.contains(VKey.from((Key<ContactResource>) resourceKey));
} else if (resourceKey.getKind().equals(KIND_HOST)) {
return domain.getNameservers().contains(VKey.from((Key<HostResource>) 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<DeletionRequest, Boolean, Void> {
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<Boolean> 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()
.<ImmutableObject>entities(resourceToSave, historyEntry.asHistoryEntry(), pollMessage);
return DeletionResult.create(
deleteAllowed ? Type.DELETED : Type.NOT_DELETED, pollMessageText);
}
private static ImmutableList<? extends ResponseData> 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<? extends EppResource> 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<? extends EppResource> 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<String, String> params = ImmutableMap.copyOf(task.extractParams());
VKey<EppResource> 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<EppResource> 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;
}
}

View file

@ -334,15 +334,6 @@
<url-pattern>/_dr/task/resaveEntity</url-pattern>
</servlet-mapping>
<!--
Deletes contacts and hosts enqueued for asynchronous deletion if they are
not referenced by any domain.
-->
<servlet-mapping>
<servlet-name>backend-servlet</servlet-name>
<url-pattern>/_dr/task/deleteContactsAndHosts</url-pattern>
</servlet-mapping>
<!-- Enqueues DNS update tasks following a host rename. -->
<servlet-mapping>
<servlet-name>backend-servlet</servlet-name>
@ -422,12 +413,6 @@ have been in the database for a certain period of time. -->
<url-pattern>/_dr/task/wipeOutDatastore</url-pattern>
</servlet-mapping>
<!-- Action to create synthetic history entries during async replication to SQL -->
<servlet-mapping>
<servlet-name>backend-servlet</servlet-name>
<url-pattern>/_dr/task/createSyntheticHistoryEntries</url-pattern>
</servlet-mapping>
<!-- Action to sync Datastore to a snapshot of the primary SQL database. -->
<servlet-mapping>
<servlet-name>backend-servlet</servlet-name>

View file

@ -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 <R extends EppResource> void checkLinkedDomains(
final String targetId,
final DateTime now,
final Class<R> resourceClass,
final Function<DomainBase, ImmutableSet<?>> getPotentialReferences)
throws EppException {
final String targetId, final DateTime now, final Class<R> resourceClass) throws EppException {
EppException failfastException =
tm().isOfy()
? tm().doTransactionless(
() -> {
final ForeignKeyIndex<R> 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<VKey<DomainBase>> keys =
getLinkedDomainKeys(fki.getResourceKey(), now, FAILFAST_CHECK_COUNT);
VKey<R> resourceVKey = fki.getResourceKey();
Predicate<DomainBase> predicate =
domain -> getPotentialReferences.apply(domain).contains(resourceVKey);
return tm().loadByKeys(keys).values().stream().anyMatch(predicate)
? new ResourceToDeleteIsReferencedException()
: null;
})
: tm().transact(
() -> {
final ForeignKeyIndex<R> fki =
ForeignKeyIndex.load(resourceClass, targetId, now);
if (fki == null) {
return new ResourceDoesNotExistException(resourceClass, targetId);
}
return isLinked(fki.getResourceKey(), now)
? new ResourceToDeleteIsReferencedException()
: null;
});
tm().transact(
() -> {
final ForeignKeyIndex<R> fki = ForeignKeyIndex.load(resourceClass, targetId, now);
if (fki == null) {
return new ResourceDoesNotExistException(resourceClass, targetId);
}
return isLinked(fki.getResourceKey(), now)
? new ResourceToDeleteIsReferencedException()
: null;
});
if (failfastException != null) {
throw failfastException;
}

View file

@ -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();
}
}

View file

@ -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<? extends HistoryEntry> 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. */

View file

@ -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 =

View file

@ -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();
}
}

View file

@ -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();
}

View file

@ -47,13 +47,12 @@ public final class PollFlowUtils {
* <p>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;
}
/**

View file

@ -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<HistoryEntry> 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<HistoryEntry>()
.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<HistoryEntry>()
.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<HistoryEntry> loadHistoryObjectsForResource(
VKey<? extends EppResource> 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<HistoryEntry> loadHistoryObjectsByRegistrars(
ImmutableCollection<String> 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 <T extends HistoryEntry> Stream<T> loadHistoryObjectFromSqlByRegistrars(

View file

@ -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();

View file

@ -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.
*
* <p>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.
*
* <p>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.
*
* <p>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.
*
* <p>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<? extends HistoryEntry> 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<? extends HistoryEntry> getHistoryClassFromParent(
Class<? extends EppResource> 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<? extends HistoryEntry> 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<Key<EppResource>, Void, Void> {
private final String registryAdminRegistrarId;
public CreateSyntheticHistoryEntriesMapper(String registryAdminRegistrarId) {
this.registryAdminRegistrarId = registryAdminRegistrarId;
}
@Override
public final void map(final Key<EppResource> 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()));
}
}
}
}

View file

@ -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.
*
* <p>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.
*
* <p>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.
*
* <p>To run the pipeline:
*
* <p><code>
* $ ./nom_build :core:cSHE --args="--region=us-central1
* --runner=DataflowRunner
* --registryEnvironment=CRASH
* --project={project-id}
* --workerMachineType=n2-standard-4"
* </code>
*
* @see google.registry.tools.javascrap.CreateSyntheticHistoryEntriesAction
*/
public class CreateSyntheticHistoryEntriesPipeline implements Serializable {
private static final ImmutableList<Class<? extends EppResource>> 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<? extends EppResource> 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<? extends EppResource> 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();
}
}

View file

@ -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<DeleteContactsAndHostsAction> {
@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<String> 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<String> 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.<EppResource>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.<EppResource>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.<EppResource>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<String> clientTrid) {
PollMessage.OneTime pollMessage = (OneTime) getOnlyPollMessageForHistoryEntry(historyEntry);
assertThat(pollMessage.getMsg()).isEqualTo(msg);
assertThat(pollMessage.getRegistrarId()).isEqualTo(registrarId);
ImmutableList<ResponseData> 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<Lock> acquireLock() {
return Lock.acquire(
DeleteContactsAndHostsAction.class.getSimpleName(),
null,
standardDays(30),
requestStatusChecker,
false);
}
}

View file

@ -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();
}

View file

@ -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();
}

View file

@ -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()

View file

@ -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<F extends Flow, R extends EppResource
ClaimsListDao.save(ClaimsList.create(clock.nowUtc(), labelsToKeys));
}
/**
* Confirms that an EppResourceIndex entity exists in Datastore for a given resource.
*/
protected static <T extends EppResource> void assertEppResourceIndexEntityFor(final T resource) {
if (!tm().isOfy()) {
// Indices aren't explicitly stored as objects in SQL
return;
}
ImmutableList<EppResourceIndex> 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 <T extends EppResource> void assertAsyncDeletionTaskEnqueued(
T resource, String requestingClientId, Trid trid, boolean isSuperuser) {
@ -180,26 +149,24 @@ public abstract class ResourceFlowTestCase<F extends Flow, R extends EppResource
}
protected void assertLastHistoryContainsResource(EppResource resource) {
if (!tm().isOfy()) {
HistoryEntry historyEntry = Iterables.getLast(DatabaseHelper.getHistoryEntries(resource));
if (resource instanceof ContactBase) {
ContactHistory contactHistory = (ContactHistory) historyEntry;
// Don't use direct equals comparison since one might be a subclass of the other
assertAboutImmutableObjects()
.that(contactHistory.getContactBase().get())
.hasFieldsEqualTo(resource);
} else if (resource instanceof DomainContent) {
DomainHistory domainHistory = (DomainHistory) historyEntry;
assertAboutImmutableObjects()
.that(domainHistory.getDomainContent().get())
.isEqualExceptFields(resource, "gracePeriods", "dsData", "nsHosts");
} else if (resource instanceof HostBase) {
HostHistory hostHistory = (HostHistory) historyEntry;
// Don't use direct equals comparison since one might be a subclass of the other
assertAboutImmutableObjects()
.that(hostHistory.getHostBase().get())
.hasFieldsEqualTo(resource);
}
HistoryEntry historyEntry = Iterables.getLast(DatabaseHelper.getHistoryEntries(resource));
if (resource instanceof ContactBase) {
ContactHistory contactHistory = (ContactHistory) historyEntry;
// Don't use direct equals comparison since one might be a subclass of the other
assertAboutImmutableObjects()
.that(contactHistory.getContactBase().get())
.hasFieldsEqualTo(resource);
} else if (resource instanceof DomainContent) {
DomainHistory domainHistory = (DomainHistory) historyEntry;
assertAboutImmutableObjects()
.that(domainHistory.getDomainContent().get())
.isEqualExceptFields(resource, "gracePeriods", "dsData", "nsHosts");
} else if (resource instanceof HostBase) {
HostHistory hostHistory = (HostHistory) historyEntry;
// Don't use direct equals comparison since one might be a subclass of the other
assertAboutImmutableObjects()
.that(hostHistory.getHostBase().get())
.hasFieldsEqualTo(resource);
}
}
}

View file

@ -15,7 +15,6 @@
package google.registry.flows.contact;
import static com.google.common.truth.Truth.assertThat;
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
import static google.registry.testing.ContactResourceSubject.assertAboutContacts;
import static google.registry.testing.DatabaseHelper.assertNoBillingEvents;
import static google.registry.testing.DatabaseHelper.newContactResource;
@ -63,9 +62,6 @@ class ContactCreateFlowTest extends ResourceFlowTestCase<ContactCreateFlow, Cont
ContactResource contact = reloadResourceByForeignKey();
assertAboutContacts().that(contact).hasOnlyOneHistoryEntryWhich().hasNoXml();
assertNoBillingEvents();
if (tm().isOfy()) {
assertEppResourceIndexEntityFor(contact);
}
assertLastHistoryContainsResource(contact);
}

View file

@ -17,7 +17,6 @@ package google.registry.flows.contact;
import static com.google.common.collect.MoreCollectors.onlyElement;
import static com.google.common.truth.Truth.assertThat;
import static google.registry.batch.AsyncTaskEnqueuer.QUEUE_ASYNC_DELETE;
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
import static google.registry.testing.ContactResourceSubject.assertAboutContacts;
import static google.registry.testing.DatabaseHelper.assertNoBillingEvents;
import static google.registry.testing.DatabaseHelper.createTld;
@ -87,11 +86,7 @@ class ContactDeleteFlowTest extends ResourceFlowTestCase<ContactDeleteFlow, Cont
@TestOfyAndSql
void testDryRun() throws Exception {
persistActiveContact(getUniqueIdFromCommand());
if (tm().isOfy()) {
dryRunFlowAssertResponse(loadFile("contact_delete_response_pending.xml"));
} else {
dryRunFlowAssertResponse(loadFile("contact_delete_response.xml"));
}
dryRunFlowAssertResponse(loadFile("contact_delete_response.xml"));
}
@TestOfyAndSql
@ -99,13 +94,8 @@ class ContactDeleteFlowTest extends ResourceFlowTestCase<ContactDeleteFlow, Cont
persistActiveContact(getUniqueIdFromCommand());
clock.advanceOneMilli();
assertTransactionalFlow(true);
if (tm().isOfy()) {
runFlowAssertResponse(loadFile("contact_delete_response_pending.xml"));
assertOfyDeleteSuccess();
} else {
runFlowAssertResponse(loadFile("contact_delete_response.xml"));
assertSqlDeleteSuccess();
}
runFlowAssertResponse(loadFile("contact_delete_response.xml"));
assertSqlDeleteSuccess();
}
@TestSqlOnly
@ -156,13 +146,8 @@ class ContactDeleteFlowTest extends ResourceFlowTestCase<ContactDeleteFlow, Cont
persistActiveContact(getUniqueIdFromCommand());
clock.advanceOneMilli();
assertTransactionalFlow(true);
if (tm().isOfy()) {
runFlowAssertResponse(loadFile("contact_delete_response_no_cltrid_pending.xml"));
assertOfyDeleteSuccess("TheRegistrar", null, false);
} else {
runFlowAssertResponse(loadFile("contact_delete_response_no_cltrid.xml"));
assertSqlDeleteSuccess();
}
runFlowAssertResponse(loadFile("contact_delete_response_no_cltrid.xml"));
assertSqlDeleteSuccess();
}
@TestOfyAndSql
@ -225,17 +210,9 @@ class ContactDeleteFlowTest extends ResourceFlowTestCase<ContactDeleteFlow, Cont
sessionMetadata.setRegistrarId("NewRegistrar");
persistActiveContact(getUniqueIdFromCommand());
clock.advanceOneMilli();
if (tm().isOfy()) {
runFlowAssertResponse(
CommitMode.LIVE,
UserPrivileges.SUPERUSER,
loadFile("contact_delete_response_pending.xml"));
assertOfyDeleteSuccess("NewRegistrar", "ABC-12345", true);
} else {
runFlowAssertResponse(
CommitMode.LIVE, UserPrivileges.SUPERUSER, loadFile("contact_delete_response.xml"));
assertSqlDeleteSuccess();
}
runFlowAssertResponse(
CommitMode.LIVE, UserPrivileges.SUPERUSER, loadFile("contact_delete_response.xml"));
assertSqlDeleteSuccess();
}
@TestOfyAndSql
@ -272,25 +249,6 @@ class ContactDeleteFlowTest extends ResourceFlowTestCase<ContactDeleteFlow, Cont
DatabaseHelper.removeDatabaseMigrationSchedule();
}
private void assertOfyDeleteSuccess(String registrarId, String clientTrid, boolean isSuperuser)
throws Exception {
ContactResource deletedContact = reloadResourceByForeignKey();
assertAsyncDeletionTaskEnqueued(
deletedContact, registrarId, Trid.create(clientTrid, "server-trid"), isSuperuser);
assertAboutContacts()
.that(deletedContact)
.hasStatusValue(StatusValue.PENDING_DELETE)
.and()
.hasOnlyOneHistoryEntryWhich()
.hasType(Type.CONTACT_PENDING_DELETE);
assertNoBillingEvents();
assertLastHistoryContainsResource(deletedContact);
}
private void assertOfyDeleteSuccess() throws Exception {
assertOfyDeleteSuccess("TheRegistrar", "ABC-12345", false);
}
private void assertSqlDeleteSuccess(HistoryEntry.Type... historyEntryTypes) throws Exception {
assertThat(reloadResourceByForeignKey()).isNull();
assertAboutContacts()

View file

@ -368,7 +368,6 @@ class DomainCreateFlowTest extends ResourceFlowTestCase<DomainCreateFlow, Domain
GracePeriodStatus.ADD, domain.getRepoId(), billingTime, "TheRegistrar", null),
createBillingEvent));
assertDnsTasksEnqueued(getUniqueIdFromCommand());
assertEppResourceIndexEntityFor(domain);
replayExtension.expectUpdateFor(domain);

View file

@ -16,7 +16,6 @@ package google.registry.flows.host;
import static com.google.common.truth.Truth.assertThat;
import static google.registry.model.EppResourceUtils.loadByForeignKey;
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
import static google.registry.testing.DatabaseHelper.assertNoBillingEvents;
import static google.registry.testing.DatabaseHelper.createTld;
import static google.registry.testing.DatabaseHelper.createTlds;
@ -105,9 +104,6 @@ class HostCreateFlowTest extends ResourceFlowTestCase<HostCreateFlow, HostResour
.hasOnlyOneHistoryEntryWhich()
.hasType(HistoryEntry.Type.HOST_CREATE);
assertNoBillingEvents();
if (tm().isOfy()) {
assertEppResourceIndexEntityFor(host);
}
assertLastHistoryContainsResource(host);
}

View file

@ -16,7 +16,6 @@ package google.registry.flows.host;
import static com.google.common.truth.Truth.assertThat;
import static google.registry.batch.AsyncTaskEnqueuer.QUEUE_ASYNC_DELETE;
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
import static google.registry.testing.DatabaseHelper.assertNoBillingEvents;
import static google.registry.testing.DatabaseHelper.createTld;
import static google.registry.testing.DatabaseHelper.loadByKey;
@ -47,7 +46,6 @@ import google.registry.flows.host.HostFlowUtils.HostNameNotNormalizedException;
import google.registry.flows.host.HostFlowUtils.HostNameNotPunyCodedException;
import google.registry.model.domain.DomainBase;
import google.registry.model.eppcommon.StatusValue;
import google.registry.model.eppcommon.Trid;
import google.registry.model.host.HostResource;
import google.registry.model.reporting.HistoryEntry.Type;
import google.registry.model.tld.Registry;
@ -86,11 +84,7 @@ class HostDeleteFlowTest extends ResourceFlowTestCase<HostDeleteFlow, HostResour
@TestOfyAndSql
void testDryRun() throws Exception {
persistActiveHost("ns1.example.tld");
if (tm().isOfy()) {
dryRunFlowAssertResponse(loadFile("host_delete_response_pending.xml"));
} else {
dryRunFlowAssertResponse(loadFile("host_delete_response.xml"));
}
dryRunFlowAssertResponse(loadFile("host_delete_response.xml"));
}
@TestOfyAndSql
@ -98,13 +92,8 @@ class HostDeleteFlowTest extends ResourceFlowTestCase<HostDeleteFlow, HostResour
persistActiveHost("ns1.example.tld");
clock.advanceOneMilli();
assertTransactionalFlow(true);
if (tm().isOfy()) {
runFlowAssertResponse(loadFile("host_delete_response_pending.xml"));
assertOfyDeleteSuccess();
} else {
runFlowAssertResponse(loadFile("host_delete_response.xml"));
assertSqlDeleteSuccess();
}
runFlowAssertResponse(loadFile("host_delete_response.xml"));
assertSqlDeleteSuccess();
}
@TestOfyAndSql
@ -113,13 +102,8 @@ class HostDeleteFlowTest extends ResourceFlowTestCase<HostDeleteFlow, HostResour
persistActiveHost("ns1.example.tld");
clock.advanceOneMilli();
assertTransactionalFlow(true);
if (tm().isOfy()) {
runFlowAssertResponse(loadFile("host_delete_response_no_cltrid_pending.xml"));
assertOfyDeleteSuccess("TheRegistrar", null, false);
} else {
runFlowAssertResponse(loadFile("host_delete_response_no_cltrid.xml"));
assertSqlDeleteSuccess();
}
runFlowAssertResponse(loadFile("host_delete_response_no_cltrid.xml"));
assertSqlDeleteSuccess();
}
@TestOfyAndSql
@ -178,15 +162,9 @@ class HostDeleteFlowTest extends ResourceFlowTestCase<HostDeleteFlow, HostResour
sessionMetadata.setRegistrarId("NewRegistrar");
persistActiveHost("ns1.example.tld");
clock.advanceOneMilli();
if (tm().isOfy()) {
runFlowAssertResponse(
CommitMode.LIVE, UserPrivileges.SUPERUSER, loadFile("host_delete_response_pending.xml"));
assertOfyDeleteSuccess("NewRegistrar", "ABC-12345", true);
} else {
runFlowAssertResponse(
CommitMode.LIVE, UserPrivileges.SUPERUSER, loadFile("host_delete_response.xml"));
assertSqlDeleteSuccess();
}
runFlowAssertResponse(
CommitMode.LIVE, UserPrivileges.SUPERUSER, loadFile("host_delete_response.xml"));
assertSqlDeleteSuccess();
}
@TestOfyAndSql
@ -206,13 +184,8 @@ class HostDeleteFlowTest extends ResourceFlowTestCase<HostDeleteFlow, HostResour
.setSuperordinateDomain(domain.createVKey())
.build());
clock.advanceOneMilli();
if (tm().isOfy()) {
runFlowAssertResponse(loadFile("host_delete_response_pending.xml"));
assertOfyDeleteSuccess();
} else {
runFlowAssertResponse(loadFile("host_delete_response.xml"));
assertSqlDeleteSuccess(true);
}
runFlowAssertResponse(loadFile("host_delete_response.xml"));
assertSqlDeleteSuccess(true);
}
@TestOfyAndSql
@ -265,13 +238,8 @@ class HostDeleteFlowTest extends ResourceFlowTestCase<HostDeleteFlow, HostResour
.setSuperordinateDomain(domain.createVKey())
.build());
clock.advanceOneMilli();
if (tm().isOfy()) {
runFlowAssertResponse(loadFile("host_delete_response_pending.xml"));
assertOfyDeleteSuccess("NewRegistrar", "ABC-12345", false);
} else {
runFlowAssertResponse(loadFile("host_delete_response.xml"));
assertSqlDeleteSuccess(true);
}
runFlowAssertResponse(loadFile("host_delete_response.xml"));
assertSqlDeleteSuccess(true);
}
@TestOfyAndSql
@ -367,25 +335,6 @@ class HostDeleteFlowTest extends ResourceFlowTestCase<HostDeleteFlow, HostResour
DatabaseHelper.removeDatabaseMigrationSchedule();
}
private void assertOfyDeleteSuccess(String registrarId, String clientTrid, boolean isSuperuser)
throws Exception {
HostResource deletedHost = reloadResourceByForeignKey();
assertAsyncDeletionTaskEnqueued(
deletedHost, registrarId, Trid.create(clientTrid, "server-trid"), isSuperuser);
assertAboutHosts()
.that(deletedHost)
.hasStatusValue(StatusValue.PENDING_DELETE)
.and()
.hasOnlyOneHistoryEntryWhich()
.hasType(Type.HOST_PENDING_DELETE);
assertNoBillingEvents();
assertNoDnsTasksEnqueued();
}
private void assertOfyDeleteSuccess() throws Exception {
assertOfyDeleteSuccess("TheRegistrar", "ABC-12345", false);
}
private void assertSqlDeleteSuccess(boolean isSubordinate) throws Exception {
assertThat(reloadResourceByForeignKey()).isNull();
HostResource deletedHost = reloadResourceByForeignKey(clock.nowUtc().minusMillis(1));

View file

@ -19,7 +19,6 @@ import static com.google.common.truth.Truth.assertThat;
import static google.registry.batch.AsyncTaskEnqueuer.PARAM_HOST_KEY;
import static google.registry.batch.AsyncTaskEnqueuer.QUEUE_ASYNC_HOST_RENAME;
import static google.registry.model.EppResourceUtils.loadByForeignKey;
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
import static google.registry.testing.DatabaseHelper.assertNoBillingEvents;
import static google.registry.testing.DatabaseHelper.createTld;
import static google.registry.testing.DatabaseHelper.getOnlyHistoryEntryOfType;
@ -198,16 +197,6 @@ class HostUpdateFlowTest extends ResourceFlowTestCase<HostUpdateFlow, HostResour
HostResource renamedHost = doSuccessfulTest();
assertThat(renamedHost.isSubordinate()).isTrue();
assertDnsTasksEnqueued("ns1.example.tld", "ns2.example.tld");
// As we don't actually store ForeignKeyIndex<HostResource> 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<HostResource> oldFkiBeforeRename =
ForeignKeyIndex.load(HostResource.class, oldHostName(), clock.nowUtc().minusMillis(1));
assertThat(oldFkiBeforeRename.getResourceKey()).isEqualTo(renamedHost.createVKey());
assertThat(oldFkiBeforeRename.getDeletionTime()).isEqualTo(clock.nowUtc());
}
ForeignKeyIndex<HostResource> oldFkiAfterRename =
ForeignKeyIndex.load(HostResource.class, oldHostName(), clock.nowUtc());
assertThat(oldFkiAfterRename).isNull();

View file

@ -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;
* <p>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<RdapDomainAction> {
RdapDomainActionTest() {
@ -247,7 +249,7 @@ class RdapDomainActionTest extends RdapActionBaseTestCase<RdapDomainAction> {
assertThat(response.getStatus()).isEqualTo(200);
}
@Test
@TestOfyAndSql
void testInvalidDomain_returns400() {
assertThat(generateActualJson("invalid/domain/name"))
.isEqualTo(
@ -258,7 +260,7 @@ class RdapDomainActionTest extends RdapActionBaseTestCase<RdapDomainAction> {
assertThat(response.getStatus()).isEqualTo(400);
}
@Test
@TestOfyAndSql
void testUnknownDomain_returns400() {
assertThat(generateActualJson("missingdomain.com"))
.isEqualTo(
@ -269,45 +271,45 @@ class RdapDomainActionTest extends RdapActionBaseTestCase<RdapDomainAction> {
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<RdapDomainAction> {
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<RdapDomainAction> {
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<RdapDomainAction> {
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<RdapDomainAction> {
// 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<RdapDomainAction> {
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<RdapDomainAction> {
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<RdapDomainAction> {
assertThat(response.getStatus()).isEqualTo(200);
}
@Test
@TestOfyAndSql
void testMetrics() {
generateActualJson("cat.lol");
verify(rdapMetrics)

View file

@ -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"));

View file

@ -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<CreateSyntheticHistoryEntriesAction> {
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<? extends HistoryEntry> 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");
}
}

View file

@ -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 <T extends EppResource> void validateHistoryEntry(
Class<? extends HistoryEntry> historyClazz, T resource) {
ImmutableList<? extends HistoryEntry> 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);
}
}

View file

@ -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 =

View file

@ -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)))

View file

@ -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());

View file

@ -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();
}

View file

@ -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