1
0
mirror of https://github.com/google/nomulus synced 2026-05-19 06:11:49 +00:00

Compare commits

...

7 Commits

Author SHA1 Message Date
Rachel Guan
548ae25fac Change Optional::isEmpty to Optional::isPresent (#1428) 2021-11-18 17:08:15 -05:00
gbrodman
8393c75929 Ignore read-only mode when running commit logs / backups (#1424)
We need to be able to continue running the backup and async replay code
while the database is in read-only mode
2021-11-18 15:42:23 -05:00
sarahcaseybot
1764ae0b3f Remove TmchCrl singleton from Datastore (#1419) 2021-11-17 14:53:29 -05:00
Rachel Guan
d76abfc23a Change TaskQueueUtils to CloudTaskUtils in CommitLogFanoutAction (#1408)
* Change TaskOptions to Task in CommitLogFanoutAction

* Add a createTask method that takes clock and jitterSeconds

* Change CreateTask parameter type and improve test cases

* Improve comments and test casse

* Improve test cases that handel jitterSeconds
2021-11-17 10:54:42 -05:00
Ben McIlwain
6af9299a3c Grandfather in old data for one-time billing event requirement (#1423)
* Grandfather in old data for one-time billing event requirement

We have data from 2018 and earlier where we didn't consistently set periodYears
for OneTime BillingEvents with certain reasons. This grandfathers in that old
data so that we can successfully move it over to Cloud SQL for now, then we can
later run a query that will backfill it, after which we can then tighten up the
requirement again. Note that the requirement is still being enforced for all
billing events from 2019 onwards.

This also improves the handling of validation, by adding a private field to the
Reason enum rather than creating a throwaway inline ImmmutableSet in the
Builder.
2021-11-16 16:12:08 -05:00
gbrodman
a53c127573 Release the replay lock in SQL, not Datastore (#1422)
* Release the replay lock in SQL, not Datastore

It's always acquired in SQL, so it should always be released in SQL.
2021-11-16 11:37:20 -05:00
Ben McIlwain
8dbf4fced9 Send registrars poll messages when we add/remove server-side statuses (#1417)
* Send registrars poll messages when we add/remove server-side status values
2021-11-16 11:35:05 -05:00
24 changed files with 590 additions and 122 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -3,4 +3,3 @@ Registrar
RegistrarContact
Registry
ServerSecret
TmchCrl

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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