diff --git a/java/google/registry/batch/BatchModule.java b/java/google/registry/batch/BatchModule.java index 4d4ebe6fd..4ed7e1c34 100644 --- a/java/google/registry/batch/BatchModule.java +++ b/java/google/registry/batch/BatchModule.java @@ -14,19 +14,26 @@ package google.registry.batch; +import static google.registry.flows.async.AsyncFlowEnqueuer.PARAM_REQUESTED_TIME; +import static google.registry.flows.async.AsyncFlowEnqueuer.PARAM_RESOURCE_KEY; import static google.registry.request.RequestParameters.extractOptionalBooleanParameter; import static google.registry.request.RequestParameters.extractOptionalIntParameter; import static google.registry.request.RequestParameters.extractOptionalParameter; +import static google.registry.request.RequestParameters.extractRequiredDatetimeParameter; +import static google.registry.request.RequestParameters.extractRequiredParameter; import com.google.api.services.bigquery.model.TableFieldSchema; import com.google.common.collect.ImmutableList; +import com.googlecode.objectify.Key; import dagger.Module; import dagger.Provides; import dagger.multibindings.IntoMap; import dagger.multibindings.StringKey; +import google.registry.model.ImmutableObject; import google.registry.request.Parameter; import java.util.Optional; import javax.servlet.http.HttpServletRequest; +import org.joda.time.DateTime; /** * Dagger module for injecting common settings for batch actions. @@ -70,4 +77,16 @@ public class BatchModule { static Optional provideForce(HttpServletRequest req) { return extractOptionalBooleanParameter(req, "force"); } + + @Provides + @Parameter(PARAM_RESOURCE_KEY) + static Key provideResourceKey(HttpServletRequest req) { + return Key.create(extractRequiredParameter(req, PARAM_RESOURCE_KEY)); + } + + @Provides + @Parameter(PARAM_REQUESTED_TIME) + static DateTime provideRequestedTime(HttpServletRequest req) { + return extractRequiredDatetimeParameter(req, PARAM_REQUESTED_TIME); + } } diff --git a/java/google/registry/batch/ResaveEntityAction.java b/java/google/registry/batch/ResaveEntityAction.java new file mode 100644 index 000000000..5575b9130 --- /dev/null +++ b/java/google/registry/batch/ResaveEntityAction.java @@ -0,0 +1,74 @@ +// Copyright 2018 The Nomulus Authors. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package google.registry.batch; + +import static google.registry.flows.async.AsyncFlowEnqueuer.PARAM_REQUESTED_TIME; +import static google.registry.flows.async.AsyncFlowEnqueuer.PARAM_RESOURCE_KEY; +import static google.registry.model.ofy.ObjectifyService.ofy; + +import com.google.common.flogger.FluentLogger; +import com.googlecode.objectify.Key; +import google.registry.model.EppResource; +import google.registry.model.ImmutableObject; +import google.registry.request.Action; +import google.registry.request.Action.Method; +import google.registry.request.Parameter; +import google.registry.request.Response; +import google.registry.request.auth.Auth; +import google.registry.util.Clock; +import javax.inject.Inject; +import org.joda.time.DateTime; + +/** + * An action that re-saves a given entity, typically after a certain amount of time has passed. + * + *

{@link EppResource}s will be projected forward to the current time. + */ +@Action(path = "/_dr/task/resaveEntity", auth = Auth.AUTH_INTERNAL_OR_ADMIN, method = Method.POST) +public class ResaveEntityAction implements Runnable { + + private static final FluentLogger logger = FluentLogger.forEnclosingClass(); + + private final Key resourceKey; + private final DateTime requestedTime; + private final Clock clock; + private final Response response; + + @Inject + ResaveEntityAction( + @Parameter(PARAM_RESOURCE_KEY) Key resourceKey, + @Parameter(PARAM_REQUESTED_TIME) DateTime requestedTime, + Clock clock, + Response response) { + this.resourceKey = resourceKey; + this.requestedTime = requestedTime; + this.clock = clock; + this.response = response; + } + + @Override + public void run() { + logger.atInfo().log( + "Re-saving entity %s which was enqueued at %s.", resourceKey, requestedTime); + ofy().transact(() -> { + ImmutableObject entity = ofy().load().key(resourceKey).now(); + ofy().save().entity( + (entity instanceof EppResource) + ? ((EppResource) entity).cloneProjectedAtTime(clock.nowUtc()) : entity + ); + }); + response.setPayload("Entity re-saved."); + } +} diff --git a/java/google/registry/env/common/backend/WEB-INF/web.xml b/java/google/registry/env/common/backend/WEB-INF/web.xml index 2744cf42d..e2bb4cc10 100644 --- a/java/google/registry/env/common/backend/WEB-INF/web.xml +++ b/java/google/registry/env/common/backend/WEB-INF/web.xml @@ -298,6 +298,12 @@ /_dr/task/resaveAllEppResources + + + backend-servlet + /_dr/task/resaveEntity + + + + async-actions + 1/s + 5 + + diff --git a/java/google/registry/flows/async/AsyncFlowEnqueuer.java b/java/google/registry/flows/async/AsyncFlowEnqueuer.java index facbb2748..93e9d1aae 100644 --- a/java/google/registry/flows/async/AsyncFlowEnqueuer.java +++ b/java/google/registry/flows/async/AsyncFlowEnqueuer.java @@ -14,6 +14,10 @@ package google.registry.flows.async; +import static com.google.common.base.Preconditions.checkArgument; +import static google.registry.util.DateTimeUtils.isBeforeOrAt; + +import com.google.appengine.api.modules.ModulesService; import com.google.appengine.api.taskqueue.Queue; import com.google.appengine.api.taskqueue.TaskOptions; import com.google.appengine.api.taskqueue.TaskOptions.Method; @@ -23,6 +27,7 @@ import com.google.common.flogger.FluentLogger; import com.googlecode.objectify.Key; import google.registry.config.RegistryConfig.Config; import google.registry.model.EppResource; +import google.registry.model.ImmutableObject; import google.registry.model.eppcommon.Trid; import google.registry.model.host.HostResource; import google.registry.util.Retrier; @@ -44,29 +49,62 @@ public final class AsyncFlowEnqueuer { public static final String PARAM_REQUESTED_TIME = "requestedTime"; /** The task queue names used by async flows. */ + public static final String QUEUE_ASYNC_ACTIONS = "async-actions"; public static final String QUEUE_ASYNC_DELETE = "async-delete-pull"; public static final String QUEUE_ASYNC_HOST_RENAME = "async-host-rename-pull"; + public static final String PATH_RESAVE_ENTITY = "/_dr/task/resaveEntity"; + private static final FluentLogger logger = FluentLogger.forEnclosingClass(); + private static final Duration MAX_ASYNC_ETA = Duration.standardDays(30); private final Duration asyncDeleteDelay; + private final Queue asyncActionsPushQueue; private final Queue asyncDeletePullQueue; private final Queue asyncDnsRefreshPullQueue; + private final ModulesService modulesService; private final Retrier retrier; @VisibleForTesting @Inject public AsyncFlowEnqueuer( + @Named(QUEUE_ASYNC_ACTIONS) Queue asyncActionsPushQueue, @Named(QUEUE_ASYNC_DELETE) Queue asyncDeletePullQueue, @Named(QUEUE_ASYNC_HOST_RENAME) Queue asyncDnsRefreshPullQueue, @Config("asyncDeleteFlowMapreduceDelay") Duration asyncDeleteDelay, + ModulesService modulesService, Retrier retrier) { + this.asyncActionsPushQueue = asyncActionsPushQueue; this.asyncDeletePullQueue = asyncDeletePullQueue; this.asyncDnsRefreshPullQueue = asyncDnsRefreshPullQueue; this.asyncDeleteDelay = asyncDeleteDelay; + this.modulesService = modulesService; this.retrier = retrier; } + /** Enqueues a task to asynchronously re-save an entity at some point in the future. */ + public void enqueueAsyncResave( + ImmutableObject entityToResave, DateTime now, DateTime whenToResave) { + checkArgument(isBeforeOrAt(now, whenToResave), "Can't enqueue a resave to run in the past"); + Key entityKey = Key.create(entityToResave); + Duration etaDuration = new Duration(now, whenToResave); + if (etaDuration.isLongerThan(MAX_ASYNC_ETA)) { + logger.atInfo().log("Ignoring async re-save of %s; %s is past the ETA threshold of %s.", + entityKey, whenToResave, MAX_ASYNC_ETA); + return; + } + logger.atInfo().log("Enqueuing async re-save of %s to run at %s.", entityKey, whenToResave); + String backendHostname = modulesService.getVersionHostname("backend", null); + TaskOptions task = + TaskOptions.Builder.withUrl(PATH_RESAVE_ENTITY) + .method(Method.POST) + .header("Host", backendHostname) + .countdownMillis(etaDuration.getMillis()) + .param(PARAM_RESOURCE_KEY, entityKey.getString()) + .param(PARAM_REQUESTED_TIME, now.toString()); + addTaskToQueueWithRetry(asyncActionsPushQueue, task); + } + /** Enqueues a task to asynchronously delete a contact or host, by key. */ public void enqueueAsyncDelete( EppResource resourceToDelete, diff --git a/java/google/registry/flows/async/AsyncFlowsModule.java b/java/google/registry/flows/async/AsyncFlowsModule.java index 1680e6a1d..0e6ee6d92 100644 --- a/java/google/registry/flows/async/AsyncFlowsModule.java +++ b/java/google/registry/flows/async/AsyncFlowsModule.java @@ -14,11 +14,12 @@ package google.registry.flows.async; +import static com.google.appengine.api.taskqueue.QueueFactory.getQueue; +import static google.registry.flows.async.AsyncFlowEnqueuer.QUEUE_ASYNC_ACTIONS; import static google.registry.flows.async.AsyncFlowEnqueuer.QUEUE_ASYNC_DELETE; import static google.registry.flows.async.AsyncFlowEnqueuer.QUEUE_ASYNC_HOST_RENAME; import com.google.appengine.api.taskqueue.Queue; -import com.google.appengine.api.taskqueue.QueueFactory; import dagger.Module; import dagger.Provides; import javax.inject.Named; @@ -27,15 +28,21 @@ import javax.inject.Named; @Module public final class AsyncFlowsModule { + @Provides + @Named(QUEUE_ASYNC_ACTIONS) + static Queue provideAsyncActionsPushQueue() { + return getQueue(QUEUE_ASYNC_ACTIONS); + } + @Provides @Named(QUEUE_ASYNC_DELETE) static Queue provideAsyncDeletePullQueue() { - return QueueFactory.getQueue(QUEUE_ASYNC_DELETE); + return getQueue(QUEUE_ASYNC_DELETE); } @Provides @Named(QUEUE_ASYNC_HOST_RENAME) static Queue provideAsyncHostRenamePullQueue() { - return QueueFactory.getQueue(QUEUE_ASYNC_HOST_RENAME); + return getQueue(QUEUE_ASYNC_HOST_RENAME); } } diff --git a/java/google/registry/flows/domain/DomainDeleteFlow.java b/java/google/registry/flows/domain/DomainDeleteFlow.java index d0a33a379..876589b5d 100644 --- a/java/google/registry/flows/domain/DomainDeleteFlow.java +++ b/java/google/registry/flows/domain/DomainDeleteFlow.java @@ -51,6 +51,7 @@ import google.registry.flows.FlowModule.TargetId; import google.registry.flows.SessionMetadata; import google.registry.flows.TransactionalFlow; import google.registry.flows.annotations.ReportingSpec; +import google.registry.flows.async.AsyncFlowEnqueuer; import google.registry.flows.custom.DomainDeleteFlowCustomLogic; import google.registry.flows.custom.DomainDeleteFlowCustomLogic.AfterValidationParameters; import google.registry.flows.custom.DomainDeleteFlowCustomLogic.BeforeResponseParameters; @@ -127,6 +128,7 @@ public final class DomainDeleteFlow implements TransactionalFlow { @Inject HistoryEntry.Builder historyBuilder; @Inject DnsQueue dnsQueue; @Inject Trid trid; + @Inject AsyncFlowEnqueuer asyncFlowEnqueuer; @Inject EppResponse.Builder responseBuilder; @Inject DomainDeleteFlowCustomLogic flowCustomLogic; @Inject DomainDeleteFlow() {} @@ -179,16 +181,19 @@ public final class DomainDeleteFlow implements TransactionalFlow { builder.setDeletionTime(now).setStatusValues(null); } else { DateTime deletionTime = now.plus(durationUntilDelete); + DateTime redemptionTime = now.plus(redemptionGracePeriodLength); PollMessage.OneTime deletePollMessage = createDeletePollMessage(existingDomain, historyEntry, deletionTime); entitiesToSave.add(deletePollMessage); + asyncFlowEnqueuer.enqueueAsyncResave(existingDomain, now, deletionTime); + asyncFlowEnqueuer.enqueueAsyncResave(existingDomain, now, redemptionTime); builder.setDeletionTime(deletionTime) .setStatusValues(ImmutableSet.of(StatusValue.PENDING_DELETE)) // Clear out all old grace periods and add REDEMPTION, which does not include a key to a // billing event because there isn't one for a domain delete. .setGracePeriods(ImmutableSet.of(GracePeriod.createWithoutBillingEvent( GracePeriodStatus.REDEMPTION, - now.plus(redemptionGracePeriodLength), + redemptionTime, clientId))) .setDeletePollMessage(Key.create(deletePollMessage)); // Note: The expiration time is unchanged, so if it's before the new deletion time, there will diff --git a/java/google/registry/flows/domain/DomainTransferRequestFlow.java b/java/google/registry/flows/domain/DomainTransferRequestFlow.java index 3d85389a8..ddff94e53 100644 --- a/java/google/registry/flows/domain/DomainTransferRequestFlow.java +++ b/java/google/registry/flows/domain/DomainTransferRequestFlow.java @@ -42,6 +42,7 @@ import google.registry.flows.FlowModule.Superuser; import google.registry.flows.FlowModule.TargetId; import google.registry.flows.TransactionalFlow; import google.registry.flows.annotations.ReportingSpec; +import google.registry.flows.async.AsyncFlowEnqueuer; import google.registry.flows.exceptions.AlreadyPendingTransferException; import google.registry.flows.exceptions.InvalidTransferPeriodValueException; import google.registry.flows.exceptions.ObjectAlreadySponsoredException; @@ -125,6 +126,7 @@ public final class DomainTransferRequestFlow implements TransactionalFlow { @Inject @Superuser boolean isSuperuser; @Inject HistoryEntry.Builder historyBuilder; @Inject Trid trid; + @Inject AsyncFlowEnqueuer asyncFlowEnqueuer; @Inject EppResponse.Builder responseBuilder; @Inject DomainPricingLogic pricingLogic; @Inject DomainTransferRequestFlow() {} @@ -224,6 +226,7 @@ public final class DomainTransferRequestFlow implements TransactionalFlow { .setTransferData(pendingTransferData) .addStatusValue(StatusValue.PENDING_TRANSFER) .build(); + asyncFlowEnqueuer.enqueueAsyncResave(newDomain, now, automaticTransferTime); ofy().save() .entities(new ImmutableSet.Builder<>() .add(newDomain, historyEntry, requestPollMessage) diff --git a/javatests/google/registry/batch/BUILD b/javatests/google/registry/batch/BUILD index 6ef4ea1ea..1836ebbcd 100644 --- a/javatests/google/registry/batch/BUILD +++ b/javatests/google/registry/batch/BUILD @@ -17,6 +17,7 @@ java_library( "//java/google/registry/flows", "//java/google/registry/mapreduce", "//java/google/registry/model", + "//java/google/registry/request", "//java/google/registry/util", "//javatests/google/registry/testing", "//javatests/google/registry/testing/mapreduce", diff --git a/javatests/google/registry/batch/DeleteContactsAndHostsActionTest.java b/javatests/google/registry/batch/DeleteContactsAndHostsActionTest.java index 48ca2c8bd..5d1993227 100644 --- a/javatests/google/registry/batch/DeleteContactsAndHostsActionTest.java +++ b/javatests/google/registry/batch/DeleteContactsAndHostsActionTest.java @@ -18,6 +18,7 @@ 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.flows.async.AsyncFlowEnqueuer.QUEUE_ASYNC_ACTIONS; import static google.registry.flows.async.AsyncFlowEnqueuer.QUEUE_ASYNC_DELETE; import static google.registry.flows.async.AsyncFlowEnqueuer.QUEUE_ASYNC_HOST_RENAME; import static google.registry.flows.async.AsyncFlowMetrics.OperationResult.STALE; @@ -57,6 +58,7 @@ import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoMoreInteractions; +import com.google.appengine.api.modules.ModulesService; import com.google.appengine.api.taskqueue.TaskOptions; import com.google.appengine.api.taskqueue.TaskOptions.Method; import com.google.common.collect.ImmutableList; @@ -136,9 +138,11 @@ public class DeleteContactsAndHostsActionTest public void setup() { enqueuer = new AsyncFlowEnqueuer( + getQueue(QUEUE_ASYNC_ACTIONS), getQueue(QUEUE_ASYNC_DELETE), getQueue(QUEUE_ASYNC_HOST_RENAME), Duration.ZERO, + mock(ModulesService.class), new Retrier(new FakeSleeper(clock), 1)); AsyncFlowMetrics asyncFlowMetricsMock = mock(AsyncFlowMetrics.class); action = new DeleteContactsAndHostsAction(); diff --git a/javatests/google/registry/batch/RefreshDnsOnHostRenameActionTest.java b/javatests/google/registry/batch/RefreshDnsOnHostRenameActionTest.java index b6473748d..11a8b60f6 100644 --- a/javatests/google/registry/batch/RefreshDnsOnHostRenameActionTest.java +++ b/javatests/google/registry/batch/RefreshDnsOnHostRenameActionTest.java @@ -15,6 +15,7 @@ package google.registry.batch; import static com.google.appengine.api.taskqueue.QueueFactory.getQueue; +import static google.registry.flows.async.AsyncFlowEnqueuer.QUEUE_ASYNC_ACTIONS; import static google.registry.flows.async.AsyncFlowEnqueuer.QUEUE_ASYNC_DELETE; import static google.registry.flows.async.AsyncFlowEnqueuer.QUEUE_ASYNC_HOST_RENAME; import static google.registry.flows.async.AsyncFlowMetrics.OperationType.DNS_REFRESH; @@ -39,6 +40,7 @@ import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoMoreInteractions; +import com.google.appengine.api.modules.ModulesService; import com.google.common.collect.ImmutableSet; import com.googlecode.objectify.Key; import google.registry.batch.RefreshDnsOnHostRenameAction.RefreshDnsOnHostRenameReducer; @@ -79,9 +81,11 @@ public class RefreshDnsOnHostRenameActionTest createTld("tld"); enqueuer = new AsyncFlowEnqueuer( + getQueue(QUEUE_ASYNC_ACTIONS), getQueue(QUEUE_ASYNC_DELETE), getQueue(QUEUE_ASYNC_HOST_RENAME), Duration.ZERO, + mock(ModulesService.class), new Retrier(new FakeSleeper(clock), 1)); AsyncFlowMetrics asyncFlowMetricsMock = mock(AsyncFlowMetrics.class); action = new RefreshDnsOnHostRenameAction(); diff --git a/javatests/google/registry/batch/ResaveEntityActionTest.java b/javatests/google/registry/batch/ResaveEntityActionTest.java new file mode 100644 index 000000000..70781a135 --- /dev/null +++ b/javatests/google/registry/batch/ResaveEntityActionTest.java @@ -0,0 +1,83 @@ +// Copyright 2018 The Nomulus Authors. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package google.registry.batch; + +import static com.google.common.truth.Truth.assertThat; +import static google.registry.model.ofy.ObjectifyService.ofy; +import static google.registry.testing.DatastoreHelper.createTld; +import static google.registry.testing.DatastoreHelper.persistActiveContact; +import static google.registry.testing.DatastoreHelper.persistDomainWithDependentResources; +import static google.registry.testing.DatastoreHelper.persistDomainWithPendingTransfer; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; + +import com.googlecode.objectify.Key; +import google.registry.model.ImmutableObject; +import google.registry.model.domain.DomainResource; +import google.registry.request.Response; +import google.registry.testing.AppEngineRule; +import google.registry.testing.FakeClock; +import google.registry.testing.ShardableTestCase; +import google.registry.util.Clock; +import org.joda.time.DateTime; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +/** Unit tests for {@link ResaveEntityAction}. */ +@RunWith(JUnit4.class) +public class ResaveEntityActionTest extends ShardableTestCase { + + @Rule + public final AppEngineRule appEngine = + AppEngineRule.builder().withDatastore().withTaskQueue().build(); + + private final Clock clock = new FakeClock(DateTime.parse("2016-02-11T10:00:00Z")); + private final Response response = mock(Response.class); + + @Before + public void before() { + createTld("tld"); + } + + private void runAction(Key resourceKey, DateTime requestedTime) { + ResaveEntityAction action = new ResaveEntityAction(resourceKey, requestedTime, clock, response); + action.run(); + } + + @Test + public void test_domainPendingTransfer_isResavedAndTransferCompleted() { + DomainResource domain = + persistDomainWithPendingTransfer( + persistDomainWithDependentResources( + "domain", + "tld", + persistActiveContact("jd1234"), + DateTime.parse("2016-02-06T10:00:00Z"), + DateTime.parse("2016-02-06T10:00:00Z"), + DateTime.parse("2017-01-02T10:11:00Z")), + DateTime.parse("2016-02-06T10:00:00Z"), + DateTime.parse("2016-02-11T10:00:00Z"), + DateTime.parse("2017-01-02T10:11:00Z"), + DateTime.parse("2016-02-06T10:00:00Z")); + assertThat(domain.getCurrentSponsorClientId()).isEqualTo("TheRegistrar"); + runAction(Key.create(domain), DateTime.parse("2016-02-06T10:00:01Z")); + DomainResource resavedDomain = ofy().load().entity(domain).now(); + assertThat(resavedDomain.getCurrentSponsorClientId()).isEqualTo("NewRegistrar"); + verify(response).setPayload("Entity re-saved."); + } +} diff --git a/javatests/google/registry/export/BigqueryPollJobActionTest.java b/javatests/google/registry/export/BigqueryPollJobActionTest.java index 2b6bb1562..8b108b31b 100644 --- a/javatests/google/registry/export/BigqueryPollJobActionTest.java +++ b/javatests/google/registry/export/BigqueryPollJobActionTest.java @@ -17,9 +17,9 @@ package google.registry.export; import static com.google.appengine.api.taskqueue.QueueFactory.getQueue; import static com.google.common.collect.Iterables.getOnlyElement; import static com.google.common.truth.Truth.assertThat; -import static com.google.common.truth.Truth.assert_; import static google.registry.testing.JUnitBackports.assertThrows; import static google.registry.testing.TaskQueueHelper.assertTasksEnqueued; +import static google.registry.testing.TestLogHandlerUtils.assertLogMessage; import static java.nio.charset.StandardCharsets.UTF_8; import static java.util.logging.Level.INFO; import static java.util.logging.Level.SEVERE; @@ -51,8 +51,6 @@ import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; -import java.util.logging.Level; -import java.util.logging.LogRecord; import org.junit.Before; import org.junit.Rule; import org.junit.Test; @@ -127,22 +125,13 @@ public class BigqueryPollJobActionTest { assertThat(taskOptions).isEqualTo(chainedTask); } - private void assertLogMessage(Level level, String message) { - for (LogRecord logRecord : logHandler.getRecords()) { - if (logRecord.getLevel().equals(level) && logRecord.getMessage().contains(message)) { - return; - } - } - assert_().fail(String.format("Log message \"%s\" not found", message)); - } - @Test public void testSuccess_jobCompletedSuccessfully() throws Exception { when(bigqueryJobsGet.execute()).thenReturn( new Job().setStatus(new JobStatus().setState("DONE"))); action.run(); - assertLogMessage(INFO, - String.format("Bigquery job succeeded - %s:%s", PROJECT_ID, JOB_ID)); + assertLogMessage( + logHandler, INFO, String.format("Bigquery job succeeded - %s:%s", PROJECT_ID, JOB_ID)); } @Test @@ -150,28 +139,31 @@ public class BigqueryPollJobActionTest { when(bigqueryJobsGet.execute()).thenReturn( new Job().setStatus(new JobStatus().setState("DONE"))); - TaskOptions chainedTask = TaskOptions.Builder - .withUrl("/_dr/something") - .method(Method.POST) - .header("X-Testing", "foo") - .param("testing", "bar") - .taskName("my_task_name"); + TaskOptions chainedTask = + TaskOptions.Builder.withUrl("/_dr/something") + .method(Method.POST) + .header("X-Testing", "foo") + .param("testing", "bar") + .taskName("my_task_name"); ByteArrayOutputStream bytes = new ByteArrayOutputStream(); new ObjectOutputStream(bytes).writeObject(chainedTask); action.payload = bytes.toByteArray(); action.run(); - assertLogMessage(INFO, - String.format("Bigquery job succeeded - %s:%s", PROJECT_ID, JOB_ID)); assertLogMessage( + logHandler, INFO, String.format("Bigquery job succeeded - %s:%s", PROJECT_ID, JOB_ID)); + assertLogMessage( + logHandler, INFO, "Added chained task my_task_name for /_dr/something to queue " + CHAINED_QUEUE_NAME); - assertTasksEnqueued(CHAINED_QUEUE_NAME, new TaskMatcher() - .url("/_dr/something") - .method("POST") - .header("X-Testing", "foo") - .param("testing", "bar") - .taskName("my_task_name")); + assertTasksEnqueued( + CHAINED_QUEUE_NAME, + new TaskMatcher() + .url("/_dr/something") + .method("POST") + .header("X-Testing", "foo") + .param("testing", "bar") + .taskName("my_task_name")); } @Test @@ -181,7 +173,8 @@ public class BigqueryPollJobActionTest { .setState("DONE") .setErrorResult(new ErrorProto().setMessage("Job failed")))); action.run(); - assertLogMessage(SEVERE, String.format("Bigquery job failed - %s:%s", PROJECT_ID, JOB_ID)); + assertLogMessage( + logHandler, SEVERE, String.format("Bigquery job failed - %s:%s", PROJECT_ID, JOB_ID)); } @Test diff --git a/javatests/google/registry/flows/EppTestComponent.java b/javatests/google/registry/flows/EppTestComponent.java index 71f12148b..3845241fd 100644 --- a/javatests/google/registry/flows/EppTestComponent.java +++ b/javatests/google/registry/flows/EppTestComponent.java @@ -14,7 +14,13 @@ package google.registry.flows; +import static com.google.appengine.api.taskqueue.QueueFactory.getQueue; +import static google.registry.flows.async.AsyncFlowEnqueuer.QUEUE_ASYNC_ACTIONS; +import static google.registry.flows.async.AsyncFlowEnqueuer.QUEUE_ASYNC_DELETE; +import static google.registry.flows.async.AsyncFlowEnqueuer.QUEUE_ASYNC_HOST_RENAME; +import static org.mockito.Matchers.any; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; import com.google.appengine.api.modules.ModulesService; import dagger.Component; @@ -24,6 +30,7 @@ import dagger.Subcomponent; import google.registry.config.RegistryConfig.ConfigModule; import google.registry.config.RegistryConfig.ConfigModule.TmchCaMode; import google.registry.dns.DnsQueue; +import google.registry.flows.async.AsyncFlowEnqueuer; import google.registry.flows.custom.CustomLogicFactory; import google.registry.flows.custom.TestCustomLogicFactory; import google.registry.flows.domain.DomainFlowTmchUtils; @@ -37,8 +44,10 @@ import google.registry.testing.FakeSleeper; import google.registry.tmch.TmchCertificateAuthority; import google.registry.tmch.TmchXmlSignature; import google.registry.util.Clock; +import google.registry.util.Retrier; import google.registry.util.Sleeper; import javax.inject.Singleton; +import org.joda.time.Duration; /** Dagger component for running EPP tests. */ @Singleton @@ -55,6 +64,7 @@ interface EppTestComponent { @Module class FakesAndMocksModule { + private AsyncFlowEnqueuer asyncFlowEnqueuer; private BigQueryMetricsEnqueuer metricsEnqueuer; private DnsQueue dnsQueue; private DomainFlowTmchUtils domainFlowTmchUtils; @@ -81,17 +91,33 @@ interface EppTestComponent { EppMetric.Builder eppMetricBuilder, TmchXmlSignature tmchXmlSignature) { FakesAndMocksModule instance = new FakesAndMocksModule(); + ModulesService modulesService = mock(ModulesService.class); + when(modulesService.getVersionHostname(any(String.class), any(String.class))) + .thenReturn("backend.hostname.fake"); + instance.asyncFlowEnqueuer = + new AsyncFlowEnqueuer( + getQueue(QUEUE_ASYNC_ACTIONS), + getQueue(QUEUE_ASYNC_DELETE), + getQueue(QUEUE_ASYNC_HOST_RENAME), + Duration.standardSeconds(90), + modulesService, + new Retrier(new FakeSleeper(clock), 1)); instance.clock = clock; instance.domainFlowTmchUtils = new DomainFlowTmchUtils(tmchXmlSignature); instance.sleeper = new FakeSleeper(clock); instance.dnsQueue = DnsQueue.create(); instance.metricBuilder = eppMetricBuilder; - instance.modulesService = mock(ModulesService.class); + instance.modulesService = modulesService; instance.metricsEnqueuer = mock(BigQueryMetricsEnqueuer.class); instance.lockHandler = new FakeLockHandler(true); return instance; } + @Provides + AsyncFlowEnqueuer provideAsyncFlowEnqueuer() { + return asyncFlowEnqueuer; + } + @Provides BigQueryMetricsEnqueuer provideBigQueryMetricsEnqueuer() { return metricsEnqueuer; diff --git a/javatests/google/registry/flows/async/AsyncFlowEnqueuerTest.java b/javatests/google/registry/flows/async/AsyncFlowEnqueuerTest.java new file mode 100644 index 000000000..8c3e389c7 --- /dev/null +++ b/javatests/google/registry/flows/async/AsyncFlowEnqueuerTest.java @@ -0,0 +1,113 @@ +// Copyright 2018 The Nomulus Authors. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package google.registry.flows.async; + +import static com.google.appengine.api.taskqueue.QueueFactory.getQueue; +import static google.registry.flows.async.AsyncFlowEnqueuer.PARAM_REQUESTED_TIME; +import static google.registry.flows.async.AsyncFlowEnqueuer.PARAM_RESOURCE_KEY; +import static google.registry.flows.async.AsyncFlowEnqueuer.PATH_RESAVE_ENTITY; +import static google.registry.flows.async.AsyncFlowEnqueuer.QUEUE_ASYNC_ACTIONS; +import static google.registry.flows.async.AsyncFlowEnqueuer.QUEUE_ASYNC_DELETE; +import static google.registry.flows.async.AsyncFlowEnqueuer.QUEUE_ASYNC_HOST_RENAME; +import static google.registry.testing.DatastoreHelper.persistActiveContact; +import static google.registry.testing.TaskQueueHelper.assertNoTasksEnqueued; +import static google.registry.testing.TaskQueueHelper.assertTasksEnqueued; +import static google.registry.testing.TestLogHandlerUtils.assertLogMessage; +import static org.joda.time.Duration.standardDays; +import static org.joda.time.Duration.standardSeconds; +import static org.mockito.Matchers.any; +import static org.mockito.Mockito.when; + +import com.google.appengine.api.modules.ModulesService; +import com.google.common.flogger.LoggerConfig; +import com.googlecode.objectify.Key; +import google.registry.model.contact.ContactResource; +import google.registry.testing.AppEngineRule; +import google.registry.testing.FakeClock; +import google.registry.testing.FakeSleeper; +import google.registry.testing.InjectRule; +import google.registry.testing.MockitoJUnitRule; +import google.registry.testing.ShardableTestCase; +import google.registry.testing.TaskQueueHelper.TaskMatcher; +import google.registry.util.CapturingLogHandler; +import google.registry.util.Retrier; +import java.util.logging.Level; +import org.joda.time.DateTime; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; +import org.mockito.Mock; + +/** Unit tests for {@link AsyncFlowEnqueuer}. */ +@RunWith(JUnit4.class) +public class AsyncFlowEnqueuerTest extends ShardableTestCase { + + @Rule + public final AppEngineRule appEngine = + AppEngineRule.builder().withDatastore().withTaskQueue().build(); + + @Rule public final InjectRule inject = new InjectRule(); + + @Rule public final MockitoJUnitRule mocks = MockitoJUnitRule.create(); + + @Mock private ModulesService modulesService; + + private AsyncFlowEnqueuer asyncFlowEnqueuer; + private final CapturingLogHandler logHandler = new CapturingLogHandler(); + private final FakeClock clock = new FakeClock(DateTime.parse("2015-05-18T12:34:56Z")); + + @Before + public void setUp() { + LoggerConfig.getConfig(AsyncFlowEnqueuer.class).addHandler(logHandler); + when(modulesService.getVersionHostname(any(String.class), any(String.class))) + .thenReturn("backend.hostname.fake"); + asyncFlowEnqueuer = + new AsyncFlowEnqueuer( + getQueue(QUEUE_ASYNC_ACTIONS), + getQueue(QUEUE_ASYNC_DELETE), + getQueue(QUEUE_ASYNC_HOST_RENAME), + standardSeconds(90), + modulesService, + new Retrier(new FakeSleeper(clock), 1)); + } + + @Test + public void test_enqueueAsyncResave_success() { + ContactResource contact = persistActiveContact("jd23456"); + asyncFlowEnqueuer.enqueueAsyncResave(contact, clock.nowUtc(), clock.nowUtc().plusDays(5)); + assertTasksEnqueued( + QUEUE_ASYNC_ACTIONS, + new TaskMatcher() + .url(PATH_RESAVE_ENTITY) + .method("POST") + .header("Host", "backend.hostname.fake") + .header("content-type", "application/x-www-form-urlencoded") + .param(PARAM_RESOURCE_KEY, Key.create(contact).getString()) + .param(PARAM_REQUESTED_TIME, clock.nowUtc().toString()) + .etaDelta( + standardDays(5).minus(standardSeconds(30)), + standardDays(5).plus(standardSeconds(30)))); + } + + @Test + public void test_enqueueAsyncResave_ignoresTasksTooFarIntoFuture() throws Exception { + ContactResource contact = persistActiveContact("jd23456"); + asyncFlowEnqueuer.enqueueAsyncResave(contact, clock.nowUtc(), clock.nowUtc().plusDays(31)); + assertNoTasksEnqueued(QUEUE_ASYNC_ACTIONS); + assertLogMessage(logHandler, Level.INFO, "Ignoring async re-save"); + } +} diff --git a/javatests/google/registry/flows/domain/DomainDeleteFlowTest.java b/javatests/google/registry/flows/domain/DomainDeleteFlowTest.java index 341ea5e38..938c84020 100644 --- a/javatests/google/registry/flows/domain/DomainDeleteFlowTest.java +++ b/javatests/google/registry/flows/domain/DomainDeleteFlowTest.java @@ -16,6 +16,10 @@ package google.registry.flows.domain; import static com.google.common.collect.MoreCollectors.onlyElement; import static com.google.common.truth.Truth.assertThat; +import static google.registry.flows.async.AsyncFlowEnqueuer.PARAM_REQUESTED_TIME; +import static google.registry.flows.async.AsyncFlowEnqueuer.PARAM_RESOURCE_KEY; +import static google.registry.flows.async.AsyncFlowEnqueuer.PATH_RESAVE_ENTITY; +import static google.registry.flows.async.AsyncFlowEnqueuer.QUEUE_ASYNC_ACTIONS; import static google.registry.flows.domain.DomainTransferFlowTestCase.persistWithPendingTransfer; import static google.registry.model.EppResourceUtils.loadByForeignKey; import static google.registry.model.ofy.ObjectifyService.ofy; @@ -45,9 +49,12 @@ import static google.registry.testing.EppExceptionSubject.assertAboutEppExceptio import static google.registry.testing.HistoryEntrySubject.assertAboutHistoryEntries; import static google.registry.testing.JUnitBackports.assertThrows; import static google.registry.testing.TaskQueueHelper.assertDnsTasksEnqueued; +import static google.registry.testing.TaskQueueHelper.assertTasksEnqueued; import static google.registry.util.DateTimeUtils.END_OF_TIME; import static google.registry.util.DateTimeUtils.START_OF_TIME; import static org.joda.money.CurrencyUnit.USD; +import static org.joda.time.Duration.standardDays; +import static org.joda.time.Duration.standardSeconds; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; @@ -85,6 +92,7 @@ import google.registry.model.reporting.HistoryEntry; import google.registry.model.transfer.TransferData; import google.registry.model.transfer.TransferResponse; import google.registry.model.transfer.TransferStatus; +import google.registry.testing.TaskQueueHelper.TaskMatcher; import java.util.Map; import org.joda.money.Money; import org.joda.time.DateTime; @@ -117,7 +125,6 @@ public class DomainDeleteFlowTest extends ResourceFlowTestCase protected void setupDomain(String label, String tld) { createTld(tld); contact = persistActiveContact("jd1234"); - domain = new DomainResource.Builder() - .setRepoId("1-".concat(Ascii.toUpperCase(tld))) - .setFullyQualifiedDomainName(label + "." + tld) - .setPersistedCurrentSponsorClientId("TheRegistrar") - .setCreationClientId("TheRegistrar") - .setCreationTimeForTest(DateTime.parse("1999-04-03T22:00:00.0Z")) - .setRegistrationExpirationTime(REGISTRATION_EXPIRATION_TIME) - .setRegistrant( - Key.create(loadByForeignKey(ContactResource.class, "jd1234", clock.nowUtc()))) - .setContacts(ImmutableSet.of( - DesignatedContact.create( - Type.ADMIN, - Key.create(loadByForeignKey(ContactResource.class, "jd1234", clock.nowUtc()))), - DesignatedContact.create( - Type.TECH, - Key.create(loadByForeignKey(ContactResource.class, "jd1234", clock.nowUtc()))))) - .setAuthInfo(DomainAuthInfo.create(PasswordAuth.create("fooBAR"))) - .addGracePeriod(GracePeriod.create( - GracePeriodStatus.ADD, clock.nowUtc().plusDays(10), "foo", null)) - .build(); - historyEntryDomainCreate = persistResource( - new HistoryEntry.Builder() - .setType(HistoryEntry.Type.DOMAIN_CREATE) - .setParent(domain) - .build()); - BillingEvent.Recurring autorenewEvent = persistResource( - new BillingEvent.Recurring.Builder() - .setReason(Reason.RENEW) - .setFlags(ImmutableSet.of(Flag.AUTO_RENEW)) - .setTargetId(label + "." + tld) - .setClientId("TheRegistrar") - .setEventTime(REGISTRATION_EXPIRATION_TIME) - .setRecurrenceEndTime(END_OF_TIME) - .setParent(historyEntryDomainCreate) - .build()); - PollMessage.Autorenew autorenewPollMessage = persistResource( - new PollMessage.Autorenew.Builder() - .setTargetId(label + "." + tld) - .setClientId("TheRegistrar") - .setEventTime(REGISTRATION_EXPIRATION_TIME) - .setAutorenewEndTime(END_OF_TIME) - .setMsg("Domain was auto-renewed.") - .setParent(historyEntryDomainCreate) - .build()); + domain = + persistDomainWithDependentResources( + label, + tld, + contact, + clock.nowUtc(), + DateTime.parse("1999-04-03T22:00:00.0Z"), + REGISTRATION_EXPIRATION_TIME); subordinateHost = persistResource( new HostResource.Builder() .setRepoId("2-".concat(Ascii.toUpperCase(tld))) @@ -161,11 +119,13 @@ public class DomainTransferFlowTestCase .setCreationTimeForTest(DateTime.parse("1999-04-03T22:00:00.0Z")) .setSuperordinateDomain(Key.create(domain)) .build()); - domain = persistResource(domain.asBuilder() - .setAutorenewBillingEvent(Key.create(autorenewEvent)) - .setAutorenewPollMessage(Key.create(autorenewPollMessage)) - .addSubordinateHost(subordinateHost.getFullyQualifiedHostName()) - .build()); + domain = + persistResource( + domain + .asBuilder() + .addSubordinateHost(subordinateHost.getFullyQualifiedHostName()) + .build()); + historyEntryDomainCreate = getOnlyHistoryEntryOfType(domain, DOMAIN_CREATE); } protected BillingEvent.OneTime getBillingEventForImplicitTransfer() { diff --git a/javatests/google/registry/flows/domain/DomainTransferRequestFlowTest.java b/javatests/google/registry/flows/domain/DomainTransferRequestFlowTest.java index 2ff63eaa9..5e35a426c 100644 --- a/javatests/google/registry/flows/domain/DomainTransferRequestFlowTest.java +++ b/javatests/google/registry/flows/domain/DomainTransferRequestFlowTest.java @@ -18,6 +18,10 @@ import static com.google.common.collect.ImmutableSet.toImmutableSet; 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.flows.async.AsyncFlowEnqueuer.PARAM_REQUESTED_TIME; +import static google.registry.flows.async.AsyncFlowEnqueuer.PARAM_RESOURCE_KEY; +import static google.registry.flows.async.AsyncFlowEnqueuer.PATH_RESAVE_ENTITY; +import static google.registry.flows.async.AsyncFlowEnqueuer.QUEUE_ASYNC_ACTIONS; import static google.registry.model.ofy.ObjectifyService.ofy; import static google.registry.model.reporting.DomainTransactionRecord.TransactionReportField.TRANSFER_SUCCESSFUL; import static google.registry.model.reporting.HistoryEntry.Type.DOMAIN_CREATE; @@ -37,9 +41,11 @@ import static google.registry.testing.EppExceptionSubject.assertAboutEppExceptio import static google.registry.testing.HistoryEntrySubject.assertAboutHistoryEntries; import static google.registry.testing.HostResourceSubject.assertAboutHosts; import static google.registry.testing.JUnitBackports.assertThrows; +import static google.registry.testing.TaskQueueHelper.assertTasksEnqueued; import static google.registry.util.DateTimeUtils.START_OF_TIME; import static org.joda.money.CurrencyUnit.EUR; import static org.joda.money.CurrencyUnit.USD; +import static org.joda.time.Duration.standardSeconds; import com.google.common.base.Predicates; import com.google.common.collect.ImmutableList; @@ -89,6 +95,7 @@ import google.registry.model.reporting.HistoryEntry; import google.registry.model.transfer.TransferData; import google.registry.model.transfer.TransferResponse; import google.registry.model.transfer.TransferStatus; +import google.registry.testing.TaskQueueHelper.TaskMatcher; import java.util.Map; import java.util.Optional; import java.util.stream.Stream; @@ -481,6 +488,18 @@ public class DomainTransferRequestFlowTest assertPollMessagesEmitted(expectedExpirationTime, implicitTransferTime); assertAboutDomainAfterAutomaticTransfer( expectedExpirationTime, implicitTransferTime, Period.create(1, Unit.YEARS)); + assertTasksEnqueued( + QUEUE_ASYNC_ACTIONS, + new TaskMatcher() + .url(PATH_RESAVE_ENTITY) + .method("POST") + .header("Host", "backend.hostname.fake") + .header("content-type", "application/x-www-form-urlencoded") + .param(PARAM_RESOURCE_KEY, Key.create(domain).getString()) + .param(PARAM_REQUESTED_TIME, clock.nowUtc().toString()) + .etaDelta( + registry.getAutomaticTransferLength().minus(standardSeconds(30)), + registry.getAutomaticTransferLength().plus(standardSeconds(30)))); } private void doSuccessfulTest( @@ -1111,19 +1130,16 @@ public class DomainTransferRequestFlowTest @Test public void testFailure_wrongCurrency_v06() { - setupDomain("example", "tld"); runWrongCurrencyTest(FEE_06_MAP); } @Test public void testFailure_wrongCurrency_v11() { - setupDomain("example", "tld"); runWrongCurrencyTest(FEE_11_MAP); } @Test public void testFailure_wrongCurrency_v12() { - setupDomain("example", "tld"); runWrongCurrencyTest(FEE_12_MAP); } diff --git a/javatests/google/registry/testing/DatastoreHelper.java b/javatests/google/registry/testing/DatastoreHelper.java index 6bf290c17..74aed39a8 100644 --- a/javatests/google/registry/testing/DatastoreHelper.java +++ b/javatests/google/registry/testing/DatastoreHelper.java @@ -70,7 +70,9 @@ import google.registry.model.domain.DesignatedContact.Type; import google.registry.model.domain.DomainApplication; import google.registry.model.domain.DomainAuthInfo; import google.registry.model.domain.DomainResource; +import google.registry.model.domain.GracePeriod; import google.registry.model.domain.launch.LaunchPhase; +import google.registry.model.domain.rgp.GracePeriodStatus; import google.registry.model.eppcommon.AuthInfo.PasswordAuth; import google.registry.model.eppcommon.StatusValue; import google.registry.model.eppcommon.Trid; @@ -552,6 +554,66 @@ public class DatastoreHelper { .build()); } + public static DomainResource persistDomainWithDependentResources( + String label, + String tld, + ContactResource contact, + DateTime now, + DateTime creationTime, + DateTime expirationTime) { + String domainName = String.format("%s.%s", label, tld); + DomainResource domain = + new DomainResource.Builder() + .setRepoId("1-".concat(Ascii.toUpperCase(tld))) + .setFullyQualifiedDomainName(domainName) + .setPersistedCurrentSponsorClientId("TheRegistrar") + .setCreationClientId("TheRegistrar") + .setCreationTimeForTest(creationTime) + .setRegistrationExpirationTime(expirationTime) + .setRegistrant(Key.create(contact)) + .setContacts( + ImmutableSet.of( + DesignatedContact.create(Type.ADMIN, Key.create(contact)), + DesignatedContact.create(Type.TECH, Key.create(contact)))) + .setAuthInfo(DomainAuthInfo.create(PasswordAuth.create("fooBAR"))) + .addGracePeriod( + GracePeriod.create(GracePeriodStatus.ADD, now.plusDays(10), "foo", null)) + .build(); + HistoryEntry historyEntryDomainCreate = + persistResource( + new HistoryEntry.Builder() + .setType(HistoryEntry.Type.DOMAIN_CREATE) + .setParent(domain) + .build()); + BillingEvent.Recurring autorenewEvent = + persistResource( + new BillingEvent.Recurring.Builder() + .setReason(Reason.RENEW) + .setFlags(ImmutableSet.of(Flag.AUTO_RENEW)) + .setTargetId(domainName) + .setClientId("TheRegistrar") + .setEventTime(expirationTime) + .setRecurrenceEndTime(END_OF_TIME) + .setParent(historyEntryDomainCreate) + .build()); + PollMessage.Autorenew autorenewPollMessage = + persistResource( + new PollMessage.Autorenew.Builder() + .setTargetId(domainName) + .setClientId("TheRegistrar") + .setEventTime(expirationTime) + .setAutorenewEndTime(END_OF_TIME) + .setMsg("Domain was auto-renewed.") + .setParent(historyEntryDomainCreate) + .build()); + return persistResource( + domain + .asBuilder() + .setAutorenewBillingEvent(Key.create(autorenewEvent)) + .setAutorenewPollMessage(Key.create(autorenewPollMessage)) + .build()); + } + public static DomainResource persistDomainWithPendingTransfer( DomainResource domain, DateTime requestTime, diff --git a/javatests/google/registry/testing/TestLogHandlerUtils.java b/javatests/google/registry/testing/TestLogHandlerUtils.java index 702619b29..1256bca32 100644 --- a/javatests/google/registry/testing/TestLogHandlerUtils.java +++ b/javatests/google/registry/testing/TestLogHandlerUtils.java @@ -14,8 +14,12 @@ package google.registry.testing; +import static com.google.common.truth.Truth.assert_; + import com.google.common.collect.Iterables; import com.google.common.testing.TestLogHandler; +import google.registry.util.CapturingLogHandler; +import java.util.logging.Level; import java.util.logging.LogRecord; /** Utility methods for working with Guava's {@link TestLogHandler}. */ @@ -38,4 +42,13 @@ public final class TestLogHandlerUtils { return Iterables.find( handler.getStoredLogRecords(), logRecord -> logRecord.getMessage().startsWith(prefix)); } + + public static void assertLogMessage(CapturingLogHandler handler, Level level, String message) { + for (LogRecord logRecord : handler.getRecords()) { + if (logRecord.getLevel().equals(level) && logRecord.getMessage().contains(message)) { + return; + } + } + assert_().fail(String.format("Log message \"%s\" not found", message)); + } }