mirror of
https://github.com/google/nomulus
synced 2026-05-19 06:11:49 +00:00
Compare commits
7 Commits
nomulus-20
...
nomulus-20
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
548ae25fac | ||
|
|
8393c75929 | ||
|
|
1764ae0b3f | ||
|
|
d76abfc23a | ||
|
|
6af9299a3c | ||
|
|
a53c127573 | ||
|
|
8dbf4fced9 |
@@ -41,11 +41,12 @@ public class BackupUtils {
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts the given {@link ImmutableObject} to a raw Datastore entity and write it to an
|
||||
* {@link OutputStream} in delimited protocol buffer format.
|
||||
* Converts the given {@link ImmutableObject} to a raw Datastore entity and write it to an {@link
|
||||
* OutputStream} in delimited protocol buffer format.
|
||||
*/
|
||||
static void serializeEntity(ImmutableObject entity, OutputStream stream) throws IOException {
|
||||
EntityTranslator.convertToPb(auditedOfy().save().toEntity(entity)).writeDelimitedTo(stream);
|
||||
EntityTranslator.convertToPb(auditedOfy().saveIgnoringReadOnly().toEntity(entity))
|
||||
.writeDelimitedTo(stream);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -74,7 +74,7 @@ public final class CommitLogCheckpointAction implements Runnable {
|
||||
return;
|
||||
}
|
||||
auditedOfy()
|
||||
.saveWithoutBackup()
|
||||
.saveIgnoringReadOnly()
|
||||
.entities(
|
||||
checkpoint, CommitLogCheckpointRoot.create(checkpoint.getCheckpointTime()));
|
||||
// Enqueue a diff task between previous and current checkpoints.
|
||||
|
||||
@@ -140,7 +140,7 @@ public class ReplayCommitLogsToSqlAction implements Runnable {
|
||||
response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
|
||||
response.setPayload(message);
|
||||
} finally {
|
||||
lock.ifPresent(Lock::release);
|
||||
lock.ifPresent(Lock::releaseSql);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -14,18 +14,15 @@
|
||||
|
||||
package google.registry.cron;
|
||||
|
||||
import static com.google.appengine.api.taskqueue.QueueFactory.getQueue;
|
||||
|
||||
import com.google.appengine.api.taskqueue.Queue;
|
||||
import com.google.appengine.api.taskqueue.TaskOptions;
|
||||
import com.google.common.collect.ImmutableMultimap;
|
||||
import google.registry.model.ofy.CommitLogBucket;
|
||||
import google.registry.request.Action;
|
||||
import google.registry.request.Action.Service;
|
||||
import google.registry.request.Parameter;
|
||||
import google.registry.request.auth.Auth;
|
||||
import google.registry.util.TaskQueueUtils;
|
||||
import java.time.Duration;
|
||||
import google.registry.util.Clock;
|
||||
import google.registry.util.CloudTasksUtils;
|
||||
import java.util.Optional;
|
||||
import java.util.Random;
|
||||
import javax.inject.Inject;
|
||||
|
||||
/** Action for fanning out cron tasks for each commit log bucket. */
|
||||
@@ -38,25 +35,27 @@ public final class CommitLogFanoutAction implements Runnable {
|
||||
|
||||
public static final String BUCKET_PARAM = "bucket";
|
||||
|
||||
private static final Random random = new Random();
|
||||
@Inject Clock clock;
|
||||
@Inject CloudTasksUtils cloudTasksUtils;
|
||||
|
||||
@Inject TaskQueueUtils taskQueueUtils;
|
||||
@Inject @Parameter("endpoint") String endpoint;
|
||||
@Inject @Parameter("queue") String queue;
|
||||
@Inject @Parameter("jitterSeconds") Optional<Integer> jitterSeconds;
|
||||
@Inject CommitLogFanoutAction() {}
|
||||
|
||||
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
Queue taskQueue = getQueue(queue);
|
||||
for (int bucketId : CommitLogBucket.getBucketIds()) {
|
||||
long delay =
|
||||
jitterSeconds.map(i -> random.nextInt((int) Duration.ofSeconds(i).toMillis())).orElse(0);
|
||||
TaskOptions taskOptions =
|
||||
TaskOptions.Builder.withUrl(endpoint)
|
||||
.param(BUCKET_PARAM, Integer.toString(bucketId))
|
||||
.countdownMillis(delay);
|
||||
taskQueueUtils.enqueue(taskQueue, taskOptions);
|
||||
cloudTasksUtils.enqueue(
|
||||
queue,
|
||||
CloudTasksUtils.createPostTask(
|
||||
endpoint,
|
||||
Service.BACKEND.toString(),
|
||||
ImmutableMultimap.of(BUCKET_PARAM, Integer.toString(bucketId)),
|
||||
clock,
|
||||
jitterSeconds));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ package google.registry.flows.domain;
|
||||
|
||||
import static com.google.common.base.MoreObjects.firstNonNull;
|
||||
import static com.google.common.collect.ImmutableSet.toImmutableSet;
|
||||
import static com.google.common.collect.ImmutableSortedSet.toImmutableSortedSet;
|
||||
import static com.google.common.collect.Sets.symmetricDifference;
|
||||
import static com.google.common.collect.Sets.union;
|
||||
import static google.registry.flows.FlowUtils.persistEntityChanges;
|
||||
@@ -43,6 +44,9 @@ import static google.registry.model.reporting.HistoryEntry.Type.DOMAIN_UPDATE;
|
||||
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
|
||||
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
import com.google.common.collect.ImmutableSortedSet;
|
||||
import com.google.common.collect.Ordering;
|
||||
import com.google.common.collect.Sets;
|
||||
import com.google.common.net.InternetDomainName;
|
||||
import google.registry.dns.DnsQueue;
|
||||
import google.registry.flows.EppException;
|
||||
@@ -76,6 +80,7 @@ import google.registry.model.eppcommon.StatusValue;
|
||||
import google.registry.model.eppinput.EppInput;
|
||||
import google.registry.model.eppinput.ResourceCommand;
|
||||
import google.registry.model.eppoutput.EppResponse;
|
||||
import google.registry.model.poll.PollMessage;
|
||||
import google.registry.model.reporting.IcannReportingTypes.ActivityReportField;
|
||||
import google.registry.model.tld.Registry;
|
||||
import java.util.Optional;
|
||||
@@ -175,6 +180,9 @@ public final class DomainUpdateFlow implements TransactionalFlow {
|
||||
Optional<BillingEvent.OneTime> statusUpdateBillingEvent =
|
||||
createBillingEventForStatusUpdates(existingDomain, newDomain, domainHistory, now);
|
||||
statusUpdateBillingEvent.ifPresent(entitiesToSave::add);
|
||||
Optional<PollMessage.OneTime> serverStatusUpdatePollMessage =
|
||||
createPollMessageForServerStatusUpdates(existingDomain, newDomain, domainHistory, now);
|
||||
serverStatusUpdatePollMessage.ifPresent(entitiesToSave::add);
|
||||
EntityChanges entityChanges =
|
||||
flowCustomLogic.beforeSave(
|
||||
BeforeSaveParameters.newBuilder()
|
||||
@@ -306,4 +314,50 @@ public final class DomainUpdateFlow implements TransactionalFlow {
|
||||
}
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
/** Enqueues a poll message iff a superuser is adding/removing server statuses. */
|
||||
private Optional<PollMessage.OneTime> createPollMessageForServerStatusUpdates(
|
||||
DomainBase existingDomain, DomainBase newDomain, DomainHistory historyEntry, DateTime now) {
|
||||
if (registrarId.equals(existingDomain.getPersistedCurrentSponsorRegistrarId())) {
|
||||
// Don't send a poll message when a superuser registrar is updating its own domain.
|
||||
return Optional.empty();
|
||||
}
|
||||
ImmutableSortedSet<String> addedServerStatuses =
|
||||
Sets.difference(newDomain.getStatusValues(), existingDomain.getStatusValues()).stream()
|
||||
.filter(StatusValue::isServerSettable)
|
||||
.map(StatusValue::getXmlName)
|
||||
.collect(toImmutableSortedSet(Ordering.natural()));
|
||||
ImmutableSortedSet<String> removedServerStatuses =
|
||||
Sets.difference(existingDomain.getStatusValues(), newDomain.getStatusValues()).stream()
|
||||
.filter(StatusValue::isServerSettable)
|
||||
.map(StatusValue::getXmlName)
|
||||
.collect(toImmutableSortedSet(Ordering.natural()));
|
||||
|
||||
String msg = "";
|
||||
if (addedServerStatuses.size() > 0 && removedServerStatuses.size() > 0) {
|
||||
msg =
|
||||
String.format(
|
||||
"The registry administrator has added the status(es) %s and removed the status(es)"
|
||||
+ " %s.",
|
||||
addedServerStatuses, removedServerStatuses);
|
||||
} else if (addedServerStatuses.size() > 0) {
|
||||
msg =
|
||||
String.format(
|
||||
"The registry administrator has added the status(es) %s.", addedServerStatuses);
|
||||
} else if (removedServerStatuses.size() > 0) {
|
||||
msg =
|
||||
String.format(
|
||||
"The registry administrator has removed the status(es) %s.", removedServerStatuses);
|
||||
} else {
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
return Optional.ofNullable(
|
||||
new PollMessage.OneTime.Builder()
|
||||
.setParent(historyEntry)
|
||||
.setEventTime(now)
|
||||
.setRegistrarId(existingDomain.getCurrentSponsorRegistrarId())
|
||||
.setMsg(msg)
|
||||
.build());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -43,7 +43,6 @@ import google.registry.model.reporting.HistoryEntry;
|
||||
import google.registry.model.server.Lock;
|
||||
import google.registry.model.server.ServerSecret;
|
||||
import google.registry.model.tld.Registry;
|
||||
import google.registry.model.tmch.TmchCrl;
|
||||
|
||||
/** Sets of classes of the Objectify-registered entities in use throughout the model. */
|
||||
public final class EntityClasses {
|
||||
@@ -85,8 +84,7 @@ public final class EntityClasses {
|
||||
Registrar.class,
|
||||
RegistrarContact.class,
|
||||
Registry.class,
|
||||
ServerSecret.class,
|
||||
TmchCrl.class);
|
||||
ServerSecret.class);
|
||||
|
||||
private EntityClasses() {}
|
||||
}
|
||||
|
||||
@@ -27,7 +27,6 @@ import static google.registry.util.PreconditionsUtils.checkArgumentNotNull;
|
||||
|
||||
import com.google.common.collect.ImmutableMap;
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
import com.google.common.collect.Sets;
|
||||
import com.googlecode.objectify.Key;
|
||||
import com.googlecode.objectify.annotation.Entity;
|
||||
import com.googlecode.objectify.annotation.Id;
|
||||
@@ -77,14 +76,29 @@ public abstract class BillingEvent extends ImmutableObject
|
||||
|
||||
/** The reason for the bill, which maps 1:1 to skus in go/registry-billing-skus. */
|
||||
public enum Reason {
|
||||
CREATE,
|
||||
CREATE(true),
|
||||
@Deprecated // TODO(b/31676071): remove this legacy value once old data is cleaned up.
|
||||
ERROR,
|
||||
FEE_EARLY_ACCESS,
|
||||
RENEW,
|
||||
RESTORE,
|
||||
SERVER_STATUS,
|
||||
TRANSFER
|
||||
ERROR(false),
|
||||
FEE_EARLY_ACCESS(true),
|
||||
RENEW(true),
|
||||
RESTORE(true),
|
||||
SERVER_STATUS(false),
|
||||
TRANSFER(true);
|
||||
|
||||
private final boolean requiresPeriod;
|
||||
|
||||
Reason(boolean requiresPeriod) {
|
||||
this.requiresPeriod = requiresPeriod;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether billing events with this reason have a period years associated with them.
|
||||
*
|
||||
* <p>Note that this is an "if an only if" condition.
|
||||
*/
|
||||
public boolean hasPeriodYears() {
|
||||
return requiresPeriod;
|
||||
}
|
||||
}
|
||||
|
||||
/** Set of flags that can be applied to billing events. */
|
||||
@@ -268,7 +282,7 @@ public abstract class BillingEvent extends ImmutableObject
|
||||
public T build() {
|
||||
T instance = getInstance();
|
||||
checkNotNull(instance.reason, "Reason must be set");
|
||||
checkNotNull(instance.clientId, "Client ID must be set");
|
||||
checkNotNull(instance.clientId, "Registrar ID must be set");
|
||||
checkNotNull(instance.eventTime, "Event time must be set");
|
||||
checkNotNull(instance.targetId, "Target ID must be set");
|
||||
checkNotNull(instance.parent, "Parent must be set");
|
||||
@@ -462,17 +476,14 @@ public abstract class BillingEvent extends ImmutableObject
|
||||
checkNotNull(instance.billingTime);
|
||||
checkNotNull(instance.cost);
|
||||
checkState(!instance.cost.isNegative(), "Costs should be non-negative.");
|
||||
ImmutableSet<Reason> reasonsWithPeriods =
|
||||
Sets.immutableEnumSet(
|
||||
Reason.CREATE,
|
||||
Reason.FEE_EARLY_ACCESS,
|
||||
Reason.RENEW,
|
||||
Reason.RESTORE,
|
||||
Reason.TRANSFER);
|
||||
checkState(
|
||||
reasonsWithPeriods.contains(instance.reason) == (instance.periodYears != null),
|
||||
"Period years must be set if and only if reason is "
|
||||
+ "CREATE, FEE_EARLY_ACCESS, RENEW, RESTORE or TRANSFER.");
|
||||
// TODO(mcilwain): Enforce this check on all billing events (not just more recent ones)
|
||||
// post-migration after we add the missing period years values in SQL.
|
||||
if (instance.eventTime.isAfter(DateTime.parse("2019-01-01T00:00:00Z"))) {
|
||||
checkState(
|
||||
instance.reason.hasPeriodYears() == (instance.periodYears != null),
|
||||
"Period years must be set if and only if reason is "
|
||||
+ "CREATE, FEE_EARLY_ACCESS, RENEW, RESTORE or TRANSFER.");
|
||||
}
|
||||
checkState(
|
||||
instance.getFlags().contains(Flag.SYNTHETIC)
|
||||
== (instance.syntheticCreationTime != null),
|
||||
|
||||
@@ -112,7 +112,6 @@ public enum StatusValue implements EppEnum {
|
||||
*/
|
||||
PENDING_UPDATE(AllowedOn.NONE),
|
||||
|
||||
|
||||
/** A non-client-settable status that prevents deletes of EPP resources. */
|
||||
SERVER_DELETE_PROHIBITED(AllowedOn.ALL),
|
||||
|
||||
@@ -162,6 +161,10 @@ public enum StatusValue implements EppEnum {
|
||||
return xmlName.startsWith("client");
|
||||
}
|
||||
|
||||
public boolean isServerSettable() {
|
||||
return xmlName.startsWith("server");
|
||||
}
|
||||
|
||||
public boolean isChargedStatus() {
|
||||
return xmlName.startsWith("server") && xmlName.endsWith("Prohibited");
|
||||
}
|
||||
|
||||
@@ -194,7 +194,7 @@ public class ReplicateToDatastoreAction implements Runnable {
|
||||
response.setStatus(SC_INTERNAL_SERVER_ERROR);
|
||||
response.setPayload(message);
|
||||
} finally {
|
||||
lock.ifPresent(Lock::release);
|
||||
lock.ifPresent(Lock::releaseSql);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -16,25 +16,18 @@ package google.registry.model.tmch;
|
||||
|
||||
import static com.google.common.base.Preconditions.checkNotNull;
|
||||
import static google.registry.persistence.transaction.TransactionManagerFactory.jpaTm;
|
||||
import static google.registry.persistence.transaction.TransactionManagerFactory.ofyTm;
|
||||
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
|
||||
|
||||
import com.googlecode.objectify.annotation.Entity;
|
||||
import google.registry.model.annotations.NotBackedUp;
|
||||
import google.registry.model.annotations.NotBackedUp.Reason;
|
||||
import google.registry.model.common.CrossTldSingleton;
|
||||
import google.registry.model.replay.NonReplicatedEntity;
|
||||
import google.registry.model.replay.SqlOnlyEntity;
|
||||
import java.util.Optional;
|
||||
import javax.annotation.concurrent.Immutable;
|
||||
import javax.persistence.Column;
|
||||
import org.joda.time.DateTime;
|
||||
|
||||
/** Datastore singleton for ICANN's TMCH CA certificate revocation list (CRL). */
|
||||
@Entity
|
||||
/** Singleton for ICANN's TMCH CA certificate revocation list (CRL). */
|
||||
@javax.persistence.Entity
|
||||
@Immutable
|
||||
@NotBackedUp(reason = Reason.EXTERNALLY_SOURCED)
|
||||
public final class TmchCrl extends CrossTldSingleton implements NonReplicatedEntity {
|
||||
public final class TmchCrl extends CrossTldSingleton implements SqlOnlyEntity {
|
||||
|
||||
@Column(name = "certificateRevocations", nullable = false)
|
||||
String crl;
|
||||
@@ -47,25 +40,23 @@ public final class TmchCrl extends CrossTldSingleton implements NonReplicatedEnt
|
||||
|
||||
/** Returns the singleton instance of this entity, without memoization. */
|
||||
public static Optional<TmchCrl> get() {
|
||||
return tm().transact(() -> tm().loadSingleton(TmchCrl.class));
|
||||
return jpaTm().transact(() -> jpaTm().loadSingleton(TmchCrl.class));
|
||||
}
|
||||
|
||||
/**
|
||||
* Change the Datastore singleton to a new ASCII-armored X.509 CRL.
|
||||
* Change the singleton to a new ASCII-armored X.509 CRL.
|
||||
*
|
||||
* <p>Please do not call this function unless your CRL is properly formatted, signed by the root,
|
||||
* and actually newer than the one currently in Datastore.
|
||||
*
|
||||
* <p>During the dual-write period, we write to both Datastore and SQL
|
||||
*/
|
||||
public static void set(final String crl, final String url) {
|
||||
tm().transact(
|
||||
jpaTm()
|
||||
.transact(
|
||||
() -> {
|
||||
TmchCrl tmchCrl = new TmchCrl();
|
||||
tmchCrl.updated = tm().getTransactionTime();
|
||||
tmchCrl.updated = jpaTm().getTransactionTime();
|
||||
tmchCrl.crl = checkNotNull(crl, "crl");
|
||||
tmchCrl.url = checkNotNull(url, "url");
|
||||
ofyTm().transactNew(() -> ofyTm().putWithoutBackup(tmchCrl));
|
||||
jpaTm().transactNew(() -> jpaTm().putWithoutBackup(tmchCrl));
|
||||
});
|
||||
}
|
||||
@@ -80,7 +71,7 @@ public final class TmchCrl extends CrossTldSingleton implements NonReplicatedEnt
|
||||
return crl;
|
||||
}
|
||||
|
||||
/** Time we last updated the Datastore with a newer ICANN CRL. */
|
||||
/** Time we last updated the Database with a newer ICANN CRL. */
|
||||
public final DateTime getUpdated() {
|
||||
return updated;
|
||||
}
|
||||
|
||||
@@ -67,7 +67,6 @@ import google.registry.model.replay.SqlReplayCheckpoint;
|
||||
import google.registry.model.server.Lock;
|
||||
import google.registry.model.tld.label.PremiumList;
|
||||
import google.registry.model.tld.label.PremiumList.PremiumEntry;
|
||||
import google.registry.model.tmch.TmchCrl;
|
||||
import google.registry.model.translators.VKeyTranslatorFactory;
|
||||
import google.registry.persistence.VKey;
|
||||
import google.registry.persistence.transaction.JpaTransactionManager;
|
||||
@@ -482,7 +481,8 @@ public class ReplayCommitLogsToSqlActionTest {
|
||||
|
||||
jpaTm().transact(() -> SqlReplayCheckpoint.set(now.minusMinutes(1).minusMillis(1)));
|
||||
// Save a couple deletes that aren't propagated to SQL (the objects deleted are irrelevant)
|
||||
Key<TmchCrl> tmchCrlKey = Key.create(TmchCrl.class, 1L);
|
||||
Key<CommitLogManifest> manifestKey =
|
||||
CommitLogManifest.createKey(getBucketKey(1), now.minusMinutes(1));
|
||||
saveDiffFile(
|
||||
gcsUtils,
|
||||
createCheckpoint(now.minusMinutes(1)),
|
||||
@@ -490,7 +490,7 @@ public class ReplayCommitLogsToSqlActionTest {
|
||||
getBucketKey(1),
|
||||
now.minusMinutes(1),
|
||||
// one object only exists in Datastore, one is dually-written (so isn't replicated)
|
||||
ImmutableSet.of(getCrossTldKey(), tmchCrlKey)));
|
||||
ImmutableSet.of(getCrossTldKey(), manifestKey)));
|
||||
|
||||
runAndAssertSuccess(now.minusMinutes(1), 1, 1);
|
||||
verify(spy, times(0)).delete(any(VKey.class));
|
||||
|
||||
@@ -15,14 +15,13 @@
|
||||
package google.registry.cron;
|
||||
|
||||
import static google.registry.cron.CommitLogFanoutAction.BUCKET_PARAM;
|
||||
import static google.registry.testing.TaskQueueHelper.assertTasksEnqueued;
|
||||
|
||||
import com.google.common.base.Joiner;
|
||||
import google.registry.model.ofy.CommitLogBucket;
|
||||
import google.registry.testing.AppEngineExtension;
|
||||
import google.registry.testing.TaskQueueHelper.TaskMatcher;
|
||||
import google.registry.util.Retrier;
|
||||
import google.registry.util.TaskQueueUtils;
|
||||
import google.registry.testing.CloudTasksHelper;
|
||||
import google.registry.testing.CloudTasksHelper.TaskMatcher;
|
||||
import google.registry.testing.FakeClock;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
@@ -34,6 +33,7 @@ class CommitLogFanoutActionTest {
|
||||
|
||||
private static final String ENDPOINT = "/the/servlet";
|
||||
private static final String QUEUE = "the-queue";
|
||||
private final CloudTasksHelper cloudTasksHelper = new CloudTasksHelper();
|
||||
|
||||
@RegisterExtension
|
||||
final AppEngineExtension appEngineExtension =
|
||||
@@ -54,15 +54,16 @@ class CommitLogFanoutActionTest {
|
||||
@Test
|
||||
void testSuccess() {
|
||||
CommitLogFanoutAction action = new CommitLogFanoutAction();
|
||||
action.taskQueueUtils = new TaskQueueUtils(new Retrier(null, 1));
|
||||
action.cloudTasksUtils = cloudTasksHelper.getTestCloudTasksUtils();
|
||||
action.endpoint = ENDPOINT;
|
||||
action.queue = QUEUE;
|
||||
action.jitterSeconds = Optional.empty();
|
||||
action.clock = new FakeClock();
|
||||
action.run();
|
||||
List<TaskMatcher> matchers = new ArrayList<>();
|
||||
for (int bucketId : CommitLogBucket.getBucketIds()) {
|
||||
matchers.add(new TaskMatcher().url(ENDPOINT).param(BUCKET_PARAM, Integer.toString(bucketId)));
|
||||
}
|
||||
assertTasksEnqueued(QUEUE, matchers);
|
||||
cloudTasksHelper.assertTasksEnqueued(QUEUE, matchers);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1449,4 +1449,39 @@ class EppLifecycleDomainTest extends EppTestCase {
|
||||
.atTime("2001-01-08T00:00:00Z")
|
||||
.hasResponse("domain_transfer_query_response_completed_fakesite.xml");
|
||||
}
|
||||
|
||||
@TestOfyAndSql
|
||||
void testDomainUpdateBySuperuser_sendsPollMessage() throws Exception {
|
||||
setIsSuperuser(false);
|
||||
assertThatLoginSucceeds("NewRegistrar", "foo-BAR2");
|
||||
createContacts(DateTime.parse("2000-06-01T00:00:00Z"));
|
||||
|
||||
// Create domain example.tld
|
||||
assertThatCommand(
|
||||
"domain_create_no_hosts_or_dsdata.xml", ImmutableMap.of("DOMAIN", "example.tld"))
|
||||
.atTime("2000-06-01T00:02:00Z")
|
||||
.hasResponse(
|
||||
"domain_create_response.xml",
|
||||
ImmutableMap.of(
|
||||
"DOMAIN", "example.tld",
|
||||
"CRDATE", "2000-06-01T00:02:00Z",
|
||||
"EXDATE", "2002-06-01T00:02:00Z"));
|
||||
assertThatLogoutSucceeds();
|
||||
|
||||
setIsSuperuser(true);
|
||||
assertThatLoginSucceeds("TheRegistrar", "password2");
|
||||
// Run domain update that adds server status as superuser
|
||||
assertThatCommand("domain_update_server_hold.xml")
|
||||
.atTime("2000-06-02T13:00:00Z")
|
||||
.hasResponse("generic_success_response.xml");
|
||||
assertThatLogoutSucceeds();
|
||||
setIsSuperuser(false);
|
||||
|
||||
assertThatLoginSucceeds("NewRegistrar", "foo-BAR2");
|
||||
// Verify that the owning registrar now has the correct poll message
|
||||
assertThatCommand("poll.xml")
|
||||
.atTime("2000-06-02T13:00:01Z")
|
||||
.hasResponse("poll_response_server_hold.xml");
|
||||
assertThatLogoutSucceeds();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,12 +19,22 @@ import static com.google.common.collect.Sets.union;
|
||||
import static com.google.common.io.BaseEncoding.base16;
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
import static google.registry.model.EppResourceUtils.loadByForeignKey;
|
||||
import static google.registry.model.eppcommon.StatusValue.CLIENT_DELETE_PROHIBITED;
|
||||
import static google.registry.model.eppcommon.StatusValue.CLIENT_HOLD;
|
||||
import static google.registry.model.eppcommon.StatusValue.CLIENT_RENEW_PROHIBITED;
|
||||
import static google.registry.model.eppcommon.StatusValue.SERVER_DELETE_PROHIBITED;
|
||||
import static google.registry.model.eppcommon.StatusValue.SERVER_HOLD;
|
||||
import static google.registry.model.eppcommon.StatusValue.SERVER_TRANSFER_PROHIBITED;
|
||||
import static google.registry.model.eppcommon.StatusValue.SERVER_UPDATE_PROHIBITED;
|
||||
import static google.registry.model.reporting.HistoryEntry.Type.DOMAIN_CREATE;
|
||||
import static google.registry.model.reporting.HistoryEntry.Type.DOMAIN_UPDATE;
|
||||
import static google.registry.model.tld.Registry.TldState.QUIET_PERIOD;
|
||||
import static google.registry.testing.DatabaseHelper.assertBillingEvents;
|
||||
import static google.registry.testing.DatabaseHelper.assertNoBillingEvents;
|
||||
import static google.registry.testing.DatabaseHelper.assertPollMessagesForResource;
|
||||
import static google.registry.testing.DatabaseHelper.createTld;
|
||||
import static google.registry.testing.DatabaseHelper.getOnlyHistoryEntryOfType;
|
||||
import static google.registry.testing.DatabaseHelper.getPollMessages;
|
||||
import static google.registry.testing.DatabaseHelper.loadByKey;
|
||||
import static google.registry.testing.DatabaseHelper.loadRegistrar;
|
||||
import static google.registry.testing.DatabaseHelper.newDomainBase;
|
||||
@@ -88,7 +98,7 @@ import google.registry.model.domain.DomainHistory;
|
||||
import google.registry.model.domain.secdns.DelegationSignerData;
|
||||
import google.registry.model.eppcommon.StatusValue;
|
||||
import google.registry.model.host.HostResource;
|
||||
import google.registry.model.reporting.HistoryEntry;
|
||||
import google.registry.model.poll.PollMessage;
|
||||
import google.registry.model.tld.Registry;
|
||||
import google.registry.persistence.VKey;
|
||||
import google.registry.testing.DualDatabaseTest;
|
||||
@@ -155,7 +165,7 @@ class DomainUpdateFlowTest extends ResourceFlowTestCase<DomainUpdateFlow, Domain
|
||||
.build());
|
||||
persistResource(
|
||||
new DomainHistory.Builder()
|
||||
.setType(HistoryEntry.Type.DOMAIN_CREATE)
|
||||
.setType(DOMAIN_CREATE)
|
||||
.setModificationTime(clock.nowUtc())
|
||||
.setRegistrarId(domain.getCreationRegistrarId())
|
||||
.setDomain(domain)
|
||||
@@ -179,7 +189,7 @@ class DomainUpdateFlowTest extends ResourceFlowTestCase<DomainUpdateFlow, Domain
|
||||
.build());
|
||||
persistResource(
|
||||
new DomainHistory.Builder()
|
||||
.setType(HistoryEntry.Type.DOMAIN_CREATE)
|
||||
.setType(DOMAIN_CREATE)
|
||||
.setModificationTime(clock.nowUtc())
|
||||
.setRegistrarId(domain.getCreationRegistrarId())
|
||||
.setDomain(domain)
|
||||
@@ -202,8 +212,7 @@ class DomainUpdateFlowTest extends ResourceFlowTestCase<DomainUpdateFlow, Domain
|
||||
.and()
|
||||
.hasAuthInfoPwd("2BARfoo")
|
||||
.and()
|
||||
.hasOneHistoryEntryEachOfTypes(
|
||||
HistoryEntry.Type.DOMAIN_CREATE, HistoryEntry.Type.DOMAIN_UPDATE)
|
||||
.hasOneHistoryEntryEachOfTypes(DOMAIN_CREATE, DOMAIN_UPDATE)
|
||||
.and()
|
||||
.hasLastEppUpdateTime(clock.nowUtc())
|
||||
.and()
|
||||
@@ -329,10 +338,7 @@ class DomainUpdateFlowTest extends ResourceFlowTestCase<DomainUpdateFlow, Domain
|
||||
assertTransactionalFlow(true);
|
||||
runFlowAssertResponse(loadFile("generic_success_response.xml"));
|
||||
DomainBase domain = reloadResourceByForeignKey();
|
||||
assertAboutDomains()
|
||||
.that(domain)
|
||||
.hasOneHistoryEntryEachOfTypes(
|
||||
HistoryEntry.Type.DOMAIN_CREATE, HistoryEntry.Type.DOMAIN_UPDATE);
|
||||
assertAboutDomains().that(domain).hasOneHistoryEntryEachOfTypes(DOMAIN_CREATE, DOMAIN_UPDATE);
|
||||
assertThat(domain.getNameservers()).hasSize(13);
|
||||
// getContacts does not return contacts of type REGISTRANT, so check these separately.
|
||||
assertThat(domain.getContacts()).hasSize(3);
|
||||
@@ -349,12 +355,9 @@ class DomainUpdateFlowTest extends ResourceFlowTestCase<DomainUpdateFlow, Domain
|
||||
persistDomain();
|
||||
runFlow();
|
||||
DomainBase domain = reloadResourceByForeignKey();
|
||||
assertAboutDomains()
|
||||
.that(domain)
|
||||
.hasOneHistoryEntryEachOfTypes(
|
||||
HistoryEntry.Type.DOMAIN_CREATE, HistoryEntry.Type.DOMAIN_UPDATE);
|
||||
assertAboutDomains().that(domain).hasOneHistoryEntryEachOfTypes(DOMAIN_CREATE, DOMAIN_UPDATE);
|
||||
assertAboutHistoryEntries()
|
||||
.that(getOnlyHistoryEntryOfType(domain, HistoryEntry.Type.DOMAIN_UPDATE))
|
||||
.that(getOnlyHistoryEntryOfType(domain, DOMAIN_UPDATE))
|
||||
.hasMetadataReason("domain-update-test")
|
||||
.and()
|
||||
.hasMetadataRequestedByRegistrar(true);
|
||||
@@ -482,10 +485,7 @@ class DomainUpdateFlowTest extends ResourceFlowTestCase<DomainUpdateFlow, Domain
|
||||
clock.advanceOneMilli();
|
||||
runFlowAssertResponse(loadFile("generic_success_response.xml"));
|
||||
DomainBase resource = reloadResourceByForeignKey();
|
||||
assertAboutDomains()
|
||||
.that(resource)
|
||||
.hasOnlyOneHistoryEntryWhich()
|
||||
.hasType(HistoryEntry.Type.DOMAIN_UPDATE);
|
||||
assertAboutDomains().that(resource).hasOnlyOneHistoryEntryWhich().hasType(DOMAIN_UPDATE);
|
||||
assertThat(resource.getDsData())
|
||||
.isEqualTo(
|
||||
expectedDsData.stream()
|
||||
@@ -687,9 +687,7 @@ class DomainUpdateFlowTest extends ResourceFlowTestCase<DomainUpdateFlow, Domain
|
||||
.setBillingTime(clock.nowUtc())
|
||||
.setParent(
|
||||
getOnlyHistoryEntryOfType(
|
||||
reloadResourceByForeignKey(),
|
||||
HistoryEntry.Type.DOMAIN_UPDATE,
|
||||
DomainHistory.class))
|
||||
reloadResourceByForeignKey(), DOMAIN_UPDATE, DomainHistory.class))
|
||||
.build());
|
||||
} else {
|
||||
assertNoBillingEvents();
|
||||
@@ -768,7 +766,7 @@ class DomainUpdateFlowTest extends ResourceFlowTestCase<DomainUpdateFlow, Domain
|
||||
.that(reloadResourceByForeignKey())
|
||||
.hasStatusValue(StatusValue.CLIENT_UPDATE_PROHIBITED)
|
||||
.and()
|
||||
.hasStatusValue(StatusValue.SERVER_HOLD);
|
||||
.hasStatusValue(SERVER_HOLD);
|
||||
}
|
||||
|
||||
private void doSecDnsFailingTest(
|
||||
@@ -910,12 +908,103 @@ class DomainUpdateFlowTest extends ResourceFlowTestCase<DomainUpdateFlow, Domain
|
||||
}
|
||||
|
||||
@TestOfyAndSql
|
||||
void testSuccess_superuserStatusValueNotClientSettable() throws Exception {
|
||||
void testSuccess_superuserCanSetServerStatusValues() throws Exception {
|
||||
setEppInput("domain_update_prohibited_status.xml");
|
||||
persistReferencedEntities();
|
||||
persistDomain();
|
||||
runFlowAssertResponse(
|
||||
CommitMode.LIVE, UserPrivileges.SUPERUSER, loadFile("generic_success_response.xml"));
|
||||
// No poll message because the server status was added by the owning registrar.
|
||||
assertThat(getPollMessages()).isEmpty();
|
||||
}
|
||||
|
||||
@TestOfyAndSql
|
||||
void testSuccess_addingServerStatusValue_sendsPollMessage() throws Exception {
|
||||
setEppInput("domain_update_prohibited_status.xml");
|
||||
persistReferencedEntities();
|
||||
persistDomain();
|
||||
persistResource(
|
||||
reloadResourceByForeignKey()
|
||||
.asBuilder()
|
||||
.setPersistedCurrentSponsorRegistrarId("NewRegistrar")
|
||||
.build());
|
||||
runFlowAssertResponse(
|
||||
CommitMode.LIVE, UserPrivileges.SUPERUSER, loadFile("generic_success_response.xml"));
|
||||
DomainBase updatedDomain = reloadResourceByForeignKey();
|
||||
assertPollMessagesForResource(
|
||||
updatedDomain,
|
||||
new PollMessage.OneTime.Builder()
|
||||
.setEventTime(clock.nowUtc())
|
||||
.setParent(getOnlyHistoryEntryOfType(updatedDomain, DOMAIN_UPDATE))
|
||||
.setRegistrarId("NewRegistrar")
|
||||
.setMsg("The registry administrator has added the status(es) [serverHold].")
|
||||
.build());
|
||||
}
|
||||
|
||||
@TestOfyAndSql
|
||||
void testSuccess_removingServerStatusValue_sendsPollMessage() throws Exception {
|
||||
setEppInput("domain_update_remove_server_statuses.xml");
|
||||
persistReferencedEntities();
|
||||
persistDomain();
|
||||
persistResource(
|
||||
reloadResourceByForeignKey()
|
||||
.asBuilder()
|
||||
.setPersistedCurrentSponsorRegistrarId("NewRegistrar")
|
||||
.setStatusValues(
|
||||
ImmutableSet.of(
|
||||
SERVER_DELETE_PROHIBITED,
|
||||
SERVER_TRANSFER_PROHIBITED,
|
||||
SERVER_UPDATE_PROHIBITED,
|
||||
CLIENT_DELETE_PROHIBITED,
|
||||
CLIENT_RENEW_PROHIBITED,
|
||||
CLIENT_HOLD,
|
||||
SERVER_HOLD))
|
||||
.build());
|
||||
runFlowAssertResponse(
|
||||
CommitMode.LIVE, UserPrivileges.SUPERUSER, loadFile("generic_success_response.xml"));
|
||||
DomainBase updatedDomain = reloadResourceByForeignKey();
|
||||
assertPollMessagesForResource(
|
||||
updatedDomain,
|
||||
new PollMessage.OneTime.Builder()
|
||||
.setEventTime(clock.nowUtc())
|
||||
.setParent(getOnlyHistoryEntryOfType(updatedDomain, DOMAIN_UPDATE))
|
||||
.setRegistrarId("NewRegistrar")
|
||||
.setMsg(
|
||||
"The registry administrator has removed the status(es) [serverHold,"
|
||||
+ " serverTransferProhibited, serverUpdateProhibited].")
|
||||
.build());
|
||||
}
|
||||
|
||||
@TestOfyAndSql
|
||||
void testSuccess_addingAndRemovingServerStatusValues_sendsPollMessage() throws Exception {
|
||||
setEppInput("domain_update_change_server_statuses.xml");
|
||||
persistReferencedEntities();
|
||||
persistDomain();
|
||||
persistResource(
|
||||
reloadResourceByForeignKey()
|
||||
.asBuilder()
|
||||
.setPersistedCurrentSponsorRegistrarId("NewRegistrar")
|
||||
.setStatusValues(
|
||||
ImmutableSet.of(
|
||||
SERVER_DELETE_PROHIBITED,
|
||||
SERVER_TRANSFER_PROHIBITED,
|
||||
CLIENT_DELETE_PROHIBITED,
|
||||
CLIENT_RENEW_PROHIBITED))
|
||||
.build());
|
||||
runFlowAssertResponse(
|
||||
CommitMode.LIVE, UserPrivileges.SUPERUSER, loadFile("generic_success_response.xml"));
|
||||
DomainBase updatedDomain = reloadResourceByForeignKey();
|
||||
assertPollMessagesForResource(
|
||||
updatedDomain,
|
||||
new PollMessage.OneTime.Builder()
|
||||
.setEventTime(clock.nowUtc())
|
||||
.setParent(getOnlyHistoryEntryOfType(updatedDomain, DOMAIN_UPDATE))
|
||||
.setRegistrarId("NewRegistrar")
|
||||
.setMsg(
|
||||
"The registry administrator has added the status(es) [serverHold,"
|
||||
+ " serverRenewProhibited] and removed the status(es)"
|
||||
+ " [serverTransferProhibited].")
|
||||
.build());
|
||||
}
|
||||
|
||||
@TestOfyAndSql
|
||||
|
||||
@@ -416,4 +416,62 @@ public class BillingEventTest extends EntityTestCase {
|
||||
assertThat(recurring.getParentKey()).isEqualTo(Key.create(domainHistory));
|
||||
new BillingEvent.OneTime.Builder().setParent(Key.create(domainHistory));
|
||||
}
|
||||
|
||||
@TestOfyAndSql
|
||||
void testReasonRequiringPeriodYears_missingPeriodYears_throwsException() {
|
||||
IllegalStateException thrown =
|
||||
assertThrows(
|
||||
IllegalStateException.class,
|
||||
() ->
|
||||
new BillingEvent.OneTime.Builder()
|
||||
.setBillingTime(DateTime.parse("2020-02-05T15:33:11Z"))
|
||||
.setEventTime(DateTime.parse("2020-01-05T15:33:11Z"))
|
||||
.setCost(Money.of(USD, 10))
|
||||
.setReason(Reason.RENEW)
|
||||
.setCost(Money.of(USD, 10))
|
||||
.setRegistrarId("TheRegistrar")
|
||||
.setTargetId("example.tld")
|
||||
.setParent(domainHistory)
|
||||
.build());
|
||||
assertThat(thrown)
|
||||
.hasMessageThat()
|
||||
.contains("Period years must be set if and only if reason is");
|
||||
}
|
||||
|
||||
@TestOfyAndSql
|
||||
void testReasonNotRequiringPeriodYears_havingPeriodYears_throwsException() {
|
||||
IllegalStateException thrown =
|
||||
assertThrows(
|
||||
IllegalStateException.class,
|
||||
() ->
|
||||
new BillingEvent.OneTime.Builder()
|
||||
.setBillingTime(DateTime.parse("2020-02-05T15:33:11Z"))
|
||||
.setEventTime(DateTime.parse("2020-01-05T15:33:11Z"))
|
||||
.setCost(Money.of(USD, 10))
|
||||
.setPeriodYears(2)
|
||||
.setReason(Reason.SERVER_STATUS)
|
||||
.setCost(Money.of(USD, 10))
|
||||
.setRegistrarId("TheRegistrar")
|
||||
.setTargetId("example.tld")
|
||||
.setParent(domainHistory)
|
||||
.build());
|
||||
assertThat(thrown)
|
||||
.hasMessageThat()
|
||||
.contains("Period years must be set if and only if reason is");
|
||||
}
|
||||
|
||||
@TestOfyAndSql
|
||||
void testReasonRequiringPeriodYears_missingPeriodYears_isAllowedOnOldData() {
|
||||
// This won't throw even though periodYears is missing on a RESTORE because the event time
|
||||
// is before 2019.
|
||||
new BillingEvent.OneTime.Builder()
|
||||
.setBillingTime(DateTime.parse("2018-02-05T15:33:11Z"))
|
||||
.setEventTime(DateTime.parse("2018-01-05T15:33:11Z"))
|
||||
.setReason(Reason.RESTORE)
|
||||
.setCost(Money.of(USD, 10))
|
||||
.setRegistrarId("TheRegistrar")
|
||||
.setTargetId("example.tld")
|
||||
.setParent(domainHistory)
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,39 +15,26 @@
|
||||
package google.registry.model.tmch;
|
||||
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
import static google.registry.testing.DatabaseHelper.loadByEntity;
|
||||
|
||||
import google.registry.model.EntityTestCase;
|
||||
import google.registry.testing.DualDatabaseTest;
|
||||
import google.registry.testing.TestOfyAndSql;
|
||||
import java.util.Optional;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
/** Unit tests for {@link TmchCrl}. */
|
||||
@DualDatabaseTest
|
||||
public class TmchCrlTest extends EntityTestCase {
|
||||
|
||||
TmchCrlTest() {
|
||||
super(JpaEntityCoverageCheck.ENABLED);
|
||||
}
|
||||
|
||||
@TestOfyAndSql
|
||||
@Test
|
||||
void testSuccess() {
|
||||
assertThat(TmchCrl.get()).isEqualTo(Optional.empty());
|
||||
TmchCrl.set("lolcat", "https://lol.cat");
|
||||
assertThat(TmchCrl.get().get().getCrl()).isEqualTo("lolcat");
|
||||
}
|
||||
|
||||
@TestOfyAndSql
|
||||
void testDualWrite() {
|
||||
TmchCrl expected = new TmchCrl();
|
||||
expected.crl = "lolcat";
|
||||
expected.url = "https://lol.cat";
|
||||
expected.updated = fakeClock.nowUtc();
|
||||
TmchCrl.set("lolcat", "https://lol.cat");
|
||||
assertThat(loadByEntity(new TmchCrl())).isEqualTo(expected);
|
||||
}
|
||||
|
||||
@TestOfyAndSql
|
||||
@Test
|
||||
void testMultipleWrites() {
|
||||
TmchCrl.set("first", "https://first.cat");
|
||||
assertThat(TmchCrl.get().get().getCrl()).isEqualTo("first");
|
||||
|
||||
@@ -3,4 +3,3 @@ Registrar
|
||||
RegistrarContact
|
||||
Registry
|
||||
ServerSecret
|
||||
TmchCrl
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
<epp xmlns="urn:ietf:params:xml:ns:epp-1.0">
|
||||
<command>
|
||||
<update>
|
||||
<domain:update
|
||||
xmlns:domain="urn:ietf:params:xml:ns:domain-1.0">
|
||||
<domain:name>example.tld</domain:name>
|
||||
<domain:add>
|
||||
<domain:status s="serverHold" lang="en">
|
||||
Payment overdue.
|
||||
</domain:status>
|
||||
<domain:status s="serverRenewProhibited"/>
|
||||
<domain:status s="clientUpdateProhibited"/>
|
||||
</domain:add>
|
||||
<domain:rem>
|
||||
<domain:status s="clientRenewProhibited"/>
|
||||
<domain:status s="serverTransferProhibited"/>
|
||||
</domain:rem>
|
||||
<domain:chg />
|
||||
</domain:update>
|
||||
</update>
|
||||
<clTRID>ABC-12345</clTRID>
|
||||
</command>
|
||||
</epp>
|
||||
@@ -0,0 +1,19 @@
|
||||
<epp xmlns="urn:ietf:params:xml:ns:epp-1.0">
|
||||
<command>
|
||||
<update>
|
||||
<domain:update
|
||||
xmlns:domain="urn:ietf:params:xml:ns:domain-1.0">
|
||||
<domain:name>example.tld</domain:name>
|
||||
<domain:add />
|
||||
<domain:rem>
|
||||
<domain:status s="clientRenewProhibited"/>
|
||||
<domain:status s="serverTransferProhibited"/>
|
||||
<domain:status s="serverHold"/>
|
||||
<domain:status s="serverUpdateProhibited"/>
|
||||
</domain:rem>
|
||||
<domain:chg />
|
||||
</domain:update>
|
||||
</update>
|
||||
<clTRID>ABC-12345</clTRID>
|
||||
</command>
|
||||
</epp>
|
||||
@@ -0,0 +1,16 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<epp xmlns="urn:ietf:params:xml:ns:epp-1.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
|
||||
<command>
|
||||
<update>
|
||||
<domain:update
|
||||
xmlns:domain="urn:ietf:params:xml:ns:domain-1.0">
|
||||
<domain:name>example.tld</domain:name>
|
||||
<domain:add>
|
||||
<domain:status s="serverHold" />
|
||||
</domain:add>
|
||||
</domain:update>
|
||||
</update>
|
||||
<clTRID>ABC-12345</clTRID>
|
||||
</command>
|
||||
</epp>
|
||||
@@ -0,0 +1,16 @@
|
||||
<epp xmlns="urn:ietf:params:xml:ns:epp-1.0">
|
||||
<response>
|
||||
<result code="1301">
|
||||
<msg>Command completed successfully; ack to dequeue</msg>
|
||||
</result>
|
||||
<msgQ count="1" id="1-7-TLD-14-16-2000">
|
||||
<qDate>2000-06-02T13:00:00Z</qDate>
|
||||
<msg>The registry administrator has added the status(es) [serverHold].
|
||||
</msg>
|
||||
</msgQ>
|
||||
<trID>
|
||||
<clTRID>ABC-12345</clTRID>
|
||||
<svTRID>server-trid</svTRID>
|
||||
</trID>
|
||||
</response>
|
||||
</epp>
|
||||
@@ -789,13 +789,6 @@ enum google.registry.model.tld.Registry$TldType {
|
||||
REAL;
|
||||
TEST;
|
||||
}
|
||||
class google.registry.model.tmch.TmchCrl {
|
||||
@Id long id;
|
||||
@Parent com.googlecode.objectify.Key<google.registry.model.common.EntityGroupRoot> parent;
|
||||
java.lang.String crl;
|
||||
java.lang.String url;
|
||||
org.joda.time.DateTime updated;
|
||||
}
|
||||
class google.registry.model.transfer.ContactTransferData {
|
||||
google.registry.model.eppcommon.Trid transferRequestTrid;
|
||||
google.registry.model.transfer.TransferStatus transferStatus;
|
||||
|
||||
@@ -16,6 +16,7 @@ package google.registry.util;
|
||||
|
||||
import static com.google.common.base.Preconditions.checkArgument;
|
||||
import static com.google.common.collect.ImmutableList.toImmutableList;
|
||||
import static java.util.concurrent.TimeUnit.SECONDS;
|
||||
|
||||
import com.google.api.gax.rpc.ApiException;
|
||||
import com.google.cloud.tasks.v2.AppEngineHttpRequest;
|
||||
@@ -34,9 +35,13 @@ import com.google.common.net.HttpHeaders;
|
||||
import com.google.common.net.MediaType;
|
||||
import com.google.common.net.UrlEscapers;
|
||||
import com.google.protobuf.ByteString;
|
||||
import com.google.protobuf.Timestamp;
|
||||
import java.io.Serializable;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.time.Instant;
|
||||
import java.util.Arrays;
|
||||
import java.util.Optional;
|
||||
import java.util.Random;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
/** Utilities for dealing with Cloud Tasks. */
|
||||
@@ -44,6 +49,7 @@ public class CloudTasksUtils implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = -7605156291755534069L;
|
||||
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
|
||||
private static final Random random = new Random();
|
||||
|
||||
private final Retrier retrier;
|
||||
private final String projectId;
|
||||
@@ -88,7 +94,7 @@ public class CloudTasksUtils implements Serializable {
|
||||
* Queue API if no service is specified, the service which enqueues the task will be used to
|
||||
* process the task. Cloud Tasks API does not support this feature so the service will always
|
||||
* needs to be explicitly specified.
|
||||
* @param params A multi-map of URL query parameters. Duplicate keys are saved as is, and it is up
|
||||
* @param params a multi-map of URL query parameters. Duplicate keys are saved as is, and it is up
|
||||
* to the server to process the duplicate keys.
|
||||
* @return the enqueued task.
|
||||
* @see <a
|
||||
@@ -129,6 +135,49 @@ public class CloudTasksUtils implements Serializable {
|
||||
return Task.newBuilder().setAppEngineHttpRequest(requestBuilder.build()).build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a {@link Task} to be enqueued with a random delay up to {@code jitterSeconds}.
|
||||
*
|
||||
* @param path the relative URI (staring with a slash and ending without one).
|
||||
* @param method the HTTP method to be used for the request, only GET and POST are supported.
|
||||
* @param service the App Engine service to route the request to. Note that with App Engine Task
|
||||
* Queue API if no service is specified, the service which enqueues the task will be used to
|
||||
* process the task. Cloud Tasks API does not support this feature so the service will always
|
||||
* needs to be explicitly specified.
|
||||
* @param params a multi-map of URL query parameters. Duplicate keys are saved as is, and it is up
|
||||
* to the server to process the duplicate keys.
|
||||
* @param clock a source of time.
|
||||
* @param jitterSeconds the number of seconds that a task is randomly delayed up to.
|
||||
* @return the enqueued task.
|
||||
* @see <a
|
||||
* href=ttps://cloud.google.com/appengine/docs/standard/java/taskqueue/push/creating-tasks#target>Specifyinig
|
||||
* the worker service</a>
|
||||
*/
|
||||
private static Task createTask(
|
||||
String path,
|
||||
HttpMethod method,
|
||||
String service,
|
||||
Multimap<String, String> params,
|
||||
Clock clock,
|
||||
Optional<Integer> jitterSeconds) {
|
||||
if (!jitterSeconds.isPresent() || jitterSeconds.get() <= 0) {
|
||||
return createTask(path, method, service, params);
|
||||
}
|
||||
Instant scheduleTime =
|
||||
Instant.ofEpochMilli(
|
||||
clock
|
||||
.nowUtc()
|
||||
.plusMillis(random.nextInt((int) SECONDS.toMillis(jitterSeconds.get())))
|
||||
.getMillis());
|
||||
return Task.newBuilder(createTask(path, method, service, params))
|
||||
.setScheduleTime(
|
||||
Timestamp.newBuilder()
|
||||
.setSeconds(scheduleTime.getEpochSecond())
|
||||
.setNanos(scheduleTime.getNano())
|
||||
.build())
|
||||
.build();
|
||||
}
|
||||
|
||||
public static Task createPostTask(String path, String service, Multimap<String, String> params) {
|
||||
return createTask(path, HttpMethod.POST, service, params);
|
||||
}
|
||||
@@ -137,6 +186,30 @@ public class CloudTasksUtils implements Serializable {
|
||||
return createTask(path, HttpMethod.GET, service, params);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a {@link Task} via HTTP.POST that will be randomly delayed up to {@code jitterSeconds}.
|
||||
*/
|
||||
public static Task createPostTask(
|
||||
String path,
|
||||
String service,
|
||||
Multimap<String, String> params,
|
||||
Clock clock,
|
||||
Optional<Integer> jitterSeconds) {
|
||||
return createTask(path, HttpMethod.POST, service, params, clock, jitterSeconds);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a {@link Task} via HTTP.GET that will be randomly delayed up to {@code jitterSeconds}.
|
||||
*/
|
||||
public static Task createGetTask(
|
||||
String path,
|
||||
String service,
|
||||
Multimap<String, String> params,
|
||||
Clock clock,
|
||||
Optional<Integer> jitterSeconds) {
|
||||
return createTask(path, HttpMethod.GET, service, params, clock, jitterSeconds);
|
||||
}
|
||||
|
||||
public abstract static class SerializableCloudTasksClient implements Serializable {
|
||||
public abstract Task enqueue(String projectId, String locationId, String queueName, Task task);
|
||||
}
|
||||
|
||||
@@ -30,6 +30,9 @@ import google.registry.testing.FakeClock;
|
||||
import google.registry.testing.FakeSleeper;
|
||||
import google.registry.util.CloudTasksUtils.SerializableCloudTasksClient;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.time.Instant;
|
||||
import java.util.Optional;
|
||||
import org.joda.time.DateTime;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
@@ -41,6 +44,7 @@ public class CloudTasksUtilsTest {
|
||||
private final CloudTasksUtils cloudTasksUtils =
|
||||
new CloudTasksUtils(
|
||||
new Retrier(new FakeSleeper(new FakeClock()), 1), "project", "location", mockClient);
|
||||
private final Clock clock = new FakeClock(DateTime.parse("2021-11-08"));
|
||||
|
||||
@BeforeEach
|
||||
void beforeEach() {
|
||||
@@ -59,6 +63,7 @@ public class CloudTasksUtilsTest {
|
||||
.isEqualTo("/the/path?key1=val1&key2=val2&key1=val3");
|
||||
assertThat(task.getAppEngineHttpRequest().getAppEngineRouting().getService())
|
||||
.isEqualTo("myservice");
|
||||
assertThat(task.getScheduleTime().getSeconds()).isEqualTo(0);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -72,6 +77,103 @@ public class CloudTasksUtilsTest {
|
||||
.isEqualTo("application/x-www-form-urlencoded");
|
||||
assertThat(task.getAppEngineHttpRequest().getBody().toString(StandardCharsets.UTF_8))
|
||||
.isEqualTo("key1=val1&key2=val2&key1=val3");
|
||||
assertThat(task.getScheduleTime().getSeconds()).isEqualTo(0);
|
||||
}
|
||||
|
||||
@SuppressWarnings("ProtoTimestampGetSecondsGetNano")
|
||||
@Test
|
||||
void testSuccess_createGetTasks_withJitterSeconds() {
|
||||
Task task =
|
||||
CloudTasksUtils.createGetTask("/the/path", "myservice", params, clock, Optional.of(100));
|
||||
assertThat(task.getAppEngineHttpRequest().getHttpMethod()).isEqualTo(HttpMethod.GET);
|
||||
assertThat(task.getAppEngineHttpRequest().getRelativeUri())
|
||||
.isEqualTo("/the/path?key1=val1&key2=val2&key1=val3");
|
||||
assertThat(task.getAppEngineHttpRequest().getAppEngineRouting().getService())
|
||||
.isEqualTo("myservice");
|
||||
|
||||
Instant scheduleTime = Instant.ofEpochSecond(task.getScheduleTime().getSeconds());
|
||||
Instant lowerBoundTime = Instant.ofEpochMilli(clock.nowUtc().getMillis());
|
||||
Instant upperBound = Instant.ofEpochMilli(clock.nowUtc().plusSeconds(100).getMillis());
|
||||
|
||||
assertThat(scheduleTime.isBefore(lowerBoundTime)).isFalse();
|
||||
assertThat(upperBound.isBefore(scheduleTime)).isFalse();
|
||||
}
|
||||
|
||||
@SuppressWarnings("ProtoTimestampGetSecondsGetNano")
|
||||
@Test
|
||||
void testSuccess_createPostTasks_withJitterSeconds() {
|
||||
Task task =
|
||||
CloudTasksUtils.createPostTask("/the/path", "myservice", params, clock, Optional.of(1));
|
||||
assertThat(task.getAppEngineHttpRequest().getHttpMethod()).isEqualTo(HttpMethod.POST);
|
||||
assertThat(task.getAppEngineHttpRequest().getRelativeUri()).isEqualTo("/the/path");
|
||||
assertThat(task.getAppEngineHttpRequest().getAppEngineRouting().getService())
|
||||
.isEqualTo("myservice");
|
||||
assertThat(task.getAppEngineHttpRequest().getHeadersMap().get("Content-Type"))
|
||||
.isEqualTo("application/x-www-form-urlencoded");
|
||||
assertThat(task.getAppEngineHttpRequest().getBody().toString(StandardCharsets.UTF_8))
|
||||
.isEqualTo("key1=val1&key2=val2&key1=val3");
|
||||
assertThat(task.getScheduleTime().getSeconds()).isNotEqualTo(0);
|
||||
|
||||
Instant scheduleTime = Instant.ofEpochSecond(task.getScheduleTime().getSeconds());
|
||||
Instant lowerBoundTime = Instant.ofEpochMilli(clock.nowUtc().getMillis());
|
||||
Instant upperBound = Instant.ofEpochMilli(clock.nowUtc().plusSeconds(1).getMillis());
|
||||
|
||||
assertThat(scheduleTime.isBefore(lowerBoundTime)).isFalse();
|
||||
assertThat(upperBound.isBefore(scheduleTime)).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSuccess_createPostTasks_withEmptyJitterSeconds() {
|
||||
Task task =
|
||||
CloudTasksUtils.createPostTask("/the/path", "myservice", params, clock, Optional.empty());
|
||||
assertThat(task.getAppEngineHttpRequest().getHttpMethod()).isEqualTo(HttpMethod.POST);
|
||||
assertThat(task.getAppEngineHttpRequest().getRelativeUri()).isEqualTo("/the/path");
|
||||
assertThat(task.getAppEngineHttpRequest().getAppEngineRouting().getService())
|
||||
.isEqualTo("myservice");
|
||||
assertThat(task.getAppEngineHttpRequest().getHeadersMap().get("Content-Type"))
|
||||
.isEqualTo("application/x-www-form-urlencoded");
|
||||
assertThat(task.getAppEngineHttpRequest().getBody().toString(StandardCharsets.UTF_8))
|
||||
.isEqualTo("key1=val1&key2=val2&key1=val3");
|
||||
assertThat(task.getScheduleTime().getSeconds()).isEqualTo(0);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSuccess_createGetTasks_withEmptyJitterSeconds() {
|
||||
Task task =
|
||||
CloudTasksUtils.createGetTask("/the/path", "myservice", params, clock, Optional.empty());
|
||||
assertThat(task.getAppEngineHttpRequest().getHttpMethod()).isEqualTo(HttpMethod.GET);
|
||||
assertThat(task.getAppEngineHttpRequest().getRelativeUri())
|
||||
.isEqualTo("/the/path?key1=val1&key2=val2&key1=val3");
|
||||
assertThat(task.getAppEngineHttpRequest().getAppEngineRouting().getService())
|
||||
.isEqualTo("myservice");
|
||||
assertThat(task.getScheduleTime().getSeconds()).isEqualTo(0);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSuccess_createPostTasks_withZeroJitterSeconds() {
|
||||
Task task =
|
||||
CloudTasksUtils.createPostTask("/the/path", "myservice", params, clock, Optional.of(0));
|
||||
assertThat(task.getAppEngineHttpRequest().getHttpMethod()).isEqualTo(HttpMethod.POST);
|
||||
assertThat(task.getAppEngineHttpRequest().getRelativeUri()).isEqualTo("/the/path");
|
||||
assertThat(task.getAppEngineHttpRequest().getAppEngineRouting().getService())
|
||||
.isEqualTo("myservice");
|
||||
assertThat(task.getAppEngineHttpRequest().getHeadersMap().get("Content-Type"))
|
||||
.isEqualTo("application/x-www-form-urlencoded");
|
||||
assertThat(task.getAppEngineHttpRequest().getBody().toString(StandardCharsets.UTF_8))
|
||||
.isEqualTo("key1=val1&key2=val2&key1=val3");
|
||||
assertThat(task.getScheduleTime().getSeconds()).isEqualTo(0);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSuccess_createGetTasks_withZeroJitterSeconds() {
|
||||
Task task =
|
||||
CloudTasksUtils.createGetTask("/the/path", "myservice", params, clock, Optional.of(0));
|
||||
assertThat(task.getAppEngineHttpRequest().getHttpMethod()).isEqualTo(HttpMethod.GET);
|
||||
assertThat(task.getAppEngineHttpRequest().getRelativeUri())
|
||||
.isEqualTo("/the/path?key1=val1&key2=val2&key1=val3");
|
||||
assertThat(task.getAppEngineHttpRequest().getAppEngineRouting().getService())
|
||||
.isEqualTo("myservice");
|
||||
assertThat(task.getScheduleTime().getSeconds()).isEqualTo(0);
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
Reference in New Issue
Block a user