Validate that a registrar has billing accounts for all its allowed TLDs (#1601)

This will require edits to a substantial number of registrars on sandbox (nearly
all of them) because almost all of them have access to at least one TLD, but
almost none of them have any billing accounts set. Until this is set, any updates
to the existing registrars that aren't adding the billing accounts will cause
failures.

Unfortunately, there wasn't any less invasive foolproof way to implement this
change, and we already had one attempt to implement it on create registrar
command that wasn't working (because allowed TLDs tend not to be added on
initial registrar creation, but rather, afterwards as an update).
This commit is contained in:
Ben McIlwain 2022-04-22 16:33:18 -04:00 committed by GitHub
parent 147d133aef
commit 1e76eeed37
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
15 changed files with 238 additions and 46 deletions

View file

@ -38,6 +38,7 @@ import google.registry.model.registrar.RegistrarAddress;
import google.registry.model.registrar.RegistrarContact; import google.registry.model.registrar.RegistrarContact;
import google.registry.model.tld.Registry; import google.registry.model.tld.Registry;
import google.registry.model.tld.Registry.TldState; import google.registry.model.tld.Registry.TldState;
import google.registry.model.tld.Registry.TldType;
import google.registry.model.tld.label.PremiumList; import google.registry.model.tld.label.PremiumList;
import google.registry.model.tld.label.PremiumListDao; import google.registry.model.tld.label.PremiumListDao;
import google.registry.persistence.VKey; import google.registry.persistence.VKey;
@ -111,6 +112,17 @@ public final class OteAccountBuilder {
DateTime.parse("2030-03-01T00:00:00Z"), DateTime.parse("2030-03-01T00:00:00Z"),
Money.of(CurrencyUnit.USD, 0)); Money.of(CurrencyUnit.USD, 0));
/**
* The default billing account map applied to all OT&E registrars.
*
* <p>This contains dummy values for USD and JPY so that OT&amp;E registrars can be granted access
* to all existing TLDs in sandbox. Note that OT&amp;E is only on sandbox and thus these dummy
* values will never be used in production (the only environment where real invoicing takes
* place).
*/
public static final ImmutableMap<CurrencyUnit, String> DEFAULT_BILLING_ACCOUNT_MAP =
ImmutableMap.of(CurrencyUnit.USD, "123", CurrencyUnit.JPY, "456");
private final ImmutableMap<String, String> registrarIdToTld; private final ImmutableMap<String, String> registrarIdToTld;
private final Registry sunriseTld; private final Registry sunriseTld;
private final Registry gaTld; private final Registry gaTld;
@ -305,6 +317,7 @@ public final class OteAccountBuilder {
.setTldStateTransitions(ImmutableSortedMap.of(START_OF_TIME, initialTldState)) .setTldStateTransitions(ImmutableSortedMap.of(START_OF_TIME, initialTldState))
.setDnsWriters(ImmutableSet.of("VoidDnsWriter")) .setDnsWriters(ImmutableSet.of("VoidDnsWriter"))
.setPremiumList(premiumList.get()) .setPremiumList(premiumList.get())
.setTldType(TldType.TEST)
.setRoidSuffix( .setRoidSuffix(
String.format( String.format(
"%S%X", "%S%X",
@ -334,6 +347,7 @@ public final class OteAccountBuilder {
.setPhoneNumber("+1.2125550100") .setPhoneNumber("+1.2125550100")
.setIcannReferralEmail("nightmare@registrar.test") .setIcannReferralEmail("nightmare@registrar.test")
.setState(Registrar.State.ACTIVE) .setState(Registrar.State.ACTIVE)
.setBillingAccountMap(DEFAULT_BILLING_ACCOUNT_MAP)
.build(); .build();
} }

View file

@ -81,6 +81,7 @@ import google.registry.model.common.EntityGroupRoot;
import google.registry.model.registrar.Registrar.BillingAccountEntry.CurrencyMapper; import google.registry.model.registrar.Registrar.BillingAccountEntry.CurrencyMapper;
import google.registry.model.replay.DatastoreAndSqlEntity; import google.registry.model.replay.DatastoreAndSqlEntity;
import google.registry.model.tld.Registry; import google.registry.model.tld.Registry;
import google.registry.model.tld.Registry.TldType;
import google.registry.persistence.VKey; import google.registry.persistence.VKey;
import google.registry.util.CidrAddressBlock; import google.registry.util.CidrAddressBlock;
import java.security.cert.CertificateParsingException; import java.security.cert.CertificateParsingException;
@ -798,13 +799,9 @@ public class Registrar extends ImmutableObject
} }
public Builder setBillingAccountMap(@Nullable Map<CurrencyUnit, String> billingAccountMap) { public Builder setBillingAccountMap(@Nullable Map<CurrencyUnit, String> billingAccountMap) {
if (billingAccountMap == null) { getInstance().billingAccountMap =
getInstance().billingAccountMap = null; nullToEmptyImmutableCopy(billingAccountMap).entrySet().stream()
} else { .collect(toImmutableMap(Map.Entry::getKey, BillingAccountEntry::new));
getInstance().billingAccountMap =
billingAccountMap.entrySet().stream()
.collect(toImmutableMap(Map.Entry::getKey, BillingAccountEntry::new));
}
return this; return this;
} }
@ -1015,6 +1012,20 @@ public class Registrar extends ImmutableObject
String.format( String.format(
"Supplied IANA ID is not valid for %s registrar type: %s", "Supplied IANA ID is not valid for %s registrar type: %s",
getInstance().type, getInstance().ianaIdentifier)); getInstance().type, getInstance().ianaIdentifier));
// In order to grant access to real TLDs, the registrar must have a corresponding billing
// account ID for that TLD's billing currency.
ImmutableSet<String> nonBillableTlds =
Registry.get(getInstance().getAllowedTlds()).stream()
.filter(r -> r.getTldType() == TldType.REAL)
.filter(r -> !getInstance().getBillingAccountMap().containsKey(r.getCurrency()))
.map(Registry::getTldStr)
.collect(toImmutableSet());
checkArgument(
nonBillableTlds.isEmpty(),
"Cannot set these allowed, real TLDs because their currency is missing "
+ "from the billing account map: %s",
nonBillableTlds);
return cloneEmptyToNull(super.build()); return cloneEmptyToNull(super.build());
} }
} }

View file

@ -72,7 +72,7 @@ public final class Registries {
.stream() .stream()
.map(Key::getName) .map(Key::getName)
.collect(toImmutableSet()); .collect(toImmutableSet());
return Registry.getAll(tlds).stream() return Registry.get(tlds).stream()
.map(e -> Maps.immutableEntry(e.getTldStr(), e.getTldType())) .map(e -> Maps.immutableEntry(e.getTldStr(), e.getTldType()))
.collect(entriesToImmutableMap()); .collect(entriesToImmutableMap());
} else { } else {
@ -105,7 +105,7 @@ public final class Registries {
/** Returns the Registry entities themselves of the given type loaded fresh from Datastore. */ /** Returns the Registry entities themselves of the given type loaded fresh from Datastore. */
public static ImmutableSet<Registry> getTldEntitiesOfType(TldType type) { public static ImmutableSet<Registry> getTldEntitiesOfType(TldType type) {
return Registry.getAll(filterValues(cache.get(), equalTo(type)).keySet()); return Registry.get(filterValues(cache.get(), equalTo(type)).keySet());
} }
/** Pass-through check that the specified TLD exists, otherwise throw an IAE. */ /** Pass-through check that the specified TLD exists, otherwise throw an IAE. */

View file

@ -142,10 +142,16 @@ public class Registry extends ImmutableObject
/** The type of TLD, which determines things like backups and escrow policy. */ /** The type of TLD, which determines things like backups and escrow policy. */
public enum TldType { public enum TldType {
/** A real, official TLD. */ /**
* A real, official TLD (but not necessarily only on production).
*
* <p>Note that, to avoid unnecessary costly DB writes, {@link
* google.registry.model.reporting.DomainTransactionRecord}s are only written out for REAL TLDs
* (these transaction records are only used for ICANN reporting purposes).
*/
REAL, REAL,
/** A test TLD, for the prober. */ /** A test TLD, for the prober, OT&amp;E, and other testing purposes. */
TEST TEST
} }
@ -232,7 +238,7 @@ public class Registry extends ImmutableObject
} }
/** Returns the registry entities for the given TLD strings, throwing if any don't exist. */ /** Returns the registry entities for the given TLD strings, throwing if any don't exist. */
static ImmutableSet<Registry> getAll(Set<String> tlds) { public static ImmutableSet<Registry> get(Set<String> tlds) {
try { try {
ImmutableMap<String, Optional<Registry>> registries = CACHE.getAll(tlds); ImmutableMap<String, Optional<Registry>> registries = CACHE.getAll(tlds);
ImmutableSet<String> missingRegistries = ImmutableSet<String> missingRegistries =
@ -1000,7 +1006,7 @@ public class Registry extends ImmutableObject
instance.renewBillingCostTransitions.checkValidity(); instance.renewBillingCostTransitions.checkValidity();
instance.eapFeeSchedule.checkValidity(); instance.eapFeeSchedule.checkValidity();
// All costs must be in the expected currency. // All costs must be in the expected currency.
// TODO(b/21854155): When we move PremiumList into Datastore, verify its currency too. checkArgumentNotNull(instance.getCurrency(), "Currency must be set");
checkArgument( checkArgument(
instance.getStandardCreateCost().getCurrencyUnit().equals(instance.currency), instance.getStandardCreateCost().getCurrencyUnit().equals(instance.currency),
"Create cost must be in the registry's currency"); "Create cost must be in the registry's currency");

View file

@ -24,14 +24,11 @@ import static java.nio.charset.StandardCharsets.US_ASCII;
import static org.joda.time.DateTimeZone.UTC; import static org.joda.time.DateTimeZone.UTC;
import com.beust.jcommander.Parameter; import com.beust.jcommander.Parameter;
import com.google.common.base.Joiner;
import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet; import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Sets;
import google.registry.flows.certs.CertificateChecker; import google.registry.flows.certs.CertificateChecker;
import google.registry.model.registrar.Registrar; import google.registry.model.registrar.Registrar;
import google.registry.model.registrar.RegistrarAddress; import google.registry.model.registrar.RegistrarAddress;
import google.registry.model.tld.Registry;
import google.registry.tools.params.KeyValueMapParameter.CurrencyUnitToStringMap; import google.registry.tools.params.KeyValueMapParameter.CurrencyUnitToStringMap;
import google.registry.tools.params.OptionalLongParameter; import google.registry.tools.params.OptionalLongParameter;
import google.registry.tools.params.OptionalPhoneNumberParameter; import google.registry.tools.params.OptionalPhoneNumberParameter;
@ -46,7 +43,6 @@ import java.util.LinkedHashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Optional; import java.util.Optional;
import java.util.Set;
import javax.annotation.Nullable; import javax.annotation.Nullable;
import javax.inject.Inject; import javax.inject.Inject;
import org.joda.money.CurrencyUnit; import org.joda.money.CurrencyUnit;
@ -433,22 +429,6 @@ abstract class CreateOrUpdateRegistrarCommand extends MutatingCommand {
// Require a phone passcode. // Require a phone passcode.
checkArgument( checkArgument(
newRegistrar.getPhonePasscode() != null, "--passcode is required for REAL registrars."); newRegistrar.getPhonePasscode() != null, "--passcode is required for REAL registrars.");
// Check if registrar has billing account IDs for the currency of the TLDs that it is
// allowed to register.
ImmutableSet<CurrencyUnit> tldCurrencies =
newRegistrar
.getAllowedTlds()
.stream()
.map(tld -> Registry.get(tld).getCurrency())
.collect(toImmutableSet());
Set<CurrencyUnit> currenciesWithoutBillingAccountId =
newRegistrar.getBillingAccountMap() == null
? tldCurrencies
: Sets.difference(tldCurrencies, newRegistrar.getBillingAccountMap().keySet());
checkArgument(
currenciesWithoutBillingAccountId.isEmpty(),
"Need billing account map entries for currencies: %s",
Joiner.on(' ').join(currenciesWithoutBillingAccountId));
} }
stageEntityChange(oldRegistrar, newRegistrar); stageEntityChange(oldRegistrar, newRegistrar);

View file

@ -15,6 +15,7 @@
package google.registry.tools; package google.registry.tools;
import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableMap;
import google.registry.tools.javascrap.BackfillRegistrarBillingAccountsCommand;
import google.registry.tools.javascrap.CompareEscrowDepositsCommand; import google.registry.tools.javascrap.CompareEscrowDepositsCommand;
import google.registry.tools.javascrap.HardDeleteHostCommand; import google.registry.tools.javascrap.HardDeleteHostCommand;
@ -30,6 +31,7 @@ public final class RegistryTool {
public static final ImmutableMap<String, Class<? extends Command>> COMMAND_MAP = public static final ImmutableMap<String, Class<? extends Command>> COMMAND_MAP =
new ImmutableMap.Builder<String, Class<? extends Command>>() new ImmutableMap.Builder<String, Class<? extends Command>>()
.put("ack_poll_messages", AckPollMessagesCommand.class) .put("ack_poll_messages", AckPollMessagesCommand.class)
.put("backfill_registrar_billing_accounts", BackfillRegistrarBillingAccountsCommand.class)
.put("canonicalize_labels", CanonicalizeLabelsCommand.class) .put("canonicalize_labels", CanonicalizeLabelsCommand.class)
.put("check_domain", CheckDomainCommand.class) .put("check_domain", CheckDomainCommand.class)
.put("check_domain_claims", CheckDomainClaimsCommand.class) .put("check_domain_claims", CheckDomainClaimsCommand.class)

View file

@ -43,6 +43,7 @@ import google.registry.request.Modules.UrlConnectionServiceModule;
import google.registry.request.Modules.UrlFetchServiceModule; import google.registry.request.Modules.UrlFetchServiceModule;
import google.registry.request.Modules.UserServiceModule; import google.registry.request.Modules.UserServiceModule;
import google.registry.tools.AuthModule.LocalCredentialModule; import google.registry.tools.AuthModule.LocalCredentialModule;
import google.registry.tools.javascrap.BackfillRegistrarBillingAccountsCommand;
import google.registry.tools.javascrap.CompareEscrowDepositsCommand; import google.registry.tools.javascrap.CompareEscrowDepositsCommand;
import google.registry.tools.javascrap.HardDeleteHostCommand; import google.registry.tools.javascrap.HardDeleteHostCommand;
import google.registry.util.UtilsModule; import google.registry.util.UtilsModule;
@ -90,6 +91,8 @@ import javax.inject.Singleton;
interface RegistryToolComponent { interface RegistryToolComponent {
void inject(AckPollMessagesCommand command); void inject(AckPollMessagesCommand command);
void inject(BackfillRegistrarBillingAccountsCommand command);
void inject(CheckDomainClaimsCommand command); void inject(CheckDomainClaimsCommand command);
void inject(CheckDomainCommand command); void inject(CheckDomainCommand command);

View file

@ -0,0 +1,56 @@
// Copyright 2021 The Nomulus Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package google.registry.tools.javascrap;
import static com.google.common.base.Preconditions.checkState;
import static google.registry.model.OteAccountBuilder.DEFAULT_BILLING_ACCOUNT_MAP;
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
import com.beust.jcommander.Parameters;
import google.registry.config.RegistryEnvironment;
import google.registry.model.registrar.Registrar;
import google.registry.tools.CommandWithRemoteApi;
/**
* Backfills the billing account maps on all Registrars that don't have any set.
*
* <p>This should not (and cannot) be used on production. Its purpose is to backfill these values on
* sandbox, where most registrars have an empty billing account map, including all that were created
* for OT&amp;E. The same default billing account map is used for all registrars, and includes
* values for USD and JPY. The actual values here don't matter as we don't do invoicing on sandbox
* anyway.
*/
@Parameters(separators = " =", commandDescription = "Backfill registrar billing account maps.")
public class BackfillRegistrarBillingAccountsCommand implements CommandWithRemoteApi {
@Override
public void run() throws Exception {
checkState(
RegistryEnvironment.get() != RegistryEnvironment.PRODUCTION,
"Do not run this on production");
System.out.println("Populating billing account maps on all registrars missing them ...");
tm().transact(
() ->
tm().loadAllOfStream(Registrar.class)
.filter(r -> r.getBillingAccountMap().isEmpty())
.forEach(
r ->
tm().update(
r.asBuilder()
.setBillingAccountMap(DEFAULT_BILLING_ACCOUNT_MAP)
.build())));
System.out.println("Done!");
}
}

View file

@ -336,7 +336,11 @@ public class SyncRegistrarsSheetTest {
@TestOfyAndSql @TestOfyAndSql
void testRun_missingValues_stillWorks() throws Exception { void testRun_missingValues_stillWorks() throws Exception {
persistNewRegistrar("SomeRegistrar", "Some Registrar", Registrar.Type.REAL, 8L); persistResource(
persistNewRegistrar("SomeRegistrar", "Some Registrar", Registrar.Type.REAL, 8L)
.asBuilder()
.setBillingAccountMap(ImmutableMap.of())
.build());
newSyncRegistrarsSheet().run("foobar"); newSyncRegistrarsSheet().run("foobar");

View file

@ -30,11 +30,14 @@ import static google.registry.testing.DatabaseHelper.persistResource;
import static google.registry.testing.DatabaseHelper.persistSimpleResource; import static google.registry.testing.DatabaseHelper.persistSimpleResource;
import static google.registry.testing.DatabaseHelper.persistSimpleResources; import static google.registry.testing.DatabaseHelper.persistSimpleResources;
import static google.registry.util.DateTimeUtils.START_OF_TIME; 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.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertThrows;
import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet; import com.google.common.collect.ImmutableSet;
import com.google.common.collect.ImmutableSortedMap;
import com.google.common.collect.ImmutableSortedSet; import com.google.common.collect.ImmutableSortedSet;
import com.googlecode.objectify.Key; import com.googlecode.objectify.Key;
import google.registry.config.RegistryConfig; import google.registry.config.RegistryConfig;
@ -42,13 +45,17 @@ import google.registry.model.EntityTestCase;
import google.registry.model.registrar.Registrar.State; import google.registry.model.registrar.Registrar.State;
import google.registry.model.registrar.Registrar.Type; import google.registry.model.registrar.Registrar.Type;
import google.registry.model.tld.Registries; import google.registry.model.tld.Registries;
import google.registry.model.tld.Registry;
import google.registry.model.tld.Registry.TldType;
import google.registry.testing.DualDatabaseTest; import google.registry.testing.DualDatabaseTest;
import google.registry.testing.TestOfyAndSql; import google.registry.testing.TestOfyAndSql;
import google.registry.testing.TestOfyOnly; import google.registry.testing.TestOfyOnly;
import google.registry.testing.TestSqlOnly; import google.registry.testing.TestSqlOnly;
import google.registry.util.CidrAddressBlock; import google.registry.util.CidrAddressBlock;
import google.registry.util.SerializeUtils; import google.registry.util.SerializeUtils;
import java.math.BigDecimal;
import org.joda.money.CurrencyUnit; import org.joda.money.CurrencyUnit;
import org.joda.money.Money;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
/** Unit tests for {@link Registrar}. */ /** Unit tests for {@link Registrar}. */
@ -60,7 +67,20 @@ class RegistrarTest extends EntityTestCase {
@BeforeEach @BeforeEach
void setUp() { void setUp() {
createTld("xn--q9jyb4c"); createTld("tld");
persistResource(
newRegistry("xn--q9jyb4c", "MINNA")
.asBuilder()
.setCurrency(JPY)
.setCreateBillingCost(Money.of(JPY, new BigDecimal(1300)))
.setRestoreBillingCost(Money.of(JPY, new BigDecimal(1700)))
.setServerStatusChangeBillingCost(Money.of(JPY, new BigDecimal(1900)))
.setRegistryLockOrUnlockBillingCost(Money.of(JPY, new BigDecimal(2700)))
.setRenewBillingCostTransitions(
ImmutableSortedMap.of(START_OF_TIME, Money.of(JPY, new BigDecimal(1100))))
.setEapFeeSchedule(ImmutableSortedMap.of(START_OF_TIME, Money.zero(JPY)))
.setPremiumList(null)
.build());
// Set up a new persisted registrar entity. // Set up a new persisted registrar entity.
registrar = registrar =
cloneAndSetAutoTimestamps( cloneAndSetAutoTimestamps(
@ -251,8 +271,10 @@ class RegistrarTest extends EntityTestCase {
} }
@TestOfyAndSql @TestOfyAndSql
void testSuccess_clearingBillingAccountMap() { void testSuccess_clearingBillingAccountMapAndAllowedTlds() {
registrar = registrar.asBuilder().setBillingAccountMap(null).build(); registrar =
registrar.asBuilder().setAllowedTlds(ImmutableSet.of()).setBillingAccountMap(null).build();
assertThat(registrar.getAllowedTlds()).isEmpty();
assertThat(registrar.getBillingAccountMap()).isEmpty(); assertThat(registrar.getBillingAccountMap()).isEmpty();
} }
@ -677,4 +699,48 @@ class RegistrarTest extends EntityTestCase {
assertThrows(IllegalArgumentException.class, () -> Registrar.loadByRegistrarIdCached("")); assertThrows(IllegalArgumentException.class, () -> Registrar.loadByRegistrarIdCached(""));
assertThat(thrown).hasMessageThat().contains("registrarId must be specified"); assertThat(thrown).hasMessageThat().contains("registrarId must be specified");
} }
@TestOfyAndSql
void testFailure_missingCurrenciesFromBillingMap() {
IllegalArgumentException thrown =
assertThrows(
IllegalArgumentException.class,
() ->
registrar
.asBuilder()
.setBillingAccountMap(null)
.setAllowedTlds(ImmutableSet.of("tld", "xn--q9jyb4c"))
.build());
assertThat(thrown)
.hasMessageThat()
.contains("their currency is missing from the billing account map: [tld, xn--q9jyb4c]");
}
@TestOfyAndSql
void testFailure_missingCurrencyFromBillingMap() {
IllegalArgumentException thrown =
assertThrows(
IllegalArgumentException.class,
() ->
registrar
.asBuilder()
.setBillingAccountMap(ImmutableMap.of(USD, "abc123"))
.setAllowedTlds(ImmutableSet.of("tld", "xn--q9jyb4c"))
.build());
assertThat(thrown)
.hasMessageThat()
.contains("their currency is missing from the billing account map: [xn--q9jyb4c]");
}
@TestOfyAndSql
void testSuccess_nonRealTldDoesntNeedEntryInBillingMap() {
persistResource(Registry.get("xn--q9jyb4c").asBuilder().setTldType(TldType.TEST).build());
// xn--q9jyb4c bills in JPY and we don't have a JPY entry in this billing account map, but it
// should succeed without throwing an error because xn--q9jyb4c is set to be a TEST TLD.
registrar
.asBuilder()
.setBillingAccountMap(ImmutableMap.of(USD, "abc123"))
.setAllowedTlds(ImmutableSet.of("tld", "xn--q9jyb4c"))
.build();
}
} }

View file

@ -190,7 +190,7 @@ public final class RegistryTest extends EntityTestCase {
@TestOfyAndSql @TestOfyAndSql
void testGetAll() { void testGetAll() {
createTld("foo"); createTld("foo");
assertThat(Registry.getAll(ImmutableSet.of("foo", "tld"))) assertThat(Registry.get(ImmutableSet.of("foo", "tld")))
.containsExactlyElementsIn( .containsExactlyElementsIn(
tm().transact( tm().transact(
() -> () ->

View file

@ -66,6 +66,7 @@ import java.util.Set;
import java.util.logging.LogManager; import java.util.logging.LogManager;
import java.util.stream.Stream; import java.util.stream.Stream;
import javax.annotation.Nullable; import javax.annotation.Nullable;
import org.joda.money.CurrencyUnit;
import org.json.JSONArray; import org.json.JSONArray;
import org.json.JSONException; import org.json.JSONException;
import org.json.JSONObject; import org.json.JSONObject;
@ -289,6 +290,7 @@ public final class AppEngineExtension implements BeforeEachCallback, AfterEachCa
.build()) .build())
.setPhoneNumber("+1.3334445555") .setPhoneNumber("+1.3334445555")
.setPhonePasscode("12345") .setPhonePasscode("12345")
.setBillingAccountMap(ImmutableMap.of(CurrencyUnit.USD, "abc123"))
.setContactsRequireSyncing(true); .setContactsRequireSyncing(true);
} }

View file

@ -762,6 +762,7 @@ public class DatabaseHelper {
.setType(type) .setType(type)
.setState(State.ACTIVE) .setState(State.ACTIVE)
.setIanaIdentifier(ianaIdentifier) .setIanaIdentifier(ianaIdentifier)
.setBillingAccountMap(ImmutableMap.of(USD, "abc123"))
.setLocalizedAddress( .setLocalizedAddress(
new RegistrarAddress.Builder() new RegistrarAddress.Builder()
.setStreet(ImmutableList.of("123 Fake St")) .setStreet(ImmutableList.of("123 Fake St"))

View file

@ -21,8 +21,11 @@ import static google.registry.testing.CertificateSamples.SAMPLE_CERT;
import static google.registry.testing.CertificateSamples.SAMPLE_CERT3; import static google.registry.testing.CertificateSamples.SAMPLE_CERT3;
import static google.registry.testing.CertificateSamples.SAMPLE_CERT3_HASH; import static google.registry.testing.CertificateSamples.SAMPLE_CERT3_HASH;
import static google.registry.testing.DatabaseHelper.createTlds; import static google.registry.testing.DatabaseHelper.createTlds;
import static google.registry.testing.DatabaseHelper.newRegistry;
import static google.registry.testing.DatabaseHelper.persistNewRegistrar; import static google.registry.testing.DatabaseHelper.persistNewRegistrar;
import static google.registry.testing.DatabaseHelper.persistResource;
import static google.registry.util.DateTimeUtils.START_OF_TIME; import static google.registry.util.DateTimeUtils.START_OF_TIME;
import static org.joda.money.CurrencyUnit.JPY;
import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.ArgumentMatchers.eq; import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verify;
@ -43,8 +46,10 @@ import google.registry.testing.DualDatabaseTest;
import google.registry.testing.InjectExtension; import google.registry.testing.InjectExtension;
import google.registry.testing.TestOfyAndSql; import google.registry.testing.TestOfyAndSql;
import java.io.IOException; import java.io.IOException;
import java.math.BigDecimal;
import java.util.Optional; import java.util.Optional;
import org.joda.money.CurrencyUnit; import org.joda.money.CurrencyUnit;
import org.joda.money.Money;
import org.joda.time.DateTime; import org.joda.time.DateTime;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.extension.RegisterExtension; import org.junit.jupiter.api.extension.RegisterExtension;
@ -604,11 +609,11 @@ class CreateRegistrarCommandTest extends CommandTestCase<CreateRegistrarCommand>
Optional<Registrar> registrar = Registrar.loadByRegistrarId("clientz"); Optional<Registrar> registrar = Registrar.loadByRegistrarId("clientz");
assertThat(registrar).isPresent(); assertThat(registrar).isPresent();
assertThat(registrar.get().getBillingAccountMap()) assertThat(registrar.get().getBillingAccountMap())
.containsExactly(CurrencyUnit.USD, "abc123", CurrencyUnit.JPY, "789xyz"); .containsExactly(CurrencyUnit.USD, "abc123", JPY, "789xyz");
} }
@TestOfyAndSql @TestOfyAndSql
void testFailure_billingAccountMap_doesNotContainEntryForTldAllowed() { void testFailure_billingAccountMap_doesNotContainEntryForAllowedTld() {
createTlds("foo"); createTlds("foo");
IllegalArgumentException thrown = IllegalArgumentException thrown =
@ -632,12 +637,26 @@ class CreateRegistrarCommandTest extends CommandTestCase<CreateRegistrarCommand>
"--cc US", "--cc US",
"--force", "--force",
"clientz")); "clientz"));
assertThat(thrown).hasMessageThat().contains("USD"); assertThat(thrown)
.hasMessageThat()
.contains("their currency is missing from the billing account map: [foo]");
} }
@TestOfyAndSql @TestOfyAndSql
void testSuccess_billingAccountMap_onlyAppliesToRealRegistrar() throws Exception { void testSuccess_billingAccountMap_onlyAppliesToRealRegistrar() throws Exception {
createTlds("foo"); persistResource(
newRegistry("foo", "FOO")
.asBuilder()
.setCurrency(JPY)
.setCreateBillingCost(Money.of(JPY, new BigDecimal(1300)))
.setRestoreBillingCost(Money.of(JPY, new BigDecimal(1700)))
.setServerStatusChangeBillingCost(Money.of(JPY, new BigDecimal(1900)))
.setRegistryLockOrUnlockBillingCost(Money.of(JPY, new BigDecimal(2700)))
.setRenewBillingCostTransitions(
ImmutableSortedMap.of(START_OF_TIME, Money.of(JPY, new BigDecimal(1100))))
.setEapFeeSchedule(ImmutableSortedMap.of(START_OF_TIME, Money.zero(JPY)))
.setPremiumList(null)
.build());
runCommandForced( runCommandForced(
"--name=blobio", "--name=blobio",
@ -656,7 +675,7 @@ class CreateRegistrarCommandTest extends CommandTestCase<CreateRegistrarCommand>
Optional<Registrar> registrar = Registrar.loadByRegistrarId("clientz"); Optional<Registrar> registrar = Registrar.loadByRegistrarId("clientz");
assertThat(registrar).isPresent(); assertThat(registrar).isPresent();
assertThat(registrar.get().getBillingAccountMap()).containsExactly(CurrencyUnit.JPY, "789xyz"); assertThat(registrar.get().getBillingAccountMap()).containsExactly(JPY, "789xyz");
} }
@TestOfyAndSql @TestOfyAndSql

View file

@ -21,8 +21,10 @@ import static google.registry.testing.CertificateSamples.SAMPLE_CERT3;
import static google.registry.testing.CertificateSamples.SAMPLE_CERT3_HASH; import static google.registry.testing.CertificateSamples.SAMPLE_CERT3_HASH;
import static google.registry.testing.DatabaseHelper.createTlds; import static google.registry.testing.DatabaseHelper.createTlds;
import static google.registry.testing.DatabaseHelper.loadRegistrar; import static google.registry.testing.DatabaseHelper.loadRegistrar;
import static google.registry.testing.DatabaseHelper.newRegistry;
import static google.registry.testing.DatabaseHelper.persistResource; import static google.registry.testing.DatabaseHelper.persistResource;
import static google.registry.util.DateTimeUtils.START_OF_TIME; import static google.registry.util.DateTimeUtils.START_OF_TIME;
import static org.joda.money.CurrencyUnit.JPY;
import static org.joda.time.DateTimeZone.UTC; import static org.joda.time.DateTimeZone.UTC;
import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertThrows;
@ -38,8 +40,10 @@ import google.registry.model.registrar.Registrar.State;
import google.registry.model.registrar.Registrar.Type; import google.registry.model.registrar.Registrar.Type;
import google.registry.testing.AppEngineExtension; import google.registry.testing.AppEngineExtension;
import google.registry.util.CidrAddressBlock; import google.registry.util.CidrAddressBlock;
import java.math.BigDecimal;
import java.util.Optional; import java.util.Optional;
import org.joda.money.CurrencyUnit; import org.joda.money.CurrencyUnit;
import org.joda.money.Money;
import org.joda.time.DateTime; import org.joda.time.DateTime;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
@ -359,6 +363,8 @@ class UpdateRegistrarCommandTest extends CommandTestCase<UpdateRegistrarCommand>
@Test @Test
void testSuccess_billingAccountMap() throws Exception { void testSuccess_billingAccountMap() throws Exception {
persistResource(
loadRegistrar("NewRegistrar").asBuilder().setBillingAccountMap(ImmutableMap.of()).build());
assertThat(loadRegistrar("NewRegistrar").getBillingAccountMap()).isEmpty(); assertThat(loadRegistrar("NewRegistrar").getBillingAccountMap()).isEmpty();
runCommand("--billing_account_map=USD=abc123,JPY=789xyz", "--force", "NewRegistrar"); runCommand("--billing_account_map=USD=abc123,JPY=789xyz", "--force", "NewRegistrar");
assertThat(loadRegistrar("NewRegistrar").getBillingAccountMap()) assertThat(loadRegistrar("NewRegistrar").getBillingAccountMap())
@ -366,8 +372,14 @@ class UpdateRegistrarCommandTest extends CommandTestCase<UpdateRegistrarCommand>
} }
@Test @Test
void testFailure_billingAccountMap_doesNotContainEntryForTldAllowed() { void testFailure_billingAccountMap_doesNotContainEntryForAllowedTld() {
createTlds("foo"); createTlds("foo");
persistResource(
loadRegistrar("NewRegistrar")
.asBuilder()
.setAllowedTlds(ImmutableSet.of())
.setBillingAccountMap(ImmutableMap.of())
.build());
assertThat(loadRegistrar("NewRegistrar").getBillingAccountMap()).isEmpty(); assertThat(loadRegistrar("NewRegistrar").getBillingAccountMap()).isEmpty();
IllegalArgumentException thrown = IllegalArgumentException thrown =
assertThrows( assertThrows(
@ -379,12 +391,28 @@ class UpdateRegistrarCommandTest extends CommandTestCase<UpdateRegistrarCommand>
"--force", "--force",
"--registrar_type=REAL", "--registrar_type=REAL",
"NewRegistrar")); "NewRegistrar"));
assertThat(thrown).hasMessageThat().contains("USD"); assertThat(thrown)
.hasMessageThat()
.contains("their currency is missing from the billing account map: [foo]");
} }
@Test @Test
void testSuccess_billingAccountMap_onlyAppliesToRealRegistrar() throws Exception { void testSuccess_billingAccountMap_onlyAppliesToRealRegistrar() throws Exception {
createTlds("foo"); persistResource(
newRegistry("foo", "FOO")
.asBuilder()
.setCurrency(JPY)
.setCreateBillingCost(Money.of(JPY, new BigDecimal(1300)))
.setRestoreBillingCost(Money.of(JPY, new BigDecimal(1700)))
.setServerStatusChangeBillingCost(Money.of(JPY, new BigDecimal(1900)))
.setRegistryLockOrUnlockBillingCost(Money.of(JPY, new BigDecimal(2700)))
.setRenewBillingCostTransitions(
ImmutableSortedMap.of(START_OF_TIME, Money.of(JPY, new BigDecimal(1100))))
.setEapFeeSchedule(ImmutableSortedMap.of(START_OF_TIME, Money.zero(JPY)))
.setPremiumList(null)
.build());
persistResource(
loadRegistrar("NewRegistrar").asBuilder().setBillingAccountMap(ImmutableMap.of()).build());
assertThat(loadRegistrar("NewRegistrar").getBillingAccountMap()).isEmpty(); assertThat(loadRegistrar("NewRegistrar").getBillingAccountMap()).isEmpty();
runCommand("--billing_account_map=JPY=789xyz", "--allowed_tlds=foo", "--force", "NewRegistrar"); runCommand("--billing_account_map=JPY=789xyz", "--allowed_tlds=foo", "--force", "NewRegistrar");
assertThat(loadRegistrar("NewRegistrar").getBillingAccountMap()) assertThat(loadRegistrar("NewRegistrar").getBillingAccountMap())