1
0
mirror of https://github.com/google/nomulus synced 2026-07-08 09:06:58 +00:00

Compare commits

...

17 Commits

Author SHA1 Message Date
sarahcaseybot 56d1ea71fe Remove BillingId from schema (#1641)
* Remove BillingId from schema

* Add back java changes

* Add transient tag

* Remove java

* Remove flyway files
2022-05-26 15:28:58 -04:00
Ben McIlwain fd3cec2a0f Slightly improve performance of ExpandRecurringBillingEventsAction (#1642)
* Slightly improve performance of ExpandRecurringBillingEventsAction

We don't need to log every single no-op batch of 50 Recurrences that are
processed (considering we have 1.5M total in our system), and we also don't need
to process Recurrences that already ended prior to the Cursor time (this gets us
down to 420k from 1.5M).
2022-05-25 17:27:21 -04:00
Michael Muller b9c40dd68d Disable Ofy tests. (#1644)
* Disable Ofy tests.

This change just turns off the Ofy tests at the root, by removing processing
for dual tests and disassociating the TestOfyOnly annotation from test
annotations.

This is far less comprehensive than #1631, but it's probably worth entering as
a stopgap solution just because it should speed up our test runs and unblock a
lot of other cleanup work.

* Fix DualDatabaseTestInvocationContextProviderTest
2022-05-25 09:47:05 -04:00
Lai Jiang 754958ef3a Remove an unnecessary pair of parentheses. (#1645) 2022-05-24 17:37:10 -04:00
Michael Muller 1bf7c925bc Fix style warning (#1643)
THe build is giving a style warning over the extra parens.
2022-05-24 17:22:08 -04:00
Weimin Yu eeca51667e Optimize RDAP entity event query (#1635)
* Optimize RDAP entity event query

For each EPP entity, directly load the latest HistoryEntry per event type
instead of loading all events through the HistoryEntryDao.

Although most entities have a small number of history entries, there are
a few entities with many entries, enough to cause OutOfMemory error.
2022-05-19 23:35:55 -04:00
sarahcaseybot 123d6359dc Change shouldPublish in GenerateInvoicesAction to default to false (#1640) 2022-05-19 17:51:21 -04:00
Rachel Guan 64fba55f06 Add renewal cost logic to DomainPricingLogic (#1610)
* Add renew cost calculation to DomainPricingLogic

* Fix typos and change assertions
2022-05-19 16:05:21 -04:00
gbrodman 3a7ac669f5 Set up jpaTm before loading data in the test server (#1633) 2022-05-19 12:13:43 -04:00
Michael Muller fc029b5ad2 Added info on problematic max-instances param (#1639)
We have backend max-instances set to 100, which apparently exceeds the default
quota for GAE.  Add info on updating the quota or changing this parameter to
the configuration doc.
2022-05-19 11:51:27 -04:00
Ben McIlwain ec5c2cdb68 Add batching to ExpandRecurringBillingEventsAction (#1636)
* Add batching to ExpandRecurringBillingEventsAction

It's OOMing on trying to load every single BillingRecurrence that needs to be
expanded simultaneously (which is to be expected). So this processes them in
transactional batches of 50.
2022-05-19 09:13:37 -04:00
Lai Jiang c4cf128844 Make Caffeine cache loading work in non-GAE thread (#1634) 2022-05-18 22:01:44 -04:00
sarahcaseybot c262ef82c9 Remove all uses of the billingIdentifier field (#1608)
* Remove all uses of the billingIdentifier field

* Add @ignore flag

* Add tag
2022-05-18 17:15:45 -04:00
Lai Jiang 03ca6cecc7 Add the ability to nullify the entire billing account map (#1630) 2022-05-17 10:33:42 -04:00
Weimin Yu bee5e0a5a9 Verify schema using Cloud Build (#1627)
* Add tool to compare  golden and actual schema
2022-05-16 16:10:09 -04:00
Michael Muller 9f0138aeb2 Cleanup gpg-agent instances and home directories (#1629)
* Cleanup gpg-agent instances and home directories

The GpgSystemCommandException leaks home directories, but more importantly it
leaks gpg-agent instances.  This can cause problems with inotify limits, since
the agent seems to make use of inotify.  Do a proper cleanup in afterEach().

* Don't fail if we can't kill the agent
2022-05-16 14:06:26 -04:00
Rachel Guan eca2b61d8b Add renewal support for AT and internal registrations in DomainCreateFlow (#1591)
* Add renewal info to create flow

* Improve PR
2022-05-13 10:04:15 -04:00
47 changed files with 1373 additions and 303 deletions
@@ -17,6 +17,7 @@ package google.registry.batch;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.collect.ImmutableSet.toImmutableSet;
import static com.google.common.collect.Sets.difference;
import static com.google.common.collect.Sets.newHashSet;
import static google.registry.mapreduce.MapreduceRunner.PARAM_DRY_RUN;
import static google.registry.mapreduce.inputs.EppResourceInputs.createChildEntityInput;
import static google.registry.model.common.Cursor.CursorType.RECURRING_BILLING;
@@ -36,11 +37,13 @@ import static google.registry.util.DomainNameUtils.getTldFromDomainName;
import com.google.appengine.tools.mapreduce.Mapper;
import com.google.appengine.tools.mapreduce.Reducer;
import com.google.appengine.tools.mapreduce.ReducerInput;
import com.google.auto.value.AutoValue;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Range;
import com.google.common.collect.Streams;
import com.google.common.flogger.FluentLogger;
import google.registry.config.RegistryConfig.Config;
import google.registry.mapreduce.MapreduceRunner;
import google.registry.mapreduce.inputs.NullInput;
import google.registry.model.ImmutableObject;
@@ -61,8 +64,10 @@ import google.registry.request.Parameter;
import google.registry.request.Response;
import google.registry.request.auth.Auth;
import google.registry.util.Clock;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import javax.inject.Inject;
import org.joda.money.Money;
import org.joda.time.DateTime;
@@ -86,6 +91,11 @@ public class ExpandRecurringBillingEventsAction implements Runnable {
@Inject Clock clock;
@Inject MapreduceRunner mrRunner;
@Inject
@Config("jdbcBatchSize")
int batchSize;
@Inject @Parameter(PARAM_DRY_RUN) boolean isDryRun;
@Inject @Parameter(PARAM_CURSOR_TIME) Optional<DateTime> cursorTimeParam;
@Inject Response response;
@@ -120,61 +130,130 @@ public class ExpandRecurringBillingEventsAction implements Runnable {
ImmutableSet.of(DomainBase.class), ImmutableSet.of(Recurring.class))))
.sendLinkToMapreduceConsole(response);
} else {
int numBillingEventsSaved =
jpaTm()
.transact(
() ->
jpaTm()
.query(
"FROM BillingRecurrence "
+ "WHERE eventTime <= :executeTime "
+ "AND eventTime < recurrenceEndTime "
+ "ORDER BY id ASC",
Recurring.class)
.setParameter("executeTime", executeTime)
// Need to get a list from the transaction and then convert it to a stream
// for further processing. If we get a stream directly, each elements gets
// processed downstream eagerly but Hibernate returns a
// ScrollableResultsIterator that cannot be advanced outside the
// transaction, resulting in an exception.
.getResultList())
.stream()
.map(
recurring ->
jpaTm()
.transact(
() ->
expandBillingEvent(recurring, executeTime, cursorTime, isDryRun)))
.reduce(0, Integer::sum);
if (!isDryRun) {
logger.atInfo().log("Saved OneTime billing events.", numBillingEventsSaved);
} else {
logger.atInfo().log("Generated OneTime billing events (dry run).", numBillingEventsSaved);
}
logger.atInfo().log(
"Recurring event expansion %s complete for billing event range [%s, %s).",
isDryRun ? "(dry run) " : "", cursorTime, executeTime);
tm().transact(
() -> {
// Check for the unlikely scenario where the cursor has been altered during the
// expansion.
DateTime currentCursorTime =
tm().loadByKeyIfPresent(Cursor.createGlobalVKey(RECURRING_BILLING))
.orElse(Cursor.createGlobal(RECURRING_BILLING, START_OF_TIME))
.getCursorTime();
if (!currentCursorTime.equals(persistedCursorTime)) {
throw new IllegalStateException(
String.format(
"Current cursor position %s does not match persisted cursor position %s.",
currentCursorTime, persistedCursorTime));
}
if (!isDryRun) {
tm().put(Cursor.createGlobal(RECURRING_BILLING, executeTime));
}
});
expandSqlBillingEventsInBatches(executeTime, cursorTime, persistedCursorTime);
}
}
private void expandSqlBillingEventsInBatches(
DateTime executeTime, DateTime cursorTime, DateTime persistedCursorTime) {
int totalBillingEventsSaved = 0;
long maxProcessedRecurrenceId = 0;
SqlBatchResults sqlBatchResults;
do {
final long prevMaxProcessedRecurrenceId = maxProcessedRecurrenceId;
sqlBatchResults =
jpaTm()
.transact(
() -> {
Set<String> expandedDomains = newHashSet();
int batchBillingEventsSaved = 0;
long maxRecurrenceId = prevMaxProcessedRecurrenceId;
List<Recurring> recurrings =
jpaTm()
.query(
"FROM BillingRecurrence "
+ "WHERE eventTime <= :executeTime "
+ "AND eventTime < recurrenceEndTime "
+ "AND id > :maxProcessedRecurrenceId "
+ "AND recurrenceEndTime > :cursorTime "
+ "ORDER BY id ASC",
Recurring.class)
.setParameter("executeTime", executeTime)
.setParameter("maxProcessedRecurrenceId", prevMaxProcessedRecurrenceId)
.setParameter("cursorTime", cursorTime)
.setMaxResults(batchSize)
.getResultList();
for (Recurring recurring : recurrings) {
if (expandedDomains.contains(recurring.getTargetId())) {
// On the off chance this batch contains multiple recurrences for the same
// domain (which is actually possible if a given domain is quickly renewed
// multiple times in a row), then short-circuit after the first one is
// processed that involves actually expanding a billing event. This is
// necessary because otherwise we get an "Inserted/updated object reloaded"
// error from Hibernate when those billing events would be loaded
// inside a transaction where they were already written. Note, there is no
// actual further work to be done in this case anyway, not unless it has
// somehow been over a year since this action last ran successfully (and if
// that were somehow true, the remaining billing events would still be
// expanded on subsequent runs).
continue;
}
int billingEventsSaved =
expandBillingEvent(recurring, executeTime, cursorTime, isDryRun);
batchBillingEventsSaved += billingEventsSaved;
if (billingEventsSaved > 0) {
expandedDomains.add(recurring.getTargetId());
}
maxRecurrenceId = Math.max(maxRecurrenceId, recurring.getId());
}
return SqlBatchResults.create(
batchBillingEventsSaved,
maxRecurrenceId,
maxRecurrenceId > prevMaxProcessedRecurrenceId);
});
totalBillingEventsSaved += sqlBatchResults.batchBillingEventsSaved();
maxProcessedRecurrenceId = sqlBatchResults.maxProcessedRecurrenceId();
if (sqlBatchResults.batchBillingEventsSaved() > 0) {
logger.atInfo().log(
"Saved %d billing events in batch (%d total) with max recurrence id %d.",
sqlBatchResults.batchBillingEventsSaved(),
totalBillingEventsSaved,
maxProcessedRecurrenceId);
} else {
// If we're churning through a lot of no-op recurrences that don't need expanding (yet?),
// then only log one no-op every so often as a good balance between letting the user track
// that the action is still running while also not spamming the logs incessantly.
logger.atInfo().atMostEvery(3, TimeUnit.MINUTES).log(
"Processed up to max recurrence id %d (no billing events saved recently).",
maxProcessedRecurrenceId);
}
} while (sqlBatchResults.shouldContinue());
if (!isDryRun) {
logger.atInfo().log("Saved %d total OneTime billing events.", totalBillingEventsSaved);
} else {
logger.atInfo().log(
"Generated %d total OneTime billing events (dry run).", totalBillingEventsSaved);
}
logger.atInfo().log(
"Recurring event expansion %s complete for billing event range [%s, %s).",
isDryRun ? "(dry run) " : "", cursorTime, executeTime);
tm().transact(
() -> {
// Check for the unlikely scenario where the cursor has been altered during the
// expansion.
DateTime currentCursorTime =
tm().loadByKeyIfPresent(Cursor.createGlobalVKey(RECURRING_BILLING))
.orElse(Cursor.createGlobal(RECURRING_BILLING, START_OF_TIME))
.getCursorTime();
if (!currentCursorTime.equals(persistedCursorTime)) {
throw new IllegalStateException(
String.format(
"Current cursor position %s does not match persisted cursor position %s.",
currentCursorTime, persistedCursorTime));
}
if (!isDryRun) {
tm().put(Cursor.createGlobal(RECURRING_BILLING, executeTime));
}
});
}
@AutoValue
abstract static class SqlBatchResults {
abstract int batchBillingEventsSaved();
abstract long maxProcessedRecurrenceId();
abstract boolean shouldContinue();
static SqlBatchResults create(
int batchBillingEventsSaved, long maxProcessedRecurrenceId, boolean shouldContinue) {
return new AutoValue_ExpandRecurringBillingEventsAction_SqlBatchResults(
batchBillingEventsSaved, maxProcessedRecurrenceId, shouldContinue);
}
}
/** Mapper to expand {@link Recurring} billing events into synthetic {@link OneTime} events. */
public static class ExpandRecurringBillingEventsMapper
extends Mapper<Recurring, DateTime, DateTime> {
@@ -688,7 +688,7 @@ public final class RegistryConfig {
@Provides
@Config("defaultShouldPublishInvoices")
public static boolean provideDefaultShouldPublishInvoices() {
return true;
return false;
}
/**
@@ -1351,6 +1351,12 @@ public final class RegistryConfig {
public static int provideWipeOutQueryBatchSize(RegistryConfigSettings config) {
return config.contactHistory.wipeOutQueryBatchSize;
}
@Provides
@Config("jdbcBatchSize")
public static int provideHibernateJdbcBatchSize(RegistryConfigSettings config) {
return config.hibernate.jdbcBatchSize;
}
}
/** Returns the App Engine project ID, which is based off the environment name. */
@@ -1555,7 +1561,7 @@ public final class RegistryConfig {
* https://docs.jboss.org/hibernate/orm/5.6/userguide/html_single/Hibernate_User_Guide.html,
* recommend between 10 and 50.
*/
public static String getHibernateJdbcBatchSize() {
public static int getHibernateJdbcBatchSize() {
return CONFIG_SETTINGS.get().hibernate.jdbcBatchSize;
}
@@ -120,7 +120,7 @@ public class RegistryConfigSettings {
public String hikariMinimumIdle;
public String hikariMaximumPoolSize;
public String hikariIdleTimeout;
public String jdbcBatchSize;
public int jdbcBatchSize;
public String jdbcFetchSize;
}
@@ -255,7 +255,7 @@
</cron>
<cron>
<url><![CDATA[/_dr/cron/fanout?queue=retryable-cron-tasks&endpoint=/_dr/task/generateInvoices&runInEmpty]]></url>
<url><![CDATA[/_dr/cron/fanout?queue=retryable-cron-tasks&endpoint=/_dr/task/generateInvoices?shouldPublish=true&runInEmpty]]></url>
<description>
Starts the beam/invoicing/InvoicingPipeline Dataflow template, which creates the overall invoice and
detail report CSVs for last month, storing them in gs://[PROJECT-ID]-billing/invoices/yyyy-MM.
@@ -120,7 +120,6 @@ class SyncRegistrarsSheet {
builder.put("registrarName", convert(registrar.getRegistrarName()));
builder.put("state", convert(registrar.getState()));
builder.put("ianaIdentifier", convert(registrar.getIanaIdentifier()));
builder.put("billingIdentifier", convert(registrar.getBillingIdentifier()));
builder.put("billingAccountMap", convert(registrar.getBillingAccountMap()));
builder.put("primaryContacts", convertContacts(contacts, byType(ADMIN)));
builder.put("techContacts", convertContacts(contacts, byType(TECH)));
@@ -33,7 +33,7 @@ import org.joda.time.DateTime;
*/
public class DomainPricingCustomLogic extends BaseFlowCustomLogic {
protected DomainPricingCustomLogic(
public DomainPricingCustomLogic(
EppInput eppInput, SessionMetadata sessionMetadata, FlowMetadata flowMetadata) {
super(eppInput, sessionMetadata, flowMetadata);
}
@@ -14,6 +14,7 @@
package google.registry.flows.domain;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.collect.ImmutableSet.toImmutableSet;
import static google.registry.flows.FlowUtils.persistEntityChanges;
import static google.registry.flows.FlowUtils.validateRegistrarIsLoggedIn;
@@ -54,6 +55,7 @@ import static google.registry.persistence.transaction.TransactionManagerFactory.
import static google.registry.util.DateTimeUtils.END_OF_TIME;
import static google.registry.util.DateTimeUtils.leapSafeAddYears;
import com.google.auto.value.AutoValue;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.common.net.InternetDomainName;
@@ -80,6 +82,7 @@ import google.registry.model.billing.BillingEvent;
import google.registry.model.billing.BillingEvent.Flag;
import google.registry.model.billing.BillingEvent.Reason;
import google.registry.model.billing.BillingEvent.Recurring;
import google.registry.model.billing.BillingEvent.RenewalPriceBehavior;
import google.registry.model.domain.DomainBase;
import google.registry.model.domain.DomainCommand;
import google.registry.model.domain.DomainCommand.Create;
@@ -115,7 +118,9 @@ import google.registry.model.tld.Registry.TldType;
import google.registry.model.tld.label.ReservationType;
import google.registry.tmch.LordnTaskUtils;
import java.util.Optional;
import javax.annotation.Nullable;
import javax.inject.Inject;
import org.joda.money.Money;
import org.joda.time.DateTime;
import org.joda.time.Duration;
@@ -327,7 +332,10 @@ public final class DomainCreateFlow implements TransactionalFlow {
now);
// Create a new autorenew billing event and poll message starting at the expiration time.
BillingEvent.Recurring autorenewBillingEvent =
createAutorenewBillingEvent(domainHistoryKey, registrationExpirationTime);
createAutorenewBillingEvent(
domainHistoryKey,
registrationExpirationTime,
getRenewalPriceInfo(isAnchorTenant, allocationToken, feesAndCredits));
PollMessage.Autorenew autorenewPollMessage =
createAutorenewPollMessage(domainHistoryKey, registrationExpirationTime);
ImmutableSet.Builder<ImmutableObject> entitiesToSave = new ImmutableSet.Builder<>();
@@ -546,7 +554,9 @@ public final class DomainCreateFlow implements TransactionalFlow {
}
private Recurring createAutorenewBillingEvent(
Key<DomainHistory> domainHistoryKey, DateTime registrationExpirationTime) {
Key<DomainHistory> domainHistoryKey,
DateTime registrationExpirationTime,
RenewalPriceInfo renewalpriceInfo) {
return new BillingEvent.Recurring.Builder()
.setReason(Reason.RENEW)
.setFlags(ImmutableSet.of(Flag.AUTO_RENEW))
@@ -555,6 +565,8 @@ public final class DomainCreateFlow implements TransactionalFlow {
.setEventTime(registrationExpirationTime)
.setRecurrenceEndTime(END_OF_TIME)
.setParent(domainHistoryKey)
.setRenewalPriceBehavior(renewalpriceInfo.renewalPriceBehavior())
.setRenewalPrice(renewalpriceInfo.renewalPrice())
.build();
}
@@ -611,6 +623,48 @@ public final class DomainCreateFlow implements TransactionalFlow {
}
}
/**
* Determines the {@link RenewalPriceBehavior} and the renewal price that needs be stored in the
* {@link Recurring} billing events.
*
* <p>By default, the renewal price is calculated during the process of renewal. Renewal price
* should be the createCost if and only if the renewal price behavior in the {@link
* AllocationToken} is 'SPECIFIED'.
*/
static RenewalPriceInfo getRenewalPriceInfo(
boolean isAnchorTenant,
Optional<AllocationToken> allocationToken,
FeesAndCredits feesAndCredits) {
if (isAnchorTenant) {
if (allocationToken.isPresent()) {
checkArgument(
allocationToken.get().getRenewalPriceBehavior() != RenewalPriceBehavior.SPECIFIED,
"Renewal price behavior cannot be SPECIFIED for anchor tenant");
}
return RenewalPriceInfo.create(RenewalPriceBehavior.NONPREMIUM, null);
} else if (allocationToken.isPresent()
&& allocationToken.get().getRenewalPriceBehavior() == RenewalPriceBehavior.SPECIFIED) {
return RenewalPriceInfo.create(
RenewalPriceBehavior.SPECIFIED, feesAndCredits.getCreateCost());
} else {
return RenewalPriceInfo.create(RenewalPriceBehavior.DEFAULT, null);
}
}
/** A class to store renewal info used in {@link Recurring} billing events. */
@AutoValue
public abstract static class RenewalPriceInfo {
static DomainCreateFlow.RenewalPriceInfo create(
RenewalPriceBehavior renewalPriceBehavior, @Nullable Money renewalPrice) {
return new AutoValue_DomainCreateFlow_RenewalPriceInfo(renewalPriceBehavior, renewalPrice);
}
public abstract RenewalPriceBehavior renewalPriceBehavior();
@Nullable
public abstract Money renewalPrice();
}
private static ImmutableList<FeeTransformResponseExtension> createResponseExtensions(
Optional<FeeCreateCommandExtension> feeCreate, FeesAndCredits feesAndCredits) {
return feeCreate.isPresent()
@@ -680,7 +680,7 @@ public class DomainFlowUtils {
break;
case RENEW:
builder.setAvailIfSupported(true);
fees = pricingLogic.getRenewPrice(registry, domainNameString, now, years).getFees();
fees = pricingLogic.getRenewPrice(registry, domainNameString, now, years, null).getFees();
break;
case RESTORE:
// The minimum allowable period per the EPP spec is 1, so, strangely, 1 year still has to be
@@ -14,8 +14,11 @@
package google.registry.flows.domain;
import static com.google.common.base.Preconditions.checkArgument;
import static google.registry.flows.domain.DomainFlowUtils.zeroInCurrency;
import static google.registry.pricing.PricingEngineProxy.getPricesForDomainName;
import static google.registry.util.DomainNameUtils.getTldFromDomainName;
import static google.registry.util.PreconditionsUtils.checkArgumentPresent;
import com.google.common.net.InternetDomainName;
import google.registry.flows.EppException;
@@ -27,15 +30,16 @@ import google.registry.flows.custom.DomainPricingCustomLogic.RenewPriceParameter
import google.registry.flows.custom.DomainPricingCustomLogic.RestorePriceParameters;
import google.registry.flows.custom.DomainPricingCustomLogic.TransferPriceParameters;
import google.registry.flows.custom.DomainPricingCustomLogic.UpdatePriceParameters;
import google.registry.model.billing.BillingEvent.Recurring;
import google.registry.model.domain.fee.BaseFee;
import google.registry.model.domain.fee.BaseFee.FeeType;
import google.registry.model.domain.fee.Fee;
import google.registry.model.domain.token.AllocationToken;
import google.registry.model.pricing.PremiumPricingEngine.DomainPrices;
import google.registry.model.tld.Registry;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.Optional;
import javax.annotation.Nullable;
import javax.inject.Inject;
import org.joda.money.CurrencyUnit;
import org.joda.money.Money;
@@ -102,18 +106,63 @@ public final class DomainPricingLogic {
.build());
}
/** Returns a new renew price for the pricer. */
@SuppressWarnings("unused")
FeesAndCredits getRenewPrice(Registry registry, String domainName, DateTime dateTime, int years)
/** Returns a new renewal cost for the pricer. */
FeesAndCredits getRenewPrice(
Registry registry,
String domainName,
DateTime dateTime,
int years,
@Nullable Recurring recurringBillingEvent)
throws EppException {
DomainPrices domainPrices = getPricesForDomainName(domainName, dateTime);
BigDecimal renewCost = domainPrices.getRenewCost().multipliedBy(years).getAmount();
checkArgument(years > 0, "Number of years must be positive");
Money renewCost;
boolean isRenewCostPremiumPrice;
// recurring billing event is null if the domain is still available. Billing events are created
// in the process of domain creation.
if (recurringBillingEvent == null) {
DomainPrices domainPrices = getPricesForDomainName(domainName, dateTime);
renewCost = domainPrices.getRenewCost().multipliedBy(years);
isRenewCostPremiumPrice = domainPrices.isPremium();
} else {
switch (recurringBillingEvent.getRenewalPriceBehavior()) {
case DEFAULT:
DomainPrices domainPrices = getPricesForDomainName(domainName, dateTime);
renewCost = domainPrices.getRenewCost().multipliedBy(years);
isRenewCostPremiumPrice = domainPrices.isPremium();
break;
// if the renewal price behavior is specified, then the renewal price should be the same
// as the creation price, which is stored in the billing event as the renewal price
case SPECIFIED:
checkArgumentPresent(
recurringBillingEvent.getRenewalPrice(),
"Unexpected behavior: renewal price cannot be null when renewal behavior is"
+ " SPECIFIED");
renewCost = recurringBillingEvent.getRenewalPrice().get().multipliedBy(years);
isRenewCostPremiumPrice = false;
break;
// if the renewal price behavior is nonpremium, it means that the domain should be renewed
// at standard price of domains at the time, even if the domain is premium
case NONPREMIUM:
renewCost =
Registry.get(getTldFromDomainName(domainName))
.getStandardRenewCost(dateTime)
.multipliedBy(years);
isRenewCostPremiumPrice = false;
break;
default:
throw new IllegalArgumentException(
String.format(
"Unknown RenewalPriceBehavior enum value: %s",
recurringBillingEvent.getRenewalPriceBehavior()));
}
}
return customLogic.customizeRenewPrice(
RenewPriceParameters.newBuilder()
.setFeesAndCredits(
new FeesAndCredits.Builder()
.setCurrency(registry.getCurrency())
.addFeeOrCredit(Fee.create(renewCost, FeeType.RENEW, domainPrices.isPremium()))
.setCurrency(renewCost.getCurrencyUnit())
.addFeeOrCredit(
Fee.create(renewCost.getAmount(), FeeType.RENEW, isRenewCostPremiumPrice))
.build())
.setRegistry(registry)
.setDomainName(InternetDomainName.from(domainName))
@@ -155,7 +155,8 @@ public final class DomainRenewFlow implements TransactionalFlow {
Optional<FeeRenewCommandExtension> feeRenew =
eppInput.getSingleExtension(FeeRenewCommandExtension.class);
FeesAndCredits feesAndCredits =
pricingLogic.getRenewPrice(Registry.get(existingDomain.getTld()), targetId, now, years);
pricingLogic.getRenewPrice(
Registry.get(existingDomain.getTld()), targetId, now, years, null);
validateFeeChallenge(targetId, now, feeRenew, feesAndCredits);
flowCustomLogic.afterValidation(
AfterValidationParameters.newBuilder()
@@ -116,4 +116,9 @@ public class AppEngineEnvironment {
}
});
}
/** Returns true if the current thread is in an App Engine Environment. */
public static boolean isInAppEngineEnvironment() {
return ApiProxy.getCurrentEnvironment() != null;
}
}
@@ -18,9 +18,13 @@ import static com.google.common.base.Suppliers.memoizeWithExpiration;
import static google.registry.config.RegistryConfig.getSingletonCacheRefreshDuration;
import static java.util.concurrent.TimeUnit.MILLISECONDS;
import com.github.benmanes.caffeine.cache.CacheLoader;
import com.github.benmanes.caffeine.cache.Caffeine;
import com.google.common.base.Supplier;
import google.registry.model.annotations.DeleteAfterMigration;
import java.time.Duration;
import org.checkerframework.checker.nullness.qual.NonNull;
import org.checkerframework.checker.nullness.qual.Nullable;
/** Utility methods related to caching Datastore entities. */
public class CacheUtils {
@@ -28,8 +32,8 @@ public class CacheUtils {
/**
* Memoize a supplier, with a short expiration specified in the environment config.
*
* <p>Use this for things that might change while code is running. (For example, the various
* lists downloaded from the TMCH get updated in Datastore and the caches need to be refreshed.)
* <p>Use this for things that might change while code is running. (For example, the various lists
* downloaded from the TMCH get updated in Datastore and the caches need to be refreshed.)
*/
public static <T> Supplier<T> memoizeWithShortExpiration(Supplier<T> original) {
return tryMemoizeWithExpiration(getSingletonCacheRefreshDuration(), original);
@@ -73,4 +77,29 @@ public class CacheUtils {
}
return caffeine;
}
/**
* A {@link CacheLoader} that automatically masquerade the background thread where the refresh
* action runs in to be an GAE thread.
*/
@DeleteAfterMigration
public abstract static class AppEngineEnvironmentCacheLoader<K, V> implements CacheLoader<K, V> {
private static final AppEngineEnvironment environment = new AppEngineEnvironment();
@Override
public @Nullable V reload(@NonNull K key, @NonNull V oldValue) throws Exception {
V value;
boolean isMasqueraded = false;
if (!AppEngineEnvironment.isInAppEngineEnvironment()) {
environment.setEnvironmentForCurrentThread();
isMasqueraded = true;
}
value = load(key);
if (isMasqueraded) {
environment.unsetEnvironmentForCurrentThread();
}
return value;
}
}
}
@@ -37,6 +37,7 @@ import com.googlecode.objectify.Key;
import com.googlecode.objectify.annotation.Id;
import com.googlecode.objectify.annotation.Index;
import google.registry.config.RegistryConfig;
import google.registry.model.CacheUtils.AppEngineEnvironmentCacheLoader;
import google.registry.model.eppcommon.StatusValue;
import google.registry.model.index.EppResourceIndex;
import google.registry.model.index.ForeignKeyIndex;
@@ -385,7 +386,7 @@ public abstract class EppResource extends BackupGroupRoot implements Buildable {
}
static final CacheLoader<VKey<? extends EppResource>, EppResource> CACHE_LOADER =
new CacheLoader<VKey<? extends EppResource>, EppResource>() {
new AppEngineEnvironmentCacheLoader<VKey<? extends EppResource>, EppResource>() {
@Override
public EppResource load(VKey<? extends EppResource> key) {
@@ -43,6 +43,7 @@ import com.googlecode.objectify.annotation.Index;
import google.registry.config.RegistryConfig;
import google.registry.model.BackupGroupRoot;
import google.registry.model.CacheUtils;
import google.registry.model.CacheUtils.AppEngineEnvironmentCacheLoader;
import google.registry.model.EppResource;
import google.registry.model.annotations.DeleteAfterMigration;
import google.registry.model.annotations.ReportedOn;
@@ -256,7 +257,8 @@ public abstract class ForeignKeyIndex<E extends EppResource> extends BackupGroup
}
static final CacheLoader<VKey<ForeignKeyIndex<?>>, Optional<ForeignKeyIndex<?>>> CACHE_LOADER =
new CacheLoader<VKey<ForeignKeyIndex<?>>, Optional<ForeignKeyIndex<?>>>() {
new AppEngineEnvironmentCacheLoader<
VKey<ForeignKeyIndex<?>>, Optional<ForeignKeyIndex<?>>>() {
@Override
public Optional<ForeignKeyIndex<?>> load(VKey<ForeignKeyIndex<?>> key) {
@@ -123,24 +123,24 @@ public class Registrar extends ImmutableObject
/** Represents the type of a registrar entity. */
public enum Type {
/** A real-world, third-party registrar. Should have non-null IANA and billing IDs. */
/** A real-world, third-party registrar. Should have non-null IANA and billing account IDs. */
REAL(Objects::nonNull),
/**
* A registrar account used by a real third-party registrar undergoing operational testing and
* evaluation. Should only be created in sandbox, and should have null IANA/billing IDs.
* evaluation. Should only be created in sandbox, and should have null IANA/billing account IDs.
*/
OTE(Objects::isNull),
/**
* A registrar used for predelegation testing. Should have a null billing ID. The IANA ID should
* be either 9995 or 9996, which are reserved for predelegation testing.
* A registrar used for predelegation testing. Should have a null billing account ID. The IANA
* ID should be either 9995 or 9996, which are reserved for predelegation testing.
*/
PDT(n -> ImmutableSet.of(9995L, 9996L).contains(n)),
/**
* A registrar used for external monitoring by ICANN. Should have IANA ID 9997 and a null
* billing ID.
* billing account ID.
*/
EXTERNAL_MONITORING(isEqual(9997L)),
@@ -148,13 +148,13 @@ public class Registrar extends ImmutableObject
* A registrar used for when the registry acts as a registrar. Must have either IANA ID 9998
* (for billable transactions) or 9999 (for non-billable transactions).
*/
// TODO(b/13786188): determine what billing ID for this should be, if any.
// TODO(b/13786188): determine what billing account ID for this should be, if any.
INTERNAL(n -> ImmutableSet.of(9998L, 9999L).contains(n)),
/** A registrar used for internal monitoring. Should have null IANA/billing IDs. */
/** A registrar used for internal monitoring. Should have null IANA/billing account IDs. */
MONITORING(Objects::isNull),
/** A registrar used for internal testing. Should have null IANA/billing IDs. */
/** A registrar used for internal testing. Should have null IANA/billing account IDs. */
TEST(Objects::isNull);
/**
@@ -387,9 +387,6 @@ public class Registrar extends ImmutableObject
*/
@Index @Nullable Long ianaIdentifier;
/** Identifier of registrar used in external billing system (e.g. Oracle). */
@Nullable Long billingIdentifier;
/** Purchase Order number used for invoices in external billing system, if applicable. */
@Nullable String poNumber;
@@ -496,11 +493,6 @@ public class Registrar extends ImmutableObject
return ianaIdentifier;
}
@Nullable
public Long getBillingIdentifier() {
return billingIdentifier;
}
public Optional<String> getPoNumber() {
return Optional.ofNullable(poNumber);
}
@@ -688,7 +680,6 @@ public class Registrar extends ImmutableObject
return new JsonMapBuilder()
.put("clientIdentifier", clientIdentifier)
.put("ianaIdentifier", ianaIdentifier)
.put("billingIdentifier", billingIdentifier)
.putString("creationTime", creationTime.getTimestamp())
.putString("lastUpdateTime", lastUpdateTime.getTimestamp())
.putString("lastCertificateUpdateTime", lastCertificateUpdateTime)
@@ -785,14 +776,6 @@ public class Registrar extends ImmutableObject
return this;
}
public Builder setBillingIdentifier(@Nullable Long billingIdentifier) {
checkArgument(
billingIdentifier == null || billingIdentifier > 0,
"Billing ID must be a positive number");
getInstance().billingIdentifier = billingIdentifier;
return this;
}
public Builder setPoNumber(Optional<String> poNumber) {
getInstance().poNumber = poNumber.orElse(null);
return this;
@@ -211,7 +211,7 @@ public class HistoryEntryDao {
jpaTm().criteriaQuery(criteriaQuery).getResultList());
}
private static Class<? extends HistoryEntry> getHistoryClassFromParent(
public static Class<? extends HistoryEntry> getHistoryClassFromParent(
Class<? extends EppResource> parent) {
if (!RESOURCE_TYPES_TO_HISTORY_TYPES.containsKey(parent)) {
throw new IllegalArgumentException(
@@ -220,7 +220,7 @@ public class HistoryEntryDao {
return RESOURCE_TYPES_TO_HISTORY_TYPES.get(parent);
}
private static String getRepoIdFieldNameFromHistoryClass(
public static String getRepoIdFieldNameFromHistoryClass(
Class<? extends HistoryEntry> historyClass) {
if (!REPO_ID_FIELD_NAMES.containsKey(historyClass)) {
throw new IllegalArgumentException(
@@ -29,7 +29,6 @@ import static google.registry.util.DateTimeUtils.START_OF_TIME;
import static google.registry.util.PreconditionsUtils.checkArgumentNotNull;
import static org.joda.money.CurrencyUnit.USD;
import com.github.benmanes.caffeine.cache.CacheLoader;
import com.github.benmanes.caffeine.cache.LoadingCache;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Joiner;
@@ -50,6 +49,7 @@ import com.googlecode.objectify.annotation.OnSave;
import com.googlecode.objectify.annotation.Parent;
import google.registry.model.Buildable;
import google.registry.model.CacheUtils;
import google.registry.model.CacheUtils.AppEngineEnvironmentCacheLoader;
import google.registry.model.CreateAutoTimestamp;
import google.registry.model.ImmutableObject;
import google.registry.model.UnsafeSerializable;
@@ -268,7 +268,7 @@ public class Registry extends ImmutableObject
private static final LoadingCache<String, Optional<Registry>> CACHE =
CacheUtils.newCacheBuilder(getSingletonCacheRefreshDuration())
.build(
new CacheLoader<String, Optional<Registry>>() {
new AppEngineEnvironmentCacheLoader<String, Optional<Registry>>() {
@Override
public Optional<Registry> load(final String tld) {
// Enter a transaction-less context briefly; we don't want to enroll every TLD in
@@ -107,7 +107,7 @@ public abstract class PersistenceModule {
properties.put(HIKARI_MAXIMUM_POOL_SIZE, getHibernateHikariMaximumPoolSize());
properties.put(HIKARI_IDLE_TIMEOUT, getHibernateHikariIdleTimeout());
properties.put(Environment.DIALECT, NomulusPostgreSQLDialect.class.getName());
properties.put(JDBC_BATCH_SIZE, getHibernateJdbcBatchSize());
properties.put(JDBC_BATCH_SIZE, Integer.toString(getHibernateJdbcBatchSize()));
properties.put(JDBC_FETCH_SIZE, getHibernateJdbcFetchSize());
return properties.build();
}
@@ -20,11 +20,14 @@ import static com.google.common.collect.ImmutableList.toImmutableList;
import static com.google.common.collect.ImmutableSet.toImmutableSet;
import static com.google.common.collect.ImmutableSetMultimap.toImmutableSetMultimap;
import static google.registry.model.EppResourceUtils.isLinked;
import static google.registry.persistence.transaction.TransactionManagerFactory.jpaTm;
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
import static google.registry.persistence.transaction.TransactionManagerUtil.transactIfJpaTm;
import static google.registry.rdap.RdapIcannStandardInformation.CONTACT_REDACTED_VALUE;
import static google.registry.util.CollectionUtils.union;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Strings;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
@@ -81,6 +84,7 @@ import java.util.Set;
import java.util.stream.Stream;
import javax.annotation.Nullable;
import javax.inject.Inject;
import javax.persistence.Entity;
import org.joda.time.DateTime;
/**
@@ -150,6 +154,19 @@ public class RdapJsonFormatter {
INTERNAL
}
/**
* JPQL query template for finding the latest history entry per event type for an EPP entity.
*
* <p>User should replace '%entityName%', '%repoIdField%', and '%repoIdValue%' with valid values.
* A DomainHistory query may look like below: {@code select e from DomainHistory e where
* domainRepoId = '17-Q9JYB4C' and modificationTime in (select max(modificationTime) from
* DomainHistory where domainRepoId = '17-Q9JYB4C' and type is not null group by type)}
*/
private static final String GET_LAST_HISTORY_BY_TYPE_JPQL_TEMPLATE =
"select e from %entityName% e where %repoIdField% = '%repoIdValue%' and modificationTime in "
+ " (select max(modificationTime) from %entityName% where "
+ " %repoIdField% = '%repoIdValue%' and type is not null group by type)";
/** Map of EPP status values to the RDAP equivalents. */
private static final ImmutableMap<StatusValue, RdapStatus> STATUS_TO_RDAP_STATUS_MAP =
new ImmutableMap.Builder<StatusValue, RdapStatus>()
@@ -855,17 +872,8 @@ public class RdapJsonFormatter {
return rolesBuilder.build();
}
/**
* Creates the list of optional events to list in domain, nameserver, or contact replies.
*
* <p>Only has entries for optional events that won't be shown in "SUMMARY" versions of these
* objects. These are either stated as optional in the RDAP Response Profile 15feb19, or not
* mentioned at all but thought to be useful anyway.
*
* <p>Any required event should be added elsewhere, preferably without using HistoryEntries (so
* that we don't need to load HistoryEntries for "summary" responses).
*/
private ImmutableList<Event> makeOptionalEvents(EppResource resource) {
@VisibleForTesting
ImmutableMap<EventAction, HistoryEntry> getLastHistoryEntryByType(EppResource resource) {
HashMap<EventAction, HistoryEntry> lastEntryOfType = Maps.newHashMap();
// Events (such as transfer, but also create) can appear multiple times. We only want the last
// time they appeared.
@@ -878,8 +886,26 @@ public class RdapJsonFormatter {
// 2.3.2.3 An event of *eventAction* type *transfer*, with the last date and time that the
// domain was transferred. The event of *eventAction* type *transfer* MUST be omitted if the
// domain name has not been transferred since it was created.
Iterable<? extends HistoryEntry> historyEntries =
HistoryEntryDao.loadHistoryObjectsForResource(resource.createVKey());
Iterable<? extends HistoryEntry> historyEntries;
if (tm().isOfy()) {
historyEntries = HistoryEntryDao.loadHistoryObjectsForResource(resource.createVKey());
} else {
VKey<? extends EppResource> resourceVkey = resource.createVKey();
Class<? extends HistoryEntry> historyClass =
HistoryEntryDao.getHistoryClassFromParent(resourceVkey.getKind());
String entityName = historyClass.getAnnotation(Entity.class).name();
if (Strings.isNullOrEmpty(entityName)) {
entityName = historyClass.getSimpleName();
}
String repoIdFieldName = HistoryEntryDao.getRepoIdFieldNameFromHistoryClass(historyClass);
String jpql =
GET_LAST_HISTORY_BY_TYPE_JPQL_TEMPLATE
.replace("%entityName%", entityName)
.replace("%repoIdField%", repoIdFieldName)
.replace("%repoIdValue%", resourceVkey.getSqlKey().toString());
historyEntries =
jpaTm().transact(() -> jpaTm().getEntityManager().createQuery(jpql).getResultList());
}
for (HistoryEntry historyEntry : historyEntries) {
EventAction rdapEventAction =
HISTORY_ENTRY_TYPE_TO_RDAP_EVENT_ACTION_MAP.get(historyEntry.getType());
@@ -889,6 +915,21 @@ public class RdapJsonFormatter {
}
lastEntryOfType.put(rdapEventAction, historyEntry);
}
return ImmutableMap.copyOf(lastEntryOfType);
}
/**
* Creates the list of optional events to list in domain, nameserver, or contact replies.
*
* <p>Only has entries for optional events that won't be shown in "SUMMARY" versions of these
* objects. These are either stated as optional in the RDAP Response Profile 15feb19, or not
* mentioned at all but thought to be useful anyway.
*
* <p>Any required event should be added elsewhere, preferably without using HistoryEntries (so
* that we don't need to load HistoryEntries for "summary" responses).
*/
private ImmutableList<Event> makeOptionalEvents(EppResource resource) {
ImmutableMap<EventAction, HistoryEntry> lastEntryOfType = getLastHistoryEntryByType(resource);
ImmutableList.Builder<Event> eventsBuilder = new ImmutableList.Builder<>();
DateTime creationTime = resource.getCreationTime();
DateTime lastChangeTime =
@@ -19,12 +19,12 @@ import static google.registry.config.RegistryConfig.ConfigModule.TmchCaMode.PROD
import static google.registry.config.RegistryConfig.getSingletonCacheRefreshDuration;
import static google.registry.util.ResourceUtils.readResourceUtf8;
import com.github.benmanes.caffeine.cache.CacheLoader;
import com.github.benmanes.caffeine.cache.LoadingCache;
import com.google.common.collect.ImmutableMap;
import google.registry.config.RegistryConfig.Config;
import google.registry.config.RegistryConfig.ConfigModule.TmchCaMode;
import google.registry.model.CacheUtils;
import google.registry.model.CacheUtils.AppEngineEnvironmentCacheLoader;
import google.registry.model.tmch.TmchCrl;
import google.registry.util.Clock;
import google.registry.util.X509Utils;
@@ -78,7 +78,7 @@ public final class TmchCertificateAuthority {
private static final LoadingCache<TmchCaMode, X509CRL> CRL_CACHE =
CacheUtils.newCacheBuilder(getSingletonCacheRefreshDuration())
.build(
new CacheLoader<TmchCaMode, X509CRL>() {
new AppEngineEnvironmentCacheLoader<TmchCaMode, X509CRL>() {
@Override
public X509CRL load(final TmchCaMode tmchCaMode) throws GeneralSecurityException {
Optional<TmchCrl> storedCrl = TmchCrl.get();
@@ -150,14 +150,6 @@ abstract class CreateOrUpdateRegistrarCommand extends MutatingCommand {
validateWith = OptionalLongParameter.class)
Optional<Long> ianaId;
@Nullable
@Parameter(
names = "--billing_id",
description = "Registrar Billing ID (i.e. Oracle #)",
converter = OptionalLongParameter.class,
validateWith = OptionalLongParameter.class)
private Optional<Long> billingId;
@Nullable
@Parameter(
names = "--po_number",
@@ -173,7 +165,8 @@ abstract class CreateOrUpdateRegistrarCommand extends MutatingCommand {
"Registrar Billing Account key-value pairs (formatted as key=value[,key=value...]), "
+ "where key is a currency unit (USD, JPY, etc) and value is the registrar's billing "
+ "account id for that currency. During update, only the pairs that need updating "
+ "need to be provided.",
+ "need to be provided, except when an empty string is provided, in which case the"
+ "entire map is nullified.",
converter = CurrencyUnitToStringMap.class,
validateWith = CurrencyUnitToStringMap.class)
private Map<CurrencyUnit, String> billingAccountMap;
@@ -362,13 +355,12 @@ abstract class CreateOrUpdateRegistrarCommand extends MutatingCommand {
if (ianaId != null) {
builder.setIanaIdentifier(ianaId.orElse(null));
}
if (billingId != null) {
builder.setBillingIdentifier(billingId.orElse(null));
}
Optional.ofNullable(poNumber).ifPresent(builder::setPoNumber);
if (billingAccountMap != null) {
LinkedHashMap<CurrencyUnit, String> newBillingAccountMap = new LinkedHashMap<>();
if (oldRegistrar != null && oldRegistrar.getBillingAccountMap() != null) {
if (oldRegistrar != null
&& oldRegistrar.getBillingAccountMap() != null
&& !billingAccountMap.isEmpty()) {
newBillingAccountMap.putAll(oldRegistrar.getBillingAccountMap());
}
newBillingAccountMap.putAll(billingAccountMap);
@@ -90,6 +90,7 @@ public class ExpandRecurringBillingEventsActionTest
action.mrRunner = makeDefaultRunner();
action.clock = clock;
action.cursorTimeParam = Optional.empty();
action.batchSize = 2;
createTld("tld");
domain =
persistResource(
@@ -279,11 +280,12 @@ public class ExpandRecurringBillingEventsActionTest
assertHistoryEntryMatches(
domain, persistedEntry, "TheRegistrar", DateTime.parse("2000-02-19T00:00:00Z"), true);
BillingEvent.OneTime expected = defaultOneTimeBuilder().setParent(persistedEntry).build();
// Persist an otherwise identical billing event that differs only in billing time.
// Persist an otherwise identical billing event that differs only in billing time (and ID).
BillingEvent.OneTime persisted =
persistResource(
expected
.asBuilder()
.setId(15891L)
.setBillingTime(DateTime.parse("1999-02-19T00:00:00Z"))
.setEventTime(DateTime.parse("1999-01-05T00:00:00Z"))
.build());
@@ -639,43 +641,95 @@ public class ExpandRecurringBillingEventsActionTest
@TestOfyAndSql
void testSuccess_expandMultipleEvents() throws Exception {
persistResource(recurring);
DomainBase domain2 =
persistResource(
newDomainBase("example2.tld")
.asBuilder()
.setCreationTimeForTest(DateTime.parse("1999-04-05T00:00:00Z"))
.build());
DomainHistory historyEntry2 =
persistResource(
new DomainHistory.Builder()
.setRegistrarId(domain2.getCreationRegistrarId())
.setType(HistoryEntry.Type.DOMAIN_CREATE)
.setModificationTime(DateTime.parse("1999-04-05T00:00:00Z"))
.setDomain(domain2)
.build());
BillingEvent.Recurring recurring2 =
persistResource(
recurring
new BillingEvent.Recurring.Builder()
.setParent(historyEntry2)
.setRegistrarId(domain2.getCreationRegistrarId())
.setEventTime(DateTime.parse("2000-04-05T00:00:00Z"))
.setFlags(ImmutableSet.of(Flag.AUTO_RENEW))
.setReason(Reason.RENEW)
.setRecurrenceEndTime(END_OF_TIME)
.setTargetId(domain2.getDomainName())
.build());
DomainBase domain3 =
persistResource(
newDomainBase("example3.tld")
.asBuilder()
.setEventTime(recurring.getEventTime().plusMonths(3))
.setId(3L)
.setCreationTimeForTest(DateTime.parse("1999-06-05T00:00:00Z"))
.build());
DomainHistory historyEntry3 =
persistResource(
new DomainHistory.Builder()
.setRegistrarId(domain3.getCreationRegistrarId())
.setType(HistoryEntry.Type.DOMAIN_CREATE)
.setModificationTime(DateTime.parse("1999-06-05T00:00:00Z"))
.setDomain(domain3)
.build());
BillingEvent.Recurring recurring3 =
persistResource(
new BillingEvent.Recurring.Builder()
.setParent(historyEntry3)
.setRegistrarId(domain3.getCreationRegistrarId())
.setEventTime(DateTime.parse("2000-06-05T00:00:00Z"))
.setFlags(ImmutableSet.of(Flag.AUTO_RENEW))
.setReason(Reason.RENEW)
.setRecurrenceEndTime(END_OF_TIME)
.setTargetId(domain3.getDomainName())
.build());
action.cursorTimeParam = Optional.of(START_OF_TIME);
runAction();
List<DomainHistory> persistedEntries =
getHistoryEntriesOfType(domain, DOMAIN_AUTORENEW, DomainHistory.class);
assertThat(persistedEntries).hasSize(2);
DomainHistory persistedHistory1 =
getOnlyHistoryEntryOfType(domain, DOMAIN_AUTORENEW, DomainHistory.class);
assertHistoryEntryMatches(
domain,
persistedEntries.get(0),
"TheRegistrar",
DateTime.parse("2000-02-19T00:00:00Z"),
true);
domain, persistedHistory1, "TheRegistrar", DateTime.parse("2000-02-19T00:00:00Z"), true);
BillingEvent.OneTime expected =
defaultOneTimeBuilder()
.setParent(persistedEntries.get(0))
.setParent(persistedHistory1)
.setCancellationMatchingBillingEvent(recurring.createVKey())
.build();
DomainHistory persistedHistory2 =
getOnlyHistoryEntryOfType(domain2, DOMAIN_AUTORENEW, DomainHistory.class);
assertHistoryEntryMatches(
domain,
persistedEntries.get(1),
"TheRegistrar",
DateTime.parse("2000-05-20T00:00:00Z"),
true);
domain2, persistedHistory2, "TheRegistrar", DateTime.parse("2000-05-20T00:00:00Z"), true);
BillingEvent.OneTime expected2 =
defaultOneTimeBuilder()
.setBillingTime(DateTime.parse("2000-05-20T00:00:00Z"))
.setEventTime(DateTime.parse("2000-04-05T00:00:00Z"))
.setParent(persistedEntries.get(1))
.setParent(persistedHistory2)
.setTargetId(domain2.getDomainName())
.setCancellationMatchingBillingEvent(recurring2.createVKey())
.build();
assertBillingEventsForResource(domain, expected, expected2, recurring, recurring2);
DomainHistory persistedHistory3 =
getOnlyHistoryEntryOfType(domain3, DOMAIN_AUTORENEW, DomainHistory.class);
assertHistoryEntryMatches(
domain3, persistedHistory3, "TheRegistrar", DateTime.parse("2000-07-20T00:00:00Z"), true);
BillingEvent.OneTime expected3 =
defaultOneTimeBuilder()
.setBillingTime(DateTime.parse("2000-07-20T00:00:00Z"))
.setEventTime(DateTime.parse("2000-06-05T00:00:00Z"))
.setTargetId(domain3.getDomainName())
.setParent(persistedHistory3)
.setCancellationMatchingBillingEvent(recurring3.createVKey())
.build();
assertBillingEventsForResource(domain, expected, recurring);
assertBillingEventsForResource(domain2, expected2, recurring2);
assertBillingEventsForResource(domain3, expected3, recurring3);
assertCursorAt(currentTestTime);
}
@@ -204,7 +204,6 @@ public class SyncRegistrarsSheetTest {
assertThat(row).containsEntry("registrarName", "AAA Registrar Inc.");
assertThat(row).containsEntry("state", "SUSPENDED");
assertThat(row).containsEntry("ianaIdentifier", "8");
assertThat(row).containsEntry("billingIdentifier", "");
assertThat(row)
.containsEntry(
"primaryContacts",
@@ -300,7 +299,6 @@ public class SyncRegistrarsSheetTest {
assertThat(row).containsEntry("registrarName", "Another Registrar LLC");
assertThat(row).containsEntry("state", "ACTIVE");
assertThat(row).containsEntry("ianaIdentifier", "1");
assertThat(row).containsEntry("billingIdentifier", "");
assertThat(row).containsEntry("primaryContacts", "");
assertThat(row).containsEntry("techContacts", "");
assertThat(row).containsEntry("marketingContacts", "");
@@ -350,7 +348,6 @@ public class SyncRegistrarsSheetTest {
assertThat(row).containsEntry("registrarName", "Some Registrar");
assertThat(row).containsEntry("state", "ACTIVE");
assertThat(row).containsEntry("ianaIdentifier", "8");
assertThat(row).containsEntry("billingIdentifier", "");
assertThat(row).containsEntry("primaryContacts", "");
assertThat(row).containsEntry("techContacts", "");
assertThat(row).containsEntry("marketingContacts", "");
@@ -21,6 +21,9 @@ import static google.registry.flows.FlowTestCase.UserPrivileges.SUPERUSER;
import static google.registry.model.billing.BillingEvent.Flag.ANCHOR_TENANT;
import static google.registry.model.billing.BillingEvent.Flag.RESERVED;
import static google.registry.model.billing.BillingEvent.Flag.SUNRISE;
import static google.registry.model.billing.BillingEvent.RenewalPriceBehavior.DEFAULT;
import static google.registry.model.billing.BillingEvent.RenewalPriceBehavior.NONPREMIUM;
import static google.registry.model.billing.BillingEvent.RenewalPriceBehavior.SPECIFIED;
import static google.registry.model.domain.fee.Fee.FEE_EXTENSION_URIS;
import static google.registry.model.domain.token.AllocationToken.TokenType.SINGLE_USE;
import static google.registry.model.domain.token.AllocationToken.TokenType.UNLIMITED_USE;
@@ -83,6 +86,7 @@ import google.registry.flows.ResourceFlowTestCase;
import google.registry.flows.domain.DomainCreateFlow.AnchorTenantCreatePeriodException;
import google.registry.flows.domain.DomainCreateFlow.MustHaveSignedMarksInCurrentPhaseException;
import google.registry.flows.domain.DomainCreateFlow.NoGeneralRegistrationsInCurrentPhaseException;
import google.registry.flows.domain.DomainCreateFlow.RenewalPriceInfo;
import google.registry.flows.domain.DomainCreateFlow.SignedMarksOnlyDuringSunriseException;
import google.registry.flows.domain.DomainFlowTmchUtils.FoundMarkExpiredException;
import google.registry.flows.domain.DomainFlowTmchUtils.FoundMarkNotYetValidException;
@@ -149,9 +153,12 @@ import google.registry.flows.exceptions.ResourceCreateContentionException;
import google.registry.model.billing.BillingEvent;
import google.registry.model.billing.BillingEvent.Flag;
import google.registry.model.billing.BillingEvent.Reason;
import google.registry.model.billing.BillingEvent.RenewalPriceBehavior;
import google.registry.model.domain.DomainBase;
import google.registry.model.domain.DomainHistory;
import google.registry.model.domain.GracePeriod;
import google.registry.model.domain.fee.BaseFee.FeeType;
import google.registry.model.domain.fee.Fee;
import google.registry.model.domain.launch.LaunchNotice;
import google.registry.model.domain.rgp.GracePeriodStatus;
import google.registry.model.domain.secdns.DelegationSignerData;
@@ -177,6 +184,7 @@ import google.registry.testing.TestOfyAndSql;
import google.registry.testing.TestOfyOnly;
import java.math.BigDecimal;
import java.util.Map;
import java.util.Optional;
import javax.annotation.Nullable;
import org.joda.money.Money;
import org.joda.time.DateTime;
@@ -258,13 +266,20 @@ class DomainCreateFlowTest extends ResourceFlowTestCase<DomainCreateFlow, Domain
DomainBase domain = reloadResourceByForeignKey();
boolean isAnchorTenant = expectedBillingFlags.contains(ANCHOR_TENANT);
// Calculate the total creation cost.
Money creationCost =
isAnchorTenant
? Money.of(USD, 0)
: isDomainPremium(getUniqueIdFromCommand(), clock.nowUtc())
? Money.of(USD, 200)
: Money.of(USD, 26);
// Set up the creation cost.
FeesAndCredits feesAndCredits =
new FeesAndCredits.Builder()
.setCurrency(USD)
.addFeeOrCredit(
Fee.create(
isAnchorTenant
? BigDecimal.valueOf(0)
: isDomainPremium(getUniqueIdFromCommand(), clock.nowUtc())
? BigDecimal.valueOf(200)
: BigDecimal.valueOf(26),
FeeType.CREATE,
isDomainPremium(getUniqueIdFromCommand(), clock.nowUtc())))
.build();
Money eapFee =
Money.of(
Registry.get(domainTld).getCurrency(),
@@ -284,13 +299,16 @@ class DomainCreateFlowTest extends ResourceFlowTestCase<DomainCreateFlow, Domain
.hasType(HistoryEntry.Type.DOMAIN_CREATE)
.and()
.hasPeriodYears(2);
RenewalPriceInfo renewalPriceInfo =
DomainCreateFlow.getRenewalPriceInfo(
isAnchorTenant, Optional.ofNullable(allocationToken), feesAndCredits);
// There should be one bill for the create and one for the recurring autorenew event.
BillingEvent.OneTime createBillingEvent =
new BillingEvent.OneTime.Builder()
.setReason(Reason.CREATE)
.setTargetId(getUniqueIdFromCommand())
.setRegistrarId("TheRegistrar")
.setCost(creationCost)
.setCost(feesAndCredits.getCreateCost())
.setPeriodYears(2)
.setEventTime(clock.nowUtc())
.setBillingTime(billingTime)
@@ -308,6 +326,8 @@ class DomainCreateFlowTest extends ResourceFlowTestCase<DomainCreateFlow, Domain
.setEventTime(domain.getRegistrationExpirationTime())
.setRecurrenceEndTime(END_OF_TIME)
.setParent(historyEntry)
.setRenewalPriceBehavior(renewalPriceInfo.renewalPriceBehavior())
.setRenewalPrice(renewalPriceInfo.renewalPrice())
.build();
ImmutableSet.Builder<BillingEvent> expectedBillingEvents =
@@ -1159,6 +1179,28 @@ class DomainCreateFlowTest extends ResourceFlowTestCase<DomainCreateFlow, Domain
assertAllocationTokenWasRedeemed("abcDEF23456");
}
@TestOfyAndSql
void testSuccess_internalRegistrationWithSpecifiedRenewalPrice() throws Exception {
allocationToken =
persistResource(
new AllocationToken.Builder()
.setToken("abc123")
.setTokenType(SINGLE_USE)
.setDomainName("resdom.tld")
.setRenewalPriceBehavior(SPECIFIED)
.build());
// Despite the domain being FULLY_BLOCKED, the non-superuser create succeeds the domain is also
// RESERVED_FOR_SPECIFIC_USE and the correct allocation token is passed.
setEppInput(
"domain_create_allocationtoken.xml", ImmutableMap.of("DOMAIN", "resdom.tld", "YEARS", "2"));
persistContactsAndHosts();
runFlowAssertResponse(
loadFile("domain_create_response.xml", ImmutableMap.of("DOMAIN", "resdom.tld")));
assertSuccessfulCreate("tld", ImmutableSet.of(RESERVED), allocationToken);
assertNoLordn();
assertAllocationTokenWasRedeemed("abc123");
}
@TestOfyAndSql
void testFailure_anchorTenant_notTwoYearPeriod() {
setEppInput("domain_create_anchor_tenant_invalid_years.xml");
@@ -2594,4 +2636,127 @@ class DomainCreateFlowTest extends ResourceFlowTestCase<DomainCreateFlow, Domain
assertAboutEppExceptions().that(thrown).marshalsToXml();
DatabaseHelper.removeDatabaseMigrationSchedule();
}
@TestOfyAndSql
void testGetRenewalPriceInfo_isAnchorTenantWithoutToken_returnsNonPremiumAndNullPrice() {
assertThat(
DomainCreateFlow.getRenewalPriceInfo(
true,
Optional.empty(),
new FeesAndCredits.Builder()
.setCurrency(USD)
.addFeeOrCredit(Fee.create(BigDecimal.valueOf(0), FeeType.CREATE, false))
.build()))
.isEqualTo(RenewalPriceInfo.create(NONPREMIUM, null));
}
@TestOfyAndSql
void testGetRenewalPriceInfo_isAnchorTenantWithDefaultToken_returnsNonPremiumAndNullPrice() {
assertThat(
DomainCreateFlow.getRenewalPriceInfo(
true,
Optional.of(allocationToken),
new FeesAndCredits.Builder()
.setCurrency(USD)
.addFeeOrCredit(Fee.create(BigDecimal.valueOf(0), FeeType.CREATE, false))
.build()))
.isEqualTo(RenewalPriceInfo.create(NONPREMIUM, null));
}
@TestOfyAndSql
void testGetRenewalPriceInfo_isNotAnchorTenantWithDefaultToken_returnsDefaultAndNullPrice() {
assertThat(
DomainCreateFlow.getRenewalPriceInfo(
false,
Optional.of(allocationToken),
new FeesAndCredits.Builder()
.setCurrency(USD)
.addFeeOrCredit(Fee.create(BigDecimal.valueOf(100), FeeType.CREATE, false))
.build()))
.isEqualTo(RenewalPriceInfo.create(DEFAULT, null));
}
@TestOfyAndSql
void testGetRenewalPriceInfo_isNotAnchorTenantWithoutToken_returnsDefaultAndNullPrice() {
assertThat(
DomainCreateFlow.getRenewalPriceInfo(
false,
Optional.empty(),
new FeesAndCredits.Builder()
.setCurrency(USD)
.addFeeOrCredit(Fee.create(BigDecimal.valueOf(100), FeeType.CREATE, false))
.build()))
.isEqualTo(RenewalPriceInfo.create(DEFAULT, null));
}
@TestOfyAndSql
void
testGetRenewalPriceInfo_isNotAnchorTenantWithSpecifiedInToken_returnsSpecifiedAndCreatePrice() {
AllocationToken token =
persistResource(
new AllocationToken.Builder()
.setToken("abc123")
.setTokenType(SINGLE_USE)
.setRenewalPriceBehavior(SPECIFIED)
.build());
assertThat(
DomainCreateFlow.getRenewalPriceInfo(
false,
Optional.of(token),
new FeesAndCredits.Builder()
.setCurrency(USD)
.addFeeOrCredit(Fee.create(BigDecimal.valueOf(100), FeeType.CREATE, false))
.build()))
.isEqualTo(RenewalPriceInfo.create(SPECIFIED, Money.of(USD, 100)));
}
@TestOfyAndSql
void testGetRenewalPriceInfo_isAnchorTenantWithSpecifiedStateInToken_throwsError() {
IllegalArgumentException thrown =
assertThrows(
IllegalArgumentException.class,
() ->
DomainCreateFlow.getRenewalPriceInfo(
true,
Optional.of(
persistResource(
new AllocationToken.Builder()
.setToken("abc123")
.setTokenType(SINGLE_USE)
.setRenewalPriceBehavior(SPECIFIED)
.build())),
new FeesAndCredits.Builder()
.setCurrency(USD)
.addFeeOrCredit(Fee.create(BigDecimal.valueOf(0), FeeType.CREATE, true))
.build()));
assertThat(thrown)
.hasMessageThat()
.isEqualTo("Renewal price behavior cannot be SPECIFIED for anchor tenant");
}
@TestOfyAndSql
void testGetRenewalPriceInfo_withInvalidRenewalPriceBehavior_throwsError() {
IllegalArgumentException thrown =
assertThrows(
IllegalArgumentException.class,
() ->
DomainCreateFlow.getRenewalPriceInfo(
true,
Optional.of(
persistResource(
new AllocationToken.Builder()
.setToken("abc123")
.setTokenType(SINGLE_USE)
.setRenewalPriceBehavior(RenewalPriceBehavior.valueOf("INVALID"))
.build())),
new FeesAndCredits.Builder()
.setCurrency(USD)
.addFeeOrCredit(Fee.create(BigDecimal.valueOf(0), FeeType.CREATE, true))
.build()));
assertThat(thrown)
.hasMessageThat()
.isEqualTo(
"No enum constant"
+ " google.registry.model.billing.BillingEvent.RenewalPriceBehavior.INVALID");
}
}
@@ -45,6 +45,7 @@ import google.registry.testing.TestOfyAndSql;
import org.joda.money.Money;
import org.junit.jupiter.api.BeforeEach;
/** Unit tests for {@link DomainFlowUtils}. */
@DualDatabaseTest
class DomainFlowUtilsTest extends ResourceFlowTestCase<DomainInfoFlow, DomainBase> {
@@ -0,0 +1,405 @@
// Copyright 2022 The Nomulus Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package google.registry.flows.domain;
import static com.google.common.truth.Truth.assertThat;
import static com.google.common.truth.Truth8.assertThat;
import static google.registry.model.billing.BillingEvent.Flag.AUTO_RENEW;
import static google.registry.model.billing.BillingEvent.RenewalPriceBehavior.DEFAULT;
import static google.registry.model.billing.BillingEvent.RenewalPriceBehavior.NONPREMIUM;
import static google.registry.model.billing.BillingEvent.RenewalPriceBehavior.SPECIFIED;
import static google.registry.model.domain.fee.BaseFee.FeeType.RENEW;
import static google.registry.model.reporting.HistoryEntry.Type.DOMAIN_CREATE;
import static google.registry.testing.DatabaseHelper.createTld;
import static google.registry.testing.DatabaseHelper.newDomainBase;
import static google.registry.testing.DatabaseHelper.persistPremiumList;
import static google.registry.testing.DatabaseHelper.persistResource;
import static google.registry.util.DateTimeUtils.END_OF_TIME;
import static google.registry.util.DateTimeUtils.START_OF_TIME;
import static org.joda.money.CurrencyUnit.USD;
import static org.joda.time.DateTimeZone.UTC;
import static org.junit.jupiter.api.Assertions.assertThrows;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.ImmutableSortedMap;
import google.registry.flows.EppException;
import google.registry.flows.FlowMetadata;
import google.registry.flows.HttpSessionMetadata;
import google.registry.flows.SessionMetadata;
import google.registry.flows.custom.DomainPricingCustomLogic;
import google.registry.model.billing.BillingEvent;
import google.registry.model.billing.BillingEvent.Reason;
import google.registry.model.billing.BillingEvent.Recurring;
import google.registry.model.billing.BillingEvent.RenewalPriceBehavior;
import google.registry.model.domain.DomainBase;
import google.registry.model.domain.DomainHistory;
import google.registry.model.domain.fee.Fee;
import google.registry.model.eppinput.EppInput;
import google.registry.model.tld.Registry;
import google.registry.testing.AppEngineExtension;
import google.registry.testing.DualDatabaseTest;
import google.registry.testing.FakeClock;
import google.registry.testing.FakeHttpSession;
import google.registry.testing.TestOfyAndSql;
import google.registry.util.Clock;
import java.util.Optional;
import javax.inject.Inject;
import org.joda.money.Money;
import org.joda.time.DateTime;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.extension.RegisterExtension;
import org.mockito.Mock;
/** Unit tests for {@link DomainPricingLogic}. */
@DualDatabaseTest
public class DomainPricingLogicTest {
DomainPricingLogic domainPricingLogic = new DomainPricingLogic();
@RegisterExtension
public final AppEngineExtension appEngine =
AppEngineExtension.builder().withDatastoreAndCloudSql().build();
@Inject Clock clock = new FakeClock(DateTime.now(UTC));
@Mock EppInput eppInput;
SessionMetadata sessionMetadata;
@Mock FlowMetadata flowMetadata;
Registry registry;
DomainBase domain;
@BeforeEach
void beforeEach() throws Exception {
createTld("example");
sessionMetadata = new HttpSessionMetadata(new FakeHttpSession());
domainPricingLogic.customLogic =
new DomainPricingCustomLogic(eppInput, sessionMetadata, flowMetadata);
registry =
persistResource(
Registry.get("example")
.asBuilder()
.setRenewBillingCostTransitions(
ImmutableSortedMap.of(
START_OF_TIME, Money.of(USD, 1), clock.nowUtc(), Money.of(USD, 10)))
.setPremiumList(persistPremiumList("tld2", USD, "premium,USD 100"))
.build());
}
/** helps to set up the domain info and returns a recurring billing event for testing */
private Recurring persistDomainAndSetRecurringBillingEvent(
String domainName, RenewalPriceBehavior renewalPriceBehavior, Optional<Money> renewalPrice) {
domain =
persistResource(
newDomainBase(domainName)
.asBuilder()
.setCreationTimeForTest(DateTime.parse("1999-01-05T00:00:00Z"))
.build());
DomainHistory historyEntry =
persistResource(
new DomainHistory.Builder()
.setRegistrarId(domain.getCreationRegistrarId())
.setType(DOMAIN_CREATE)
.setModificationTime(DateTime.parse("1999-01-05T00:00:00Z"))
.setDomain(domain)
.build());
Recurring recurring =
persistResource(
new BillingEvent.Recurring.Builder()
.setParent(historyEntry)
.setRegistrarId(domain.getCreationRegistrarId())
.setEventTime(DateTime.parse("1999-01-05T00:00:00Z"))
.setFlags(ImmutableSet.of(AUTO_RENEW))
.setId(2L)
.setReason(Reason.RENEW)
.setRenewalPriceBehavior(renewalPriceBehavior)
.setRenewalPrice(renewalPrice.isPresent() ? renewalPrice.get() : null)
.setRecurrenceEndTime(END_OF_TIME)
.setTargetId(domain.getDomainName())
.build());
persistResource(domain.asBuilder().setAutorenewBillingEvent(recurring.createVKey()).build());
return recurring;
}
@TestOfyAndSql
void testGetDomainRenewPrice_oneYear_standardDomain_noBilling_isStandardPrice()
throws EppException {
assertThat(
domainPricingLogic.getRenewPrice(registry, "standard.example", clock.nowUtc(), 1, null))
.isEqualTo(
new FeesAndCredits.Builder()
.setCurrency(USD)
.addFeeOrCredit(Fee.create(Money.of(USD, 10).getAmount(), RENEW, false))
.build());
}
@TestOfyAndSql
void testGetDomainRenewPrice_multiYear_standardDomain_noBilling_isStandardPrice()
throws EppException {
assertThat(
domainPricingLogic.getRenewPrice(registry, "standard.example", clock.nowUtc(), 5, null))
.isEqualTo(
new FeesAndCredits.Builder()
.setCurrency(USD)
.addFeeOrCredit(Fee.create(Money.of(USD, 50).getAmount(), RENEW, false))
.build());
}
@TestOfyAndSql
void testGetDomainRenewPrice_oneYear_premiumDomain_noBilling_isPremiumPrice()
throws EppException {
assertThat(
domainPricingLogic.getRenewPrice(registry, "premium.example", clock.nowUtc(), 1, null))
.isEqualTo(
new FeesAndCredits.Builder()
.setCurrency(USD)
.addFeeOrCredit(Fee.create(Money.of(USD, 100).getAmount(), RENEW, true))
.build());
}
@TestOfyAndSql
void testGetDomainRenewPrice_multiYear_premiumDomain_noBilling_isPremiumPrice()
throws EppException {
assertThat(
domainPricingLogic.getRenewPrice(registry, "premium.example", clock.nowUtc(), 5, null))
.isEqualTo(
new FeesAndCredits.Builder()
.setCurrency(USD)
.addFeeOrCredit(Fee.create(Money.of(USD, 500).getAmount(), RENEW, true))
.build());
}
@TestOfyAndSql
void testGetDomainRenewPrice_oneYear_premiumDomain_default_isPremiumPrice() throws EppException {
assertThat(
domainPricingLogic.getRenewPrice(
registry,
"premium.example",
clock.nowUtc(),
1,
persistDomainAndSetRecurringBillingEvent(
"premium.example", DEFAULT, Optional.empty())))
.isEqualTo(
new FeesAndCredits.Builder()
.setCurrency(USD)
.addFeeOrCredit(Fee.create(Money.of(USD, 100).getAmount(), RENEW, true))
.build());
}
@TestOfyAndSql
void testGetDomainRenewPrice_multiYear_premiumDomain_default_isPremiumCost() throws EppException {
assertThat(
domainPricingLogic.getRenewPrice(
registry,
"premium.example",
clock.nowUtc(),
5,
persistDomainAndSetRecurringBillingEvent(
"premium.example", DEFAULT, Optional.empty())))
.isEqualTo(
new FeesAndCredits.Builder()
.setCurrency(USD)
.addFeeOrCredit(Fee.create(Money.of(USD, 500).getAmount(), RENEW, true))
.build());
}
@TestOfyAndSql
void testGetDomainRenewPrice_oneYear_standardDomain_default_isNonPremiumPrice()
throws EppException {
assertThat(
domainPricingLogic.getRenewPrice(
registry,
"standard.example",
clock.nowUtc(),
1,
persistDomainAndSetRecurringBillingEvent(
"premium.example", DEFAULT, Optional.empty())))
.isEqualTo(
new FeesAndCredits.Builder()
.setCurrency(USD)
.addFeeOrCredit(Fee.create(Money.of(USD, 10).getAmount(), RENEW, false))
.build());
}
@TestOfyAndSql
void testGetDomainRenewPrice_multiYear_standardDomain_default_isNonPremiumCost()
throws EppException {
assertThat(
domainPricingLogic.getRenewPrice(
registry,
"standard.example",
clock.nowUtc(),
5,
persistDomainAndSetRecurringBillingEvent(
"standard.example", DEFAULT, Optional.empty())))
.isEqualTo(
new FeesAndCredits.Builder()
.setCurrency(USD)
.addFeeOrCredit(Fee.create(Money.of(USD, 50).getAmount(), RENEW, false))
.build());
}
@TestOfyAndSql
void testGetDomainRenewPrice_oneYear_premiumDomain_anchorTenant_isNonPremiumPrice()
throws EppException {
assertThat(
domainPricingLogic.getRenewPrice(
registry,
"premium.example",
clock.nowUtc(),
1,
persistDomainAndSetRecurringBillingEvent(
"premium.example", NONPREMIUM, Optional.empty())))
.isEqualTo(
new FeesAndCredits.Builder()
.setCurrency(USD)
.addFeeOrCredit(Fee.create(Money.of(USD, 10).getAmount(), RENEW, false))
.build());
}
@TestOfyAndSql
void testGetDomainRenewPrice_multiYear_premiumDomain_anchorTenant_isNonPremiumCost()
throws EppException {
assertThat(
domainPricingLogic.getRenewPrice(
registry,
"premium.example",
clock.nowUtc(),
5,
persistDomainAndSetRecurringBillingEvent(
"premium.example", NONPREMIUM, Optional.empty())))
.isEqualTo(
new FeesAndCredits.Builder()
.setCurrency(USD)
.addFeeOrCredit(Fee.create(Money.of(USD, 50).getAmount(), RENEW, false))
.build());
}
@TestOfyAndSql
void testGetDomainRenewPrice_oneYear_standardDomain_anchorTenant_isNonPremiumPrice()
throws EppException {
assertThat(
domainPricingLogic.getRenewPrice(
registry,
"standard.example",
clock.nowUtc(),
1,
persistDomainAndSetRecurringBillingEvent(
"standard.example", NONPREMIUM, Optional.empty())))
.isEqualTo(
new FeesAndCredits.Builder()
.setCurrency(USD)
.addFeeOrCredit(Fee.create(Money.of(USD, 10).getAmount(), RENEW, false))
.build());
}
@TestOfyAndSql
void testGetDomainRenewPrice_multiYear_standardDomain_anchorTenant_isNonPremiumCost()
throws EppException {
assertThat(
domainPricingLogic.getRenewPrice(
registry,
"standard.example",
clock.nowUtc(),
5,
persistDomainAndSetRecurringBillingEvent(
"standard.example", NONPREMIUM, Optional.empty())))
.isEqualTo(
new FeesAndCredits.Builder()
.setCurrency(USD)
.addFeeOrCredit(Fee.create(Money.of(USD, 50).getAmount(), RENEW, false))
.build());
}
@TestOfyAndSql
void testGetDomainRenewPrice_oneYear_standardDomain_internalRegistration_isSpecifiedPrice()
throws EppException {
assertThat(
domainPricingLogic.getRenewPrice(
registry,
"standard.example",
clock.nowUtc(),
1,
persistDomainAndSetRecurringBillingEvent(
"standard.example", SPECIFIED, Optional.of(Money.of(USD, 1)))))
.isEqualTo(
new FeesAndCredits.Builder()
.setCurrency(USD)
.addFeeOrCredit(Fee.create(Money.of(USD, 1).getAmount(), RENEW, false))
.build());
}
@TestOfyAndSql
void testGetDomainRenewPrice_multiYear_standardDomain_internalRegistration_isSpecifiedPrice()
throws EppException {
assertThat(
domainPricingLogic.getRenewPrice(
registry,
"standard.example",
clock.nowUtc(),
5,
persistDomainAndSetRecurringBillingEvent(
"standard.example", SPECIFIED, Optional.of(Money.of(USD, 1)))))
.isEqualTo(
new FeesAndCredits.Builder()
.setCurrency(USD)
.addFeeOrCredit(Fee.create(Money.of(USD, 5).getAmount(), RENEW, false))
.build());
}
@TestOfyAndSql
void testGetDomainRenewPrice_oneYear_premiumDomain_internalRegistration_isSpecifiedPrice()
throws EppException {
assertThat(
domainPricingLogic.getRenewPrice(
registry,
"premium.example",
clock.nowUtc(),
1,
persistDomainAndSetRecurringBillingEvent(
"premium.example", SPECIFIED, Optional.of(Money.of(USD, 17)))))
.isEqualTo(
new FeesAndCredits.Builder()
.setCurrency(USD)
.addFeeOrCredit(Fee.create(Money.of(USD, 17).getAmount(), RENEW, false))
.build());
}
@TestOfyAndSql
void testGetDomainRenewPrice_multiYear_premiumDomain_internalRegistration_isSpecifiedPrice()
throws EppException {
assertThat(
domainPricingLogic.getRenewPrice(
registry,
"premium.example",
clock.nowUtc(),
5,
persistDomainAndSetRecurringBillingEvent(
"premium.example", SPECIFIED, Optional.of(Money.of(USD, 17)))))
.isEqualTo(
new FeesAndCredits.Builder()
.setCurrency(USD)
.addFeeOrCredit(Fee.create(Money.of(USD, 85).getAmount(), RENEW, false))
.build());
}
@TestOfyAndSql
void testGetDomainRenewPrice_negativeYear_throwsException() throws EppException {
IllegalArgumentException thrown =
assertThrows(
IllegalArgumentException.class,
() ->
domainPricingLogic.getRenewPrice(
registry, "standard.example", clock.nowUtc(), -1, null));
assertThat(thrown).hasMessageThat().isEqualTo("Number of years must be positive");
}
}
@@ -121,7 +121,6 @@ class RegistrarTest extends EntityTestCase {
.setIcannReferralEmail("foo@example.com")
.setDriveFolderId("drive folder id")
.setIanaIdentifier(8L)
.setBillingIdentifier(5325L)
.setBillingAccountMap(
ImmutableMap.of(CurrencyUnit.USD, "abc123", CurrencyUnit.JPY, "789xyz"))
.setPhonePasscode("01234")
@@ -261,12 +260,11 @@ class RegistrarTest extends EntityTestCase {
}
@TestOfyAndSql
void testSuccess_clearingIanaAndBillingIds() {
void testSuccess_clearingIanaId() {
registrar
.asBuilder()
.setType(Type.TEST)
.setIanaIdentifier(null)
.setBillingIdentifier(null)
.build();
}
@@ -15,6 +15,7 @@
package google.registry.rdap;
import static com.google.common.truth.Truth.assertThat;
import static google.registry.rdap.RdapDataStructures.EventAction.TRANSFER;
import static google.registry.rdap.RdapTestHelper.assertThat;
import static google.registry.testing.DatabaseHelper.createTld;
import static google.registry.testing.DatabaseHelper.persistResource;
@@ -28,7 +29,9 @@ import static google.registry.testing.TestDataHelper.loadFile;
import static javax.servlet.http.HttpServletResponse.SC_BAD_REQUEST;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Maps;
import com.google.gson.Gson;
import com.google.gson.JsonObject;
import google.registry.model.contact.ContactResource;
@@ -51,6 +54,7 @@ import google.registry.testing.DualDatabaseTest;
import google.registry.testing.FakeClock;
import google.registry.testing.InjectExtension;
import google.registry.testing.TestOfyAndSql;
import google.registry.testing.TestSqlOnly;
import org.joda.time.DateTime;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.extension.RegisterExtension;
@@ -482,6 +486,17 @@ class RdapJsonFormatterTest {
.isEqualTo(loadJson("rdapjson_domain_summary.json"));
}
@TestSqlOnly
void testGetLastHistoryEntryByType() {
// Expected data are from "rdapjson_domain_summary.json"
assertThat(
Maps.transformValues(
rdapJsonFormatter.getLastHistoryEntryByType(domainBaseFull),
HistoryEntry::getModificationTime))
.containsExactlyEntriesIn(
ImmutableMap.of(TRANSFER, DateTime.parse("1999-12-01T00:00:00.000Z")));
}
@TestOfyAndSql
void testDomain_logged_out() {
rdapJsonFormatter.rdapAuthorization = RdapAuthorization.PUBLIC_AUTHORIZATION;
@@ -147,8 +147,8 @@ public final class RegistryTestServerMain {
: UserInfo.create(loginEmail, loginUserId))
.build();
appEngine.setUp();
AppEngineExtension.loadInitialData();
new JpaTestExtensions.Builder().buildIntegrationTestExtension().beforeEach(null);
AppEngineExtension.loadInitialData();
System.out.printf("%sLoading Datastore fixtures...%s\n", BLUE, RESET);
for (Fixture fixture : fixtures) {
fixture.load();
@@ -1,3 +1,4 @@
// Copyright 2020 The Nomulus Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
@@ -64,15 +65,13 @@ class DualDatabaseTestInvocationContextProvider implements TestTemplateInvocatio
@Override
public Stream<TestTemplateInvocationContext> provideTestTemplateInvocationContexts(
ExtensionContext context) {
TestTemplateInvocationContext ofyContext =
createInvocationContext(context.getDisplayName() + " with Datastore", DatabaseType.OFY);
TestTemplateInvocationContext sqlContext =
createInvocationContext(context.getDisplayName() + " with PostgreSQL", DatabaseType.JPA);
Method testMethod = context.getTestMethod().orElseThrow(IllegalStateException::new);
if (testMethod.isAnnotationPresent(TestOfyAndSql.class)) {
return Stream.of(ofyContext, sqlContext);
return Stream.of(sqlContext);
} else if (testMethod.isAnnotationPresent(TestOfyOnly.class)) {
return Stream.of(ofyContext);
throw new RuntimeException("Ofy-only test invoked.");
} else if (testMethod.isAnnotationPresent(TestSqlOnly.class)) {
return Stream.of(sqlContext);
} else {
@@ -73,10 +73,10 @@ public class DualDatabaseTestInvocationContextProviderTest {
@AfterAll
static void assertEachTransactionManagerIsUsed() {
assertThat(testBothDbsOfyCounter).isEqualTo(1);
assertThat(testBothDbsOfyCounter).isEqualTo(0);
assertThat(testBothDbsSqlCounter).isEqualTo(1);
assertThat(testOfyOnlyOfyCounter).isEqualTo(1);
assertThat(testOfyOnlyOfyCounter).isEqualTo(0);
assertThat(testOfyOnlySqlCounter).isEqualTo(0);
assertThat(testSqlOnlyOfyCounter).isEqualTo(0);
@@ -21,6 +21,7 @@ import static com.google.common.base.Strings.isNullOrEmpty;
import static com.google.common.truth.Truth.assertWithMessage;
import static java.nio.charset.StandardCharsets.UTF_8;
import com.google.common.flogger.FluentLogger;
import com.google.common.io.ByteSource;
import com.google.common.io.CharStreams;
import java.io.File;
@@ -43,6 +44,7 @@ import org.junit.jupiter.api.extension.ExtensionContext;
*/
public final class GpgSystemCommandExtension implements BeforeEachCallback, AfterEachCallback {
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
private static final File DEV_NULL = new File("/dev/null");
private static final String TEMP_FILE_PREFIX = "gpgtest";
@@ -122,9 +124,29 @@ public final class GpgSystemCommandExtension implements BeforeEachCallback, Afte
.isEqualTo(0);
}
static void deleteTree(File dir) {
for (File file : dir.listFiles()) {
if (file.isDirectory()) {
deleteTree(file);
} else {
file.delete();
}
}
dir.delete();
}
@Override
public void afterEach(ExtensionContext context) {
// TODO(weiminyu): we should delete the cwd tree.
// Kill the gpg-agent.
try {
exec("gpgconf", "--homedir", conf.getPath(), "--kill", "gpg-agent");
} catch (IOException e) {
logger.atInfo().log("Unable to kill gpg-agent: %s", e);
}
// Clean up the temporary directory.
deleteTree(cwd);
cwd = DEV_NULL;
conf = DEV_NULL;
}
@@ -19,10 +19,8 @@ import static java.lang.annotation.RetentionPolicy.RUNTIME;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import org.junit.jupiter.api.TestTemplate;
/** Annotation to indicate a test method will be executed only with Datastore. */
@Target({METHOD})
@Retention(RUNTIME)
@TestTemplate
public @interface TestOfyOnly {}
@@ -545,28 +545,6 @@ class CreateRegistrarCommandTest extends CommandTestCase<CreateRegistrarCommand>
assertThat(registrar.get().getIanaIdentifier()).isEqualTo(12345);
}
@TestOfyAndSql
void testSuccess_billingId() throws Exception {
runCommandForced(
"--name=blobio",
"--password=some_password",
"--registrar_type=REAL",
"--iana_id=8",
"--billing_id=12345",
"--passcode=01234",
"--icann_referral_email=foo@bar.test",
"--street=\"123 Fake St\"",
"--city Fakington",
"--state MA",
"--zip 00351",
"--cc US",
"clientz");
Optional<Registrar> registrar = Registrar.loadByRegistrarId("clientz");
assertThat(registrar).isPresent();
assertThat(registrar.get().getBillingIdentifier()).isEqualTo(12345);
}
@TestOfyAndSql
void testSuccess_poNumber() throws Exception {
runCommandForced(
@@ -805,7 +783,6 @@ class CreateRegistrarCommandTest extends CommandTestCase<CreateRegistrarCommand>
"--registrar_type=TEST",
"--icann_referral_email=foo@bar.test",
"--iana_id=null",
"--billing_id=null",
"--phone=null",
"--fax=null",
"--url=null",
@@ -821,7 +798,6 @@ class CreateRegistrarCommandTest extends CommandTestCase<CreateRegistrarCommand>
assertThat(registrarOptional).isPresent();
Registrar registrar = registrarOptional.get();
assertThat(registrar.getIanaIdentifier()).isNull();
assertThat(registrar.getBillingIdentifier()).isNull();
assertThat(registrar.getPhoneNumber()).isNull();
assertThat(registrar.getFaxNumber()).isNull();
assertThat(registrar.getUrl()).isNull();
@@ -835,7 +811,6 @@ class CreateRegistrarCommandTest extends CommandTestCase<CreateRegistrarCommand>
"--password=some_password",
"--registrar_type=TEST",
"--iana_id=",
"--billing_id=",
"--phone=",
"--fax=",
"--url=",
@@ -852,7 +827,6 @@ class CreateRegistrarCommandTest extends CommandTestCase<CreateRegistrarCommand>
assertThat(registrarOptional).isPresent();
Registrar registrar = registrarOptional.get();
assertThat(registrar.getIanaIdentifier()).isNull();
assertThat(registrar.getBillingIdentifier()).isNull();
assertThat(registrar.getPhoneNumber()).isNull();
assertThat(registrar.getFaxNumber()).isNull();
assertThat(registrar.getUrl()).isNull();
@@ -1514,48 +1488,6 @@ class CreateRegistrarCommandTest extends CommandTestCase<CreateRegistrarCommand>
"clientz"));
}
@TestOfyAndSql
void testFailure_negativeBillingId() {
assertThrows(
IllegalArgumentException.class,
() ->
runCommandForced(
"--name=blobio",
"--password=some_password",
"--registrar_type=REAL",
"--iana_id=8",
"--billing_id=-1",
"--passcode=01234",
"--icann_referral_email=foo@bar.test",
"--street=\"123 Fake St\"",
"--city Fakington",
"--state MA",
"--zip 00351",
"--cc US",
"clientz"));
}
@TestOfyAndSql
void testFailure_nonIntegerBillingId() {
assertThrows(
ParameterException.class,
() ->
runCommandForced(
"--name=blobio",
"--password=some_password",
"--registrar_type=REAL",
"--iana_id=8",
"--billing_id=ABC12345",
"--passcode=01234",
"--icann_referral_email=foo@bar.test",
"--street=\"123 Fake St\"",
"--city Fakington",
"--state MA",
"--zip 00351",
"--cc US",
"clientz"));
}
@TestOfyAndSql
void testFailure_missingPhonePasscode() {
assertThrows(
@@ -33,6 +33,7 @@ import google.registry.model.registrar.Registrar;
import google.registry.persistence.VKey;
import google.registry.testing.AppEngineExtension;
import java.util.Arrays;
import java.util.Optional;
import org.joda.time.DateTime;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
@@ -58,7 +59,7 @@ public class MutatingCommandTest {
void beforeEach() {
registrar1 = persistNewRegistrar("Registrar1", "Registrar1", Registrar.Type.REAL, 1L);
registrar2 = persistNewRegistrar("Registrar2", "Registrar2", Registrar.Type.REAL, 2L);
newRegistrar1 = registrar1.asBuilder().setBillingIdentifier(42L).build();
newRegistrar1 = registrar1.asBuilder().setPoNumber(Optional.of("23")).build();
newRegistrar2 = registrar2.asBuilder().setBlockPremiumNames(true).build();
createTld("tld");
@@ -94,18 +95,19 @@ public class MutatingCommandTest {
};
command.init();
String changes = command.prompt();
assertThat(changes).isEqualTo(
"Update HostResource@2-ROID\n"
+ "lastEppUpdateTime: null -> 2014-09-09T09:09:09.000Z\n"
+ "\n"
+ "Update HostResource@3-ROID\n"
+ "currentSponsorClientId: TheRegistrar -> Registrar2\n"
+ "\n"
+ "Update Registrar@Registrar1\n"
+ "billingIdentifier: null -> 42\n"
+ "\n"
+ "Update Registrar@Registrar2\n"
+ "blockPremiumNames: false -> true\n");
assertThat(changes)
.isEqualTo(
"Update HostResource@2-ROID\n"
+ "lastEppUpdateTime: null -> 2014-09-09T09:09:09.000Z\n"
+ "\n"
+ "Update HostResource@3-ROID\n"
+ "currentSponsorClientId: TheRegistrar -> Registrar2\n"
+ "\n"
+ "Update Registrar@Registrar1\n"
+ "poNumber: null -> 23\n"
+ "\n"
+ "Update Registrar@Registrar2\n"
+ "blockPremiumNames: false -> true\n");
String results = command.execute();
assertThat(results).isEqualTo("Updated 4 entities.\n");
assertThat(loadByEntity(host1)).isEqualTo(newHost1);
@@ -25,6 +25,7 @@ import static google.registry.testing.DatabaseHelper.newRegistry;
import static google.registry.testing.DatabaseHelper.persistResource;
import static google.registry.util.DateTimeUtils.START_OF_TIME;
import static org.joda.money.CurrencyUnit.JPY;
import static org.joda.money.CurrencyUnit.USD;
import static org.joda.time.DateTimeZone.UTC;
import static org.junit.jupiter.api.Assertions.assertThrows;
@@ -42,7 +43,6 @@ import google.registry.testing.AppEngineExtension;
import google.registry.util.CidrAddressBlock;
import java.math.BigDecimal;
import java.util.Optional;
import org.joda.money.CurrencyUnit;
import org.joda.money.Money;
import org.joda.time.DateTime;
import org.junit.jupiter.api.BeforeEach;
@@ -347,13 +347,6 @@ class UpdateRegistrarCommandTest extends CommandTestCase<UpdateRegistrarCommand>
assertThat(loadRegistrar("NewRegistrar").getIanaIdentifier()).isEqualTo(12345);
}
@Test
void testSuccess_billingId() throws Exception {
assertThat(loadRegistrar("NewRegistrar").getBillingIdentifier()).isNull();
runCommand("--billing_id=12345", "--force", "NewRegistrar");
assertThat(loadRegistrar("NewRegistrar").getBillingIdentifier()).isEqualTo(12345);
}
@Test
void testSuccess_poNumber() throws Exception {
assertThat(loadRegistrar("NewRegistrar").getPoNumber()).isEmpty();
@@ -368,7 +361,18 @@ class UpdateRegistrarCommandTest extends CommandTestCase<UpdateRegistrarCommand>
assertThat(loadRegistrar("NewRegistrar").getBillingAccountMap()).isEmpty();
runCommand("--billing_account_map=USD=abc123,JPY=789xyz", "--force", "NewRegistrar");
assertThat(loadRegistrar("NewRegistrar").getBillingAccountMap())
.containsExactly(CurrencyUnit.USD, "abc123", CurrencyUnit.JPY, "789xyz");
.containsExactly(USD, "abc123", JPY, "789xyz");
}
@Test
void testSuccess_billingAccountMap_nullify() throws Exception {
persistResource(
loadRegistrar("NewRegistrar")
.asBuilder()
.setBillingAccountMap(ImmutableMap.of(USD, "abc123", JPY, "789xyz"))
.build());
runCommand("--billing_account_map=\"\"", "--force", "NewRegistrar");
assertThat(loadRegistrar("NewRegistrar").getBillingAccountMap()).isEmpty();
}
@Test
@@ -415,8 +419,7 @@ class UpdateRegistrarCommandTest extends CommandTestCase<UpdateRegistrarCommand>
loadRegistrar("NewRegistrar").asBuilder().setBillingAccountMap(ImmutableMap.of()).build());
assertThat(loadRegistrar("NewRegistrar").getBillingAccountMap()).isEmpty();
runCommand("--billing_account_map=JPY=789xyz", "--allowed_tlds=foo", "--force", "NewRegistrar");
assertThat(loadRegistrar("NewRegistrar").getBillingAccountMap())
.containsExactly(CurrencyUnit.JPY, "789xyz");
assertThat(loadRegistrar("NewRegistrar").getBillingAccountMap()).containsExactly(JPY, "789xyz");
}
@Test
@@ -425,12 +428,11 @@ class UpdateRegistrarCommandTest extends CommandTestCase<UpdateRegistrarCommand>
persistResource(
loadRegistrar("NewRegistrar")
.asBuilder()
.setBillingAccountMap(
ImmutableMap.of(CurrencyUnit.USD, "abc123", CurrencyUnit.JPY, "789xyz"))
.setBillingAccountMap(ImmutableMap.of(USD, "abc123", JPY, "789xyz"))
.build());
runCommand("--billing_account_map=JPY=123xyz", "--allowed_tlds=foo", "--force", "NewRegistrar");
assertThat(loadRegistrar("NewRegistrar").getBillingAccountMap())
.containsExactly(CurrencyUnit.JPY, "123xyz", CurrencyUnit.USD, "abc123");
.containsExactly(JPY, "123xyz", USD, "abc123");
}
@Test
@@ -495,7 +497,7 @@ class UpdateRegistrarCommandTest extends CommandTestCase<UpdateRegistrarCommand>
.setContactsRequireSyncing(true)
.build());
// Make some unrelated change where we don't specify the flags for the booleans.
runCommandForced("--billing_id=12345", "NewRegistrar");
runCommandForced("NewRegistrar");
// Make sure that the boolean fields didn't get reset back to false.
Registrar reloadedRegistrar = loadRegistrar("NewRegistrar");
assertThat(reloadedRegistrar.getBlockPremiumNames()).isTrue();
@@ -520,7 +522,6 @@ class UpdateRegistrarCommandTest extends CommandTestCase<UpdateRegistrarCommand>
.asBuilder()
.setType(Type.PDT) // for non-null IANA ID
.setIanaIdentifier(9995L)
.setBillingIdentifier(1L)
.setPhoneNumber("+1.2125555555")
.setFaxNumber("+1.2125555556")
.setUrl("http://www.example.tld")
@@ -528,7 +529,6 @@ class UpdateRegistrarCommandTest extends CommandTestCase<UpdateRegistrarCommand>
.build());
assertThat(registrar.getIanaIdentifier()).isNotNull();
assertThat(registrar.getBillingIdentifier()).isNotNull();
assertThat(registrar.getPhoneNumber()).isNotNull();
assertThat(registrar.getFaxNumber()).isNotNull();
assertThat(registrar.getUrl()).isNotNull();
@@ -537,7 +537,6 @@ class UpdateRegistrarCommandTest extends CommandTestCase<UpdateRegistrarCommand>
runCommand(
"--registrar_type=TEST", // necessary for null IANA ID
"--iana_id=null",
"--billing_id=null",
"--phone=null",
"--fax=null",
"--url=null",
@@ -547,7 +546,6 @@ class UpdateRegistrarCommandTest extends CommandTestCase<UpdateRegistrarCommand>
registrar = loadRegistrar("NewRegistrar");
assertThat(registrar.getIanaIdentifier()).isNull();
assertThat(registrar.getBillingIdentifier()).isNull();
assertThat(registrar.getPhoneNumber()).isNull();
assertThat(registrar.getFaxNumber()).isNull();
assertThat(registrar.getUrl()).isNull();
@@ -563,7 +561,6 @@ class UpdateRegistrarCommandTest extends CommandTestCase<UpdateRegistrarCommand>
.asBuilder()
.setType(Type.PDT) // for non-null IANA ID
.setIanaIdentifier(9995L)
.setBillingIdentifier(1L)
.setPhoneNumber("+1.2125555555")
.setFaxNumber("+1.2125555556")
.setUrl("http://www.example.tld")
@@ -571,7 +568,6 @@ class UpdateRegistrarCommandTest extends CommandTestCase<UpdateRegistrarCommand>
.build());
assertThat(registrar.getIanaIdentifier()).isNotNull();
assertThat(registrar.getBillingIdentifier()).isNotNull();
assertThat(registrar.getPhoneNumber()).isNotNull();
assertThat(registrar.getFaxNumber()).isNotNull();
assertThat(registrar.getUrl()).isNotNull();
@@ -580,7 +576,6 @@ class UpdateRegistrarCommandTest extends CommandTestCase<UpdateRegistrarCommand>
runCommand(
"--registrar_type=TEST", // necessary for null IANA ID
"--iana_id=",
"--billing_id=",
"--phone=",
"--fax=",
"--url=",
@@ -590,7 +585,6 @@ class UpdateRegistrarCommandTest extends CommandTestCase<UpdateRegistrarCommand>
registrar = loadRegistrar("NewRegistrar");
assertThat(registrar.getIanaIdentifier()).isNull();
assertThat(registrar.getBillingIdentifier()).isNull();
assertThat(registrar.getPhoneNumber()).isNull();
assertThat(registrar.getFaxNumber()).isNull();
assertThat(registrar.getUrl()).isNull();
@@ -654,20 +648,6 @@ class UpdateRegistrarCommandTest extends CommandTestCase<UpdateRegistrarCommand>
ParameterException.class, () -> runCommand("--iana_id=ABC123", "--force", "NewRegistrar"));
}
@Test
void testFailure_negativeBillingId() {
assertThrows(
IllegalArgumentException.class,
() -> runCommand("--billing_id=-1", "--force", "NewRegistrar"));
}
@Test
void testFailure_nonIntegerBillingId() {
assertThrows(
ParameterException.class,
() -> runCommand("--billing_id=ABC123", "--force", "NewRegistrar"));
}
@Test
void testFailure_passcodeTooShort() {
assertThrows(
@@ -568,7 +568,6 @@ class google.registry.model.registrar.Registrar {
google.registry.model.registrar.Registrar$Type type;
google.registry.model.registrar.RegistrarAddress internationalizedAddress;
google.registry.model.registrar.RegistrarAddress localizedAddress;
java.lang.Long billingIdentifier;
java.lang.Long ianaIdentifier;
java.lang.String clientCertificate;
java.lang.String clientCertificateHash;
@@ -261,7 +261,7 @@ td.section {
</tr>
<tr>
<td class="property_name">generated on</td>
<td class="property_value">2022-05-05 16:42:10.390681</td>
<td class="property_value">2022-05-23 16:42:00.648335</td>
</tr>
<tr>
<td class="property_name">last flyway file</td>
@@ -284,7 +284,7 @@ td.section {
generated on
</text>
<text text-anchor="start" x="4055.5" y="-10.8" font-family="Helvetica,sans-Serif" font-size="14.00">
2022-05-05 16:42:10.390681
2022-05-23 16:42:00.648335
</text>
<polygon fill="none" stroke="#888888" points="3968,-4 3968,-44 4233,-44 4233,-4 3968,-4" /> <!-- allocationtoken_a08ccbef -->
<g id="node1" class="node">
@@ -261,7 +261,7 @@ td.section {
</tr>
<tr>
<td class="property_name">generated on</td>
<td class="property_value">2022-05-05 16:42:08.300052</td>
<td class="property_value">2022-05-23 16:41:58.740617</td>
</tr>
<tr>
<td class="property_name">last flyway file</td>
@@ -284,7 +284,7 @@ td.section {
generated on
</text>
<text text-anchor="start" x="4755.52" y="-10.8" font-family="Helvetica,sans-Serif" font-size="14.00">
2022-05-05 16:42:08.300052
2022-05-23 16:41:58.740617
</text>
<polygon fill="none" stroke="#888888" points="4668.02,-4 4668.02,-44 4933.02,-44 4933.02,-4 4668.02,-4" /> <!-- allocationtoken_a08ccbef -->
<g id="node1" class="node">
@@ -572,7 +572,6 @@
registrar_id text not null,
allowed_tlds text[],
billing_account_map hstore,
billing_identifier int8,
block_premium_names boolean not null,
client_certificate text,
client_certificate_hash text,
+7
View File
@@ -52,6 +52,13 @@ you will need to make any modifications beyond simple changes to
likely you'll need to add cronjobs, URL paths, Datastore indexes, and task
queues, and thus edit those associated XML files.
The existing codebase is configured for running a full-scale registry with
multiple TLDs. In order to deploy to App Engine, you will either need to
[increase your
quota](https://cloud.google.com/compute/quotas#requesting_additional_quota) to
allow for at least 100 running instances or reduce `max-instances` in the
backend `appengine-web.xml` files to 25 or less.
## Global configuration
Global configuration is managed through YAML files that are built with and
+3 -1
View File
@@ -72,7 +72,8 @@ steps:
sed -i s/'$${_IMAGE}'/nomulus-tool/g release/cloudbuild-tag.yaml
sed -i s/':$${TAG_NAME}'/@$digest/g release/cloudbuild-tag.yaml
sed -i s/'nomulus-tool:latest'/nomulus-tool@$digest/g release/cloudbuild-deploy-*.yaml
sed -i s/'nomulus-tool:latest'/nomulus-tool@$digest/g release/cloudbuild-schema-deploy-*.yaml
# schema-deploy and schema-verify scripts
sed -i s/'nomulus-tool:latest'/nomulus-tool@$digest/g release/cloudbuild-schema-*.yaml
# Build and stage Dataflow Flex templates.
- name: 'gcr.io/${PROJECT_ID}/builder:latest'
entrypoint: /bin/bash
@@ -143,6 +144,7 @@ artifacts:
- 'release/cloudbuild-deploy-*.yaml'
- 'release/cloudbuild-delete-*.yaml'
- 'release/cloudbuild-schema-deploy-*.yaml'
- 'release/cloudbuild-schema-verify-*.yaml'
timeout: 7200s
options:
+23 -1
View File
@@ -113,7 +113,20 @@ steps:
docker push gcr.io/${PROJECT_ID}/schema_deployer:latest
docker push gcr.io/${PROJECT_ID}/schema_deployer:${TAG_NAME}
dir: 'release/schema-deployer/'
# Do text replacement in the schema-deploy config, hardcoding image digests.
# Build the schema_verifier image and upload it to GCR.
- name: 'gcr.io/cloud-builders/docker'
entrypoint: /bin/bash
args:
- -c
- |
set -e
docker build -t gcr.io/${PROJECT_ID}/schema_verifier:${TAG_NAME} .
docker tag gcr.io/${PROJECT_ID}/schema_verifier:${TAG_NAME} \
gcr.io/${PROJECT_ID}/schema_verifier:latest
docker push gcr.io/${PROJECT_ID}/schema_verifier:latest
docker push gcr.io/${PROJECT_ID}/schema_verifier:${TAG_NAME}
dir: 'release/schema-verifier/'
# Do text replacement in the schema-deploy and schema-verify configs.
- name: 'gcr.io/cloud-builders/gcloud'
entrypoint: /bin/bash
args:
@@ -126,14 +139,23 @@ steps:
schema_deployer_digest=$( \
gcloud container images list-tags gcr.io/${PROJECT_ID}/schema_deployer \
--format='get(digest)' --filter='tags = ${TAG_NAME}')
schema_verifier_digest=$( \
gcloud container images list-tags gcr.io/${PROJECT_ID}/schema_verifier \
--format='get(digest)' --filter='tags = ${TAG_NAME}')
sed -i s/builder:latest/builder@$builder_digest/g \
release/cloudbuild-schema-deploy.yaml
sed -i s/builder:latest/builder@$builder_digest/g \
release/cloudbuild-schema-verify.yaml
sed -i s/schema_deployer:latest/schema_deployer@$schema_deployer_digest/g \
release/cloudbuild-schema-deploy.yaml
sed -i s/schema_verifier:latest/schema_verifier@$schema_verifier_digest/g \
release/cloudbuild-schema-verify.yaml
sed -i s/'$${TAG_NAME}'/${TAG_NAME}/g release/cloudbuild-schema-deploy.yaml
for environment in alpha crash sandbox production; do
sed s/'$${_ENV}'/${environment}/g release/cloudbuild-schema-deploy.yaml \
> release/cloudbuild-schema-deploy-${environment}.yaml
sed s/'$${_ENV}'/${environment}/g release/cloudbuild-schema-verify.yaml \
> release/cloudbuild-schema-verify-${environment}.yaml
done
# Upload the gradle binary to GCS if it does not exist and point URL in gradle wrapper to it.
- name: 'gcr.io/cloud-builders/gsutil'
+83
View File
@@ -0,0 +1,83 @@
# Verifies that the actual Cloud SQL schema in the environment specified by the
# '_ENV' variable is the same as the golden schema in the current release for
# that environment.
#
# To run the build locally, install cloud-build-local first.
# Then run:
# cloud-build-local --config=cloudbuild-schema-verify.yaml --dryrun=false \
# --substitutions=_ENV=[ENV] ..
#
# To manually trigger a build on GCB, run:
# gcloud builds submit --config=cloudbuild-schema-verify.yaml \
# --substitutions=_ENV=[ENV] ..
#
# To trigger a build automatically, follow the instructions below and add a trigger:
# https://cloud.google.com/cloud-build/docs/running-builds/automate-builds
#
# Note that the release process hardens the tags and variables in this file:
# - The 'latest' tag on docker images will be replaced by their image digests.
# - The ${_ENV} pattern will be replaced by the actual environment name.
# Please refer to ./cloudbuild-release.yaml for more details.
# Note 2: to work around issue in Spinnaker's 'Deployment Manifest' stage,
# variable references must avoid the ${var} format. Valid formats include
# $var or ${"${var}"}. This file use the former. Since _ENV is expanded in the
# copies sent to Spinnaker, we preserve the brackets around them for safe
# pattern matching during release.
# See https://github.com/spinnaker/spinnaker/issues/3028 for more information.
steps:
# Download and decrypt the nomulus tool credential, which has the privilege to
# start Cloud SQL proxy to all environments. This credential is also used to
# authenticate the nomulus tool when fetching the schema deployer credential in
# the next step.
- name: 'gcr.io/$PROJECT_ID/builder:latest'
volumes:
- name: 'secrets'
path: '/secrets'
entrypoint: /bin/bash
args:
- -c
- |
set -e
gcloud secrets versions access latest \
--secret nomulus-tool-cloudbuild-credential \
> /secrets/cloud_sql_credential.json
# Fetch the Cloud SQL credential for schema_deployer
- name: 'gcr.io/$PROJECT_ID/nomulus-tool:latest'
volumes:
- name: 'secrets'
path: '/secrets'
args:
- -e
- ${_ENV}
- --credential
- /secrets/cloud_sql_credential.json
- get_sql_credential
- --user
- schema_deployer
- --output
- /secrets/schema_deployer_credential.dec
# Download the jar with the expected schema.
- name: 'gcr.io/$PROJECT_ID/builder:latest'
volumes:
- name: 'schema'
path: '/schema'
entrypoint: /bin/bash
args:
- -c
- |
set -e
deployed_schema_tag=$(gsutil cat \
gs://$PROJECT_ID-deployed-tags/sql.${_ENV}.tag)
gsutil cp gs://$PROJECT_ID-deploy/${deployed_schema_tag}/schema.jar \
/schema
# Verify the schema
- name: 'gcr.io/$PROJECT_ID/schema_verifier:latest'
volumes:
- name: 'secrets'
path: '/secrets'
- name: 'schema'
path: '/schema'
timeout: 3600s
options:
machineType: 'E2_HIGHCPU_32'
+54
View File
@@ -0,0 +1,54 @@
# Copyright 2022 The Nomulus Authors. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# This Dockerfile builds an image that can be used in Google Cloud Build.
# We need the following programs to build the schema verifier:
# 1. Bash to execute a shell script.
# 2. Cloud SQL proxy for connection to the SQL instance.
# 3. The pg_dump tool.
# 4. The unzip command to extract the golden schema from the schema jar.
#
# Please refer to verify_deployed_sql_schema.sh for expected volumes and
# arguments.
FROM marketplace.gcr.io/google/ubuntu1804
ENV DEBIAN_FRONTEND=noninteractive LANG=en_US.UTF-8
# Install pg_dump v11 (same as current server version). This needs to be
# downloaded from postgresql's own repo, because ubuntu1804 is too old. With a
# newer image 'apt-get install postgresql-client-11' may be sufficient.
RUN apt-get update -y \
&& apt-get install locales -y \
&& locale-gen en_US.UTF-8 \
&& apt-get install curl gnupg lsb-release -y \
&& curl https://www.postgresql.org/media/keys/ACCC4CF8.asc | apt-key add - \
&& sh -c \
'echo "deb http://apt.postgresql.org/pub/repos/apt $(lsb_release -cs)-pgdg main" \
> /etc/apt/sources.list.d/pgdg.list' \
&& apt-get update -y \
&& apt install postgresql-client-11 -y
# Use unzip to extract files from jars.
RUN apt-get install zip -y
# Get netstat, used for checking Cloud SQL proxy readiness.
RUN apt-get install net-tools
COPY verify_deployed_sql_schema.sh /usr/local/bin/
COPY allowed_diffs.txt /
ADD https://dl.google.com/cloudsql/cloud_sql_proxy.linux.amd64 \
/usr/local/bin/cloud_sql_proxy
RUN chmod +x /usr/local/bin/cloud_sql_proxy
ENTRYPOINT [ "verify_deployed_sql_schema.sh" ]
@@ -0,0 +1,3 @@
CREATE EXTENSION IF NOT EXISTS pgaudit WITH SCHEMA public;
COMMENT ON EXTENSION pgaudit IS 'provides auditing functionality';
SET default_with_oids = false;
+92
View File
@@ -0,0 +1,92 @@
#!/bin/bash
# Copyright 2022 The Nomulus Authors. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# This script compares the actual schema in a Cloud SQL database with the golden
# schema in the corresponding release. It detects schema changes made outside
# the normal deployment process, e.g., those made during a troubleshooting
# session that were not cleaned up.
#
# Mounted volumes and required files in them:
# - /secrets/cloud_sql_credential.json: Cloud SQL proxy credential
# - /secrets/schema_deployer_credential.dec the schema_deployer user's
# database login credential.
# - /schema/schema.jar: the jar with the golden schema.
set -e
read -r cloud_sql_instance db_user db_password \
<<<$(cat /secrets/schema_deployer_credential.dec | awk '{print $1, $2, $3}')
# Unpack the golden schema from schema.jar
unzip -p /schema/schema.jar sql/schema/nomulus.golden.sql \
> /schema/nomulus.golden.sql
echo "$(date): Connecting to ${cloud_sql_instance}."
# Set up connection to the Cloud SQL instance.
# For now we use Cloud SQL Proxy to set up a SSL tunnel to the Cloud SQL
# instance. This has two drawbacks:
# - It starts a background process, which is an anti-pattern in Docker.
# - The main job needs to wait for a while for the proxy to come up.
# We will research for a better long-term solution.
#
# Other options considered:
# - Connect using Socket Factory in this script.
# * Drawback: need to manage version and transitive dependencies
# of the postgres-socket-factory jar.
# - Create a self-contained Java application that connects using socket factory
# * Drawback: Seems an overkill
trap "pkill cloud_sql_proxy" EXIT
cloud_sql_proxy -instances="${cloud_sql_instance}"=tcp:5432 \
--credential_file=/secrets/cloud_sql_credential.json &
set +e
# Wait for cloud_sql_proxy to start:
# first sleep 1 second for the process to launch, then loop until port is ready
# or the proxy process dies.
sleep 1
while ! netstat -an | grep ':5432 ' && pgrep cloud_sql_proxy; do sleep 1; done
if ! pgrep cloud_sql_proxy; then
echo "Cloud SQL Proxy failed to set up connection."
exit 1
fi
# Download the actual sql schema
PGPASSWORD=${db_password} pg_dump -h localhost -U "${db_user}" \
-f /schema/nomulus.actual.sql --schema-only --no-owner --no-privileges \
--exclude-table flyway_schema_history \
postgres
raw_diff=$(diff /schema/nomulus.golden.sql /schema/nomulus.actual.sql)
# Clean up the raw_diff:
# - Remove diff locations (e.g. "5,6c5,6): grep "^[<>]"
# - Remove leading bracket for easier grepping later: sed -e "s/^[<>]\s//g"
# - Remove comments and blank lines: grep -v -E "^--|^$"
# - Remove patterns in allowed_diffs.txt, which are custom Cloud SQL configs we
# cannot emulate in the golden schema.
effective_diff=$(echo "${raw_diff}" \
| grep "^[<>]" | sed -e "s/^[<>]\s//g" \
| grep -v -E "^--|^$" \
| grep -v -f /allowed_diffs.txt )
if [[ ${effective_diff} == "" ]]
then
echo "Golden and actual schemas match."
exit 0
else
echo "Golden and actual schemas do not match. Diff is:"
echo "${raw_diff}"
exit 1
fi