Add --upsert flag to update list commands (#3248)

Add --upsert (-u) flag to UpdatePremiumListCommand and
UpdateReservedListCommand so that missing premium and reserved lists are
created automatically when --upsert is specified, while still failing
when --upsert is omitted and the list does not exist.

Allow CurrencyUnit to be nullable in PremiumListUtils.parseToPremiumList()
so that PremiumList.createFromLine() infers the currency unit from the
input lines when currency is unset, and use this in
UpdatePremiumListCommand when creating a new list via --upsert. Also fix
the mixed-currency example in config/files/premium/example.txt.

Also propagate exceptions from CreateOrUpdatePremiumListCommand.execute()
and CreateOrUpdateReservedListCommand.execute() instead of catching and
swallowing Throwable, and validate non-empty and parseable input data in
CreatePremiumListCommand.prompt(). Add comprehensive unit tests across
PremiumListUtilsTest and the create/update list command test suites.

BUG=http://b/569977604
This commit is contained in:
Ben McIlwain
2026-10-14 22:28:20 +00:00
committed by GitHub
parent c9aba86833
commit 19b20b7313
14 changed files with 547 additions and 69 deletions
@@ -4,4 +4,4 @@
# These are manipulated using the "nomulus" tool
# {create,update,delete,list}_premium_list commands.
foo,USD 100 # comment after the item
bar,JPY 14
bar,USD 14
@@ -200,16 +200,23 @@ public final class PremiumList extends BaseDomainLabelList<BigDecimal, PremiumEn
List<String> parts = Splitter.on(',').trimResults().splitToList(line);
checkArgument(parts.size() == 2, "Could not parse line in premium list: %s", originalLine);
List<String> moneyParts = Splitter.on(' ').trimResults().splitToList(parts.get(1));
if (moneyParts.size() == 2 && this.currency != null) {
if (!Money.parse(parts.get(1)).getCurrencyUnit().equals(this.currency)) {
BigDecimal price;
if (moneyParts.size() == 2) {
Money money = Money.parse(parts.get(1));
// If no currency was explicitly set on the PremiumList prior to parsing (e.g., when creating
// a new list via `update_premium_list --upsert` or validating a raw file), infer the list's
// currency from the first entry that includes a currency code and enforce that all
// subsequent entries use the same currency.
if (this.currency == null) {
this.currency = money.getCurrencyUnit();
} else if (!money.getCurrencyUnit().equals(this.currency)) {
throw new IllegalArgumentException(
String.format("The currency unit must be %s", this.currency.getCode()));
}
price = money.getAmount();
} else {
price = new BigDecimal(parts.get(1));
}
BigDecimal price =
moneyParts.size() == 2
? Money.parse(parts.get(1)).getAmount()
: new BigDecimal(parts.get(1));
return new PremiumEntry.Builder()
.setLabel(parts.get(0))
.setPrice(price)
@@ -14,6 +14,8 @@
package google.registry.model.tld.label;
import static com.google.common.base.Preconditions.checkArgument;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Maps;
import google.registry.model.tld.label.PremiumList.PremiumEntry;
@@ -21,13 +23,32 @@ import java.math.BigDecimal;
import java.time.Instant;
import java.util.List;
import java.util.Map;
import javax.annotation.Nullable;
import org.joda.money.CurrencyUnit;
/** Static utility methods for {@link PremiumList}. */
public class PremiumListUtils {
/**
* Parses the given CSV input lines into a {@link PremiumList}.
*
* <p>The {@code currencyUnit} is explicitly passed in when creating a list via {@code
* create_premium_list} (which requires the {@code --currency} flag) or when updating an existing
* list in the database (to ensure the updated entries match the existing list's persisted
* currency, and to support bare-number price entries that omit the currency code).
*
* <p>When {@code currencyUnit} is {@code null} (such as when creating a new list via {@code
* update_premium_list --upsert}, which does not take a {@code --currency} flag and has no
* existing database revision to consult), {@link PremiumList#createFromLine} infers the list's
* currency from the first input line that specifies a currency code and validates that all
* subsequent lines match it. If {@code currencyUnit} is {@code null} and no input line specifies
* a currency code, an {@link IllegalArgumentException} is thrown.
*/
public static PremiumList parseToPremiumList(
String name, CurrencyUnit currencyUnit, List<String> inputData, Instant creationTime) {
String name,
@Nullable CurrencyUnit currencyUnit,
List<String> inputData,
Instant creationTime) {
PremiumList partialPremiumList =
new PremiumList.Builder()
.setName(name)
@@ -35,6 +56,9 @@ public class PremiumListUtils {
.setCreationTimestamp(creationTime)
.build();
ImmutableMap<String, PremiumEntry> prices = partialPremiumList.parse(inputData);
checkArgument(
partialPremiumList.getCurrency() != null,
"Could not determine currency for premium list from input file");
Map<String, BigDecimal> priceAmounts = Maps.transformValues(prices, PremiumEntry::getValue);
return partialPremiumList.asBuilder().setLabelsToPrices(priceAmounts).build();
}
@@ -72,14 +72,9 @@ abstract class CreateOrUpdatePremiumListCommand extends ConfirmingCommand {
public String execute() throws Exception {
String message =
String.format("Saved premium list %s with %d entries.", name, inputData.size());
try {
logger.atInfo().log("Saving premium list for TLD %s.", name);
tm().transact(() -> PremiumListDao.save(name, currency, inputData));
logger.atInfo().log(message);
} catch (Throwable e) {
message = "Unexpected error saving premium list from nomulus tool command.";
logger.atSevere().withCause(e).log(message);
}
logger.atInfo().log("Saving premium list for TLD %s.", name);
tm().transact(() -> PremiumListDao.save(name, currency, inputData));
logger.atInfo().log(message);
return message;
}
@@ -15,8 +15,11 @@
package google.registry.tools;
import static com.google.common.base.Preconditions.checkArgument;
import static google.registry.model.tld.Tlds.assertTldExists;
import com.beust.jcommander.Parameter;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Splitter;
import com.google.common.flogger.FluentLogger;
import google.registry.model.tld.label.ReservedList;
import google.registry.model.tld.label.ReservedListDao;
@@ -24,6 +27,7 @@ import google.registry.tools.params.PathParameter;
import google.registry.util.Clock;
import jakarta.inject.Inject;
import java.nio.file.Path;
import java.util.List;
import java.util.stream.Collectors;
import javax.annotation.Nullable;
@@ -37,6 +41,11 @@ public abstract class CreateOrUpdateReservedListCommand extends ConfirmingComman
static final FluentLogger logger = FluentLogger.forEnclosingClass();
@VisibleForTesting
static final String INVALID_FORMAT_ERROR_MESSAGE =
"The name must be in the format {tld|common}_list-name "
+ "and contain only letters, numbers, and hyphens, plus a single underscore delimiter";
@Parameter(
names = {"-d", "--dry_run"},
description = "Does not execute the entity mutation")
@@ -71,14 +80,9 @@ public abstract class CreateOrUpdateReservedListCommand extends ConfirmingComman
String.format(
"Saved reserved list %s with %d entries.",
name, reservedList.getReservedListEntries().size());
try {
logger.atInfo().log("Saving reserved list for TLD %s.", name);
ReservedListDao.save(reservedList);
logger.atInfo().log(message);
} catch (Throwable e) {
message = "Unexpected error saving reserved list from nomulus tool command.";
logger.atSevere().withCause(e).log(message);
}
logger.atInfo().log("Saving reserved list for TLD %s.", name);
ReservedListDao.save(reservedList);
logger.atInfo().log(message);
return message;
}
@@ -102,4 +106,16 @@ public abstract class CreateOrUpdateReservedListCommand extends ConfirmingComman
"The --build_environment flag must be used when running in production");
return super.checkExecutionState();
}
static void validateListName(String name, boolean checkTldExists) {
List<String> nameParts = Splitter.on('_').splitToList(name);
checkArgument(nameParts.size() == 2, INVALID_FORMAT_ERROR_MESSAGE);
String tld = nameParts.get(0);
checkArgument(tld.matches("[-a-zA-Z0-9]+"), INVALID_FORMAT_ERROR_MESSAGE);
if (checkTldExists && !tld.equals("common")) {
assertTldExists(
tld, "The name must be in the format {tld|common}_list-name, yet TLD %s does not exist");
}
checkArgument(nameParts.get(1).matches("[-a-zA-Z0-9]+"), INVALID_FORMAT_ERROR_MESSAGE);
}
}
@@ -16,6 +16,7 @@ package google.registry.tools;
import static com.google.common.base.Preconditions.checkArgument;
import static google.registry.model.tld.Tlds.assertTldExists;
import static google.registry.model.tld.label.PremiumListUtils.parseToPremiumList;
import static google.registry.util.ListNamingUtils.convertFilePathToName;
import static java.nio.charset.StandardCharsets.UTF_8;
@@ -56,6 +57,9 @@ public class CreatePremiumListCommand extends CreateOrUpdatePremiumListCommand {
+ " (unless --override is specified), yet TLD %s does not exist");
}
inputData = Files.readAllLines(inputFile, UTF_8);
PremiumList premiumList = parseToPremiumList(name, currency, inputData, clock.now());
checkArgument(
!premiumList.getLabelsToPrices().isEmpty(), "New premium list data cannot be empty");
return String.format("Create new premium list for %s?", name);
}
}
@@ -15,14 +15,11 @@
package google.registry.tools;
import static com.google.common.base.Preconditions.checkArgument;
import static google.registry.model.tld.Tlds.assertTldExists;
import static google.registry.util.ListNamingUtils.convertFilePathToName;
import static java.nio.charset.StandardCharsets.UTF_8;
import com.beust.jcommander.Parameter;
import com.beust.jcommander.Parameters;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Splitter;
import com.google.common.base.Strings;
import google.registry.model.tld.label.ReservedList;
import java.nio.file.Files;
@@ -33,11 +30,6 @@ import java.util.List;
@Parameters(separators = " =", commandDescription = "Create a ReservedList.")
final class CreateReservedListCommand extends CreateOrUpdateReservedListCommand {
@VisibleForTesting
static final String INVALID_FORMAT_ERROR_MESSAGE =
"The name must be in the format {tld|common}_list-name "
+ "and contain only letters, numbers, and hyphens, plus a single underscore delimiter";
@Parameter(
names = {"-o", "--override"},
description = "Override restrictions on reserved list naming")
@@ -48,7 +40,7 @@ final class CreateReservedListCommand extends CreateOrUpdateReservedListCommand
name = Strings.isNullOrEmpty(name) ? convertFilePathToName(input) : name;
checkArgument(ReservedList.get(name).isEmpty(), "A reserved list already exists by this name");
if (!override) {
validateListName(name);
validateListName(name, true);
}
Instant now = clock.now();
List<String> allLines = Files.readAllLines(input, UTF_8);
@@ -62,15 +54,4 @@ final class CreateReservedListCommand extends CreateOrUpdateReservedListCommand
return String.format(
"%s\nreservedListMap=%s\n", reservedList, outputReservedListEntries(reservedList));
}
private static void validateListName(String name) {
List<String> nameParts = Splitter.on('_').splitToList(name);
checkArgument(nameParts.size() == 2, INVALID_FORMAT_ERROR_MESSAGE);
String tld = nameParts.get(0);
if (!tld.equals("common")) {
assertTldExists(
tld, "The name must be in the format {tld|common}_list-name, yet TLD %s does not exist");
}
checkArgument(nameParts.get(1).matches("[-a-zA-Z0-9]+"), INVALID_FORMAT_ERROR_MESSAGE);
}
}
@@ -15,50 +15,67 @@
package google.registry.tools;
import static com.google.common.base.Preconditions.checkArgument;
import static google.registry.model.tld.label.PremiumListUtils.parseToPremiumList;
import static google.registry.util.ListNamingUtils.convertFilePathToName;
import static java.nio.charset.StandardCharsets.UTF_8;
import com.beust.jcommander.Parameter;
import com.beust.jcommander.Parameters;
import com.google.common.base.Strings;
import google.registry.model.tld.label.PremiumList;
import google.registry.model.tld.label.PremiumListDao;
import google.registry.model.tld.label.PremiumListUtils;
import java.nio.file.Files;
import java.util.Optional;
/** Command to safely update {@link PremiumList} in Database for a given TLD. */
@Parameters(separators = " =", commandDescription = "Update a PremiumList in Database.")
class UpdatePremiumListCommand extends CreateOrUpdatePremiumListCommand {
@Parameter(
names = {"-u", "--upsert"},
description = "Create the premium list if it does not already exist.")
boolean upsert;
// Indicates if there is a new change made by this command
private boolean newChange = false;
@Override
protected String prompt() throws Exception {
name = Strings.isNullOrEmpty(name) ? convertFilePathToName(inputFile) : name;
PremiumList existingList =
PremiumListDao.getLatestRevision(name)
.orElseThrow(
() ->
new IllegalArgumentException(
String.format(
"Could not update premium list %s because it doesn't exist", name)));
Optional<PremiumList> existingList = PremiumListDao.getLatestRevision(name);
checkArgument(
upsert || existingList.isPresent(),
"Could not update premium list %s because it doesn't exist",
name);
inputData = Files.readAllLines(inputFile, UTF_8);
checkArgument(!inputData.isEmpty(), "New premium list data cannot be empty");
currency = existingList.getCurrency();
// If the list already exists in the database, pass its persisted currency so that all entries
// are validated against it. Otherwise (when creating a new list via --upsert), pass null so
// that parseToPremiumList infers the currency from the input file entries.
PremiumList updatedPremiumList =
PremiumListUtils.parseToPremiumList(name, currency, inputData, clock.now());
parseToPremiumList(
name, existingList.map(PremiumList::getCurrency).orElse(null), inputData, clock.now());
checkArgument(
!updatedPremiumList.getLabelsToPrices().isEmpty(), "New premium list data cannot be empty");
currency = updatedPremiumList.getCurrency();
if (existingList.isEmpty()) {
newChange = true;
return String.format(
"Create new premium list for %s?\n New List: %s", name, updatedPremiumList);
}
if (!existingList
.get()
.getLabelsToPrices()
.entrySet()
.equals(updatedPremiumList.getLabelsToPrices().entrySet())) {
newChange = true;
return String.format(
"Update premium list for %s?\n Old List: %s\n New List: %s",
name, existingList, updatedPremiumList);
name, existingList.get(), updatedPremiumList);
} else {
return String.format(
"This update contains no changes to the premium list for %s.\n List Contents: %s",
name, existingList);
name, existingList.get());
}
}
@@ -14,48 +14,67 @@
package google.registry.tools;
import static com.google.common.base.Preconditions.checkArgument;
import static google.registry.util.DiffUtils.prettyPrintEntityDeepDiff;
import static google.registry.util.ListNamingUtils.convertFilePathToName;
import static java.nio.charset.StandardCharsets.UTF_8;
import com.beust.jcommander.Parameter;
import com.beust.jcommander.Parameters;
import com.google.common.base.Strings;
import google.registry.model.tld.label.ReservedList;
import java.nio.file.Files;
import java.util.List;
import java.util.Optional;
/** Command to safely update {@link ReservedList}. */
@Parameters(separators = " =", commandDescription = "Update a ReservedList.")
final class UpdateReservedListCommand extends CreateOrUpdateReservedListCommand {
@Parameter(
names = {"-u", "--upsert"},
description = "Create the reserved list if it does not already exist.")
boolean upsert;
// indicates if there is a new change made by this command
private boolean newChange = true;
@Override
protected String prompt() throws Exception {
name = Strings.isNullOrEmpty(name) ? convertFilePathToName(input) : name;
ReservedList existingReservedList =
ReservedList.get(name)
.orElseThrow(
() ->
new IllegalArgumentException(
String.format(
"Could not update reserved list %s because it doesn't exist.", name)));
Optional<ReservedList> existingReservedList = ReservedList.get(name);
checkArgument(
upsert || existingReservedList.isPresent(),
"Could not update reserved list %s because it doesn't exist.",
name);
List<String> allLines = Files.readAllLines(input, UTF_8);
ReservedList.Builder updated =
existingReservedList.asBuilder().setReservedListMapFromLines(allLines);
if (existingReservedList.isEmpty()) {
// Validate the {tld|common}_list-name format without requiring the TLD to exist in the
// database yet (as reserved lists may be synced before configure_tld runs for a new TLD).
validateListName(name, false);
newChange = true;
reservedList =
new ReservedList.Builder()
.setName(name)
.setReservedListMapFromLines(allLines)
.setCreationTimestamp(clock.now())
.build();
return String.format(
"Create new reserved list for %s?\n%s\nreservedListMap=%s\n",
name, reservedList, outputReservedListEntries(reservedList));
}
ReservedList existing = existingReservedList.get();
ReservedList.Builder updated = existing.asBuilder().setReservedListMapFromLines(allLines);
reservedList = updated.build();
boolean reservedListEntriesChanged =
!existingReservedList
.getReservedListEntries()
.equals(reservedList.getReservedListEntries());
!existing.getReservedListEntries().equals(reservedList.getReservedListEntries());
if (!reservedListEntriesChanged) {
newChange = false;
return "No entity changes to apply.";
}
return String.format("Update reserved list for %s?\n", name)
+ prettyPrintEntityDeepDiff(
existingReservedList.getReservedListEntries(), reservedList.getReservedListEntries());
existing.getReservedListEntries(), reservedList.getReservedListEntries());
}
@Override
@@ -16,6 +16,7 @@ package google.registry.model.tld.label;
import static com.google.common.truth.Truth.assertThat;
import static google.registry.model.tld.label.PremiumListUtils.parseToPremiumList;
import static org.joda.money.CurrencyUnit.EUR;
import static org.joda.money.CurrencyUnit.USD;
import static org.junit.jupiter.api.Assertions.assertThrows;
@@ -56,6 +57,66 @@ class PremiumListUtilsTest {
assertThat(thrown).hasMessageThat().isEqualTo("The currency unit must be USD");
}
@Test
void parseInputToPremiumList_nullCurrency_infersFromInput() {
PremiumList premiumList =
parseToPremiumList(
"testlist",
null,
ImmutableList.of("# leading comment", " ", "foo,EUR 99.50 # inline", "bar,EUR 30"),
SAMPLE_TIME);
assertThat(premiumList.getName()).isEqualTo("testlist");
assertThat(premiumList.getCurrency()).isEqualTo(EUR);
assertThat(premiumList.getLabelsToPrices())
.containsExactly("foo", twoDigits(99.50), "bar", twoDigits(30));
}
@Test
void parseInputToPremiumList_nullCurrency_throwsOnInconsistentCurrencies() {
IllegalArgumentException thrown =
assertThrows(
IllegalArgumentException.class,
() ->
parseToPremiumList(
"testlist",
null,
ImmutableList.of("foo,EUR 99.50", "bar,USD 30"),
SAMPLE_TIME));
assertThat(thrown).hasMessageThat().isEqualTo("The currency unit must be EUR");
}
@Test
void parseInputToPremiumList_nullCurrency_throwsWhenCurrencyCannotBeDetermined() {
IllegalArgumentException thrown =
assertThrows(
IllegalArgumentException.class,
() ->
parseToPremiumList(
"testlist",
null,
ImmutableList.of("# comment only", "foo,99.50"),
SAMPLE_TIME));
assertThat(thrown)
.hasMessageThat()
.isEqualTo("Could not determine currency for premium list from input file");
}
@Test
void parseInputToPremiumList_nullCurrency_throwsOnMalformedLine() {
IllegalArgumentException thrown =
assertThrows(
IllegalArgumentException.class,
() ->
parseToPremiumList(
"testlist",
null,
ImmutableList.of("malformed_line_without_comma"),
SAMPLE_TIME));
assertThat(thrown)
.hasMessageThat()
.isEqualTo("Could not parse line in premium list: malformed_line_without_comma");
}
private static BigDecimal twoDigits(double num) {
return BigDecimal.valueOf((long) (num * 100.0), 2);
}
@@ -15,12 +15,14 @@
package google.registry.tools;
import static com.google.common.truth.Truth.assertThat;
import static java.nio.charset.StandardCharsets.UTF_8;
import static org.junit.jupiter.api.Assertions.assertThrows;
import com.google.common.io.Files;
import google.registry.model.tld.Tld;
import google.registry.model.tld.label.PremiumListDao;
import google.registry.testing.DatabaseHelper;
import java.io.File;
import java.nio.file.Path;
import java.nio.file.Paths;
import org.junit.jupiter.api.BeforeEach;
@@ -76,6 +78,65 @@ class CreatePremiumListCommandTest<C extends CreatePremiumListCommand>
assertThrows(NullPointerException.class, command::prompt);
}
@Test
void commandPrompt_failureEmptyInputFile() throws Exception {
Path emptyFile = tmpDir.resolve(TLD_TEST + "_empty.txt");
Files.write(new byte[0], emptyFile.toFile());
command.name = TLD_TEST;
command.inputFile = emptyFile;
command.currencyUnit = "USD";
IllegalArgumentException thrown = assertThrows(IllegalArgumentException.class, command::prompt);
assertThat(thrown).hasMessageThat().isEqualTo("New premium list data cannot be empty");
}
@Test
void commandPrompt_failureCommentOnlyInputFile() throws Exception {
Path commentOnlyFile = tmpDir.resolve(TLD_TEST + "_comments.txt");
Files.asCharSink(commentOnlyFile.toFile(), UTF_8).write("# only a comment\n \n");
command.name = TLD_TEST;
command.inputFile = commentOnlyFile;
command.currencyUnit = "USD";
IllegalArgumentException thrown = assertThrows(IllegalArgumentException.class, command::prompt);
assertThat(thrown).hasMessageThat().isEqualTo("New premium list data cannot be empty");
}
@Test
void commandPrompt_failureMalformedInputFile() throws Exception {
Path malformedFile = tmpDir.resolve(TLD_TEST + "_malformed_prompt.txt");
Files.asCharSink(malformedFile.toFile(), UTF_8).write("malformed_line_without_comma\n");
command.name = TLD_TEST;
command.inputFile = malformedFile;
command.currencyUnit = "USD";
IllegalArgumentException thrown = assertThrows(IllegalArgumentException.class, command::prompt);
assertThat(thrown).hasMessageThat().contains("Could not parse line in premium list");
}
@Test
void commandRun_failureMalformedInputFile() throws Exception {
File malformedFile = tmpDir.resolve(TLD_TEST + "_malformed.txt").toFile();
Files.asCharSink(malformedFile, UTF_8).write("malformed_line_without_comma\n");
assertThrows(
IllegalArgumentException.class,
() ->
runCommandForced(
"--name=" + TLD_TEST, "--input=" + malformedFile.getPath(), "--currency=USD"));
assertThat(PremiumListDao.getLatestRevision(TLD_TEST)).isEmpty();
}
@Test
void commandRun_failureCurrencyMismatchInInputFile() throws Exception {
File mismatchedFile = tmpDir.resolve(TLD_TEST + "_jpy.txt").toFile();
Files.asCharSink(mismatchedFile, UTF_8).write("doge,JPY 9090\n");
IllegalArgumentException thrown =
assertThrows(
IllegalArgumentException.class,
() ->
runCommandForced(
"--name=" + TLD_TEST, "--input=" + mismatchedFile.getPath(), "--currency=USD"));
assertThat(thrown).hasMessageThat().isEqualTo("The currency unit must be USD");
assertThat(PremiumListDao.getLatestRevision(TLD_TEST)).isEmpty();
}
@Test
void commandPrompt_failurePremiumListAlreadyExists() {
String randomStr = "random";
@@ -126,4 +187,32 @@ class CreatePremiumListCommandTest<C extends CreatePremiumListCommand>
"--name=" + TLD_TEST, "--input=" + premiumTermsPath, "--currency=USD", "--dry_run");
assertThat(PremiumListDao.getLatestRevision(TLD_TEST)).isEmpty();
}
@Test
void testFailure_runCommandOnProduction_noFlag() {
IllegalArgumentException thrown =
assertThrows(
IllegalArgumentException.class,
() ->
runCommandInEnvironment(
RegistryToolEnvironment.PRODUCTION,
"--name=" + TLD_TEST,
"--input=" + premiumTermsPath,
"--currency=USD"));
assertThat(thrown)
.hasMessageThat()
.isEqualTo("The --build_environment flag must be used when running in production");
}
@Test
void testSuccess_runCommandOnProduction_buildEnvFlag() throws Exception {
runCommandInEnvironment(
RegistryToolEnvironment.PRODUCTION,
"--name=" + TLD_TEST,
"--input=" + premiumTermsPath,
"--currency=USD",
"--build_environment",
"-f");
assertThat(PremiumListDao.getLatestRevision(TLD_TEST)).isPresent();
}
}
@@ -187,4 +187,30 @@ class CreateReservedListCommandTest
"--name=xn--q9jyb4c_common-reserved", "--input=" + reservedTermsPath, "--dry_run");
assertThat(ReservedList.get("xn--q9jyb4c_common-reserved")).isEmpty();
}
@Test
void testFailure_runCommandOnProduction_noFlag() {
IllegalArgumentException thrown =
assertThrows(
IllegalArgumentException.class,
() ->
runCommandInEnvironment(
RegistryToolEnvironment.PRODUCTION,
"--name=xn--q9jyb4c_common-reserved",
"--input=" + reservedTermsPath));
assertThat(thrown)
.hasMessageThat()
.isEqualTo("The --build_environment flag must be used when running in production");
}
@Test
void testSuccess_runCommandOnProduction_buildEnvFlag() throws Exception {
runCommandInEnvironment(
RegistryToolEnvironment.PRODUCTION,
"--name=xn--q9jyb4c_common-reserved",
"--input=" + reservedTermsPath,
"--build_environment",
"-f");
assertThat(ReservedList.get("xn--q9jyb4c_common-reserved")).isPresent();
}
}
@@ -18,6 +18,7 @@ import static com.google.common.collect.ImmutableList.toImmutableList;
import static com.google.common.truth.Truth.assertThat;
import static google.registry.model.ImmutableObjectSubject.immutableObjectCorrespondence;
import static java.nio.charset.StandardCharsets.UTF_8;
import static org.joda.money.CurrencyUnit.JPY;
import static org.joda.money.CurrencyUnit.USD;
import static org.junit.jupiter.api.Assertions.assertThrows;
@@ -31,6 +32,7 @@ import java.math.BigDecimal;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Optional;
import org.joda.money.Money;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
@@ -147,6 +149,17 @@ class UpdatePremiumListCommandTest<C extends UpdatePremiumListCommand>
assertThat(thrown).hasMessageThat().isEqualTo("New premium list data cannot be empty");
}
@Test
void commandPrompt_failureUpdateCommentOnlyList() throws Exception {
Path tmpPath = tmpDir.resolve(String.format("%s_comments.txt", TLD_TEST));
Files.asCharSink(tmpPath.toFile(), UTF_8).write("# only a comment\n \n");
command.inputFile = tmpPath;
command.name = TLD_TEST;
IllegalArgumentException thrown = assertThrows(IllegalArgumentException.class, command::prompt);
assertThat(thrown).hasMessageThat().isEqualTo("New premium list data cannot be empty");
}
@Test
void commandPrompt_failureNoPreviousVersion() {
registry = createTld("random", null, null);
@@ -227,4 +240,151 @@ class UpdatePremiumListCommandTest<C extends UpdatePremiumListCommand>
.comparingElementsUsing(immutableObjectCorrespondence("revisionId"))
.containsExactly(PremiumEntry.create(0L, new BigDecimal("9999.00"), "eth"));
}
@Test
void commandRun_failureMalformedInputFile() throws Exception {
File malformedFile = tmpDir.resolve(TLD_TEST + "_malformed.txt").toFile();
Files.asCharSink(malformedFile, UTF_8).write("malformed_line_without_comma\n");
assertThrows(
IllegalArgumentException.class,
() -> runCommandForced("--name=" + TLD_TEST, "--input=" + malformedFile.getPath()));
}
@Test
void commandRun_failureCurrencyMismatchInInputFile() throws Exception {
File mismatchedFile = tmpDir.resolve(TLD_TEST + "_jpy.txt").toFile();
Files.asCharSink(mismatchedFile, UTF_8).write("doge,JPY 9090\n");
IllegalArgumentException thrown =
assertThrows(
IllegalArgumentException.class,
() -> runCommandForced("--name=" + TLD_TEST, "--input=" + mismatchedFile.getPath()));
assertThat(thrown).hasMessageThat().isEqualTo("The currency unit must be USD");
}
@Test
void commandPrompt_successCreateListWithUpsert() throws Exception {
File tmpFile = tmpDir.resolve("newlist.txt").toFile();
Files.asCharSink(tmpFile, UTF_8).write("omg,USD 1234");
command.inputFile = Paths.get(tmpFile.getPath());
command.name = "newlist";
command.upsert = true;
assertThat(command.prompt()).contains("Create new premium list for newlist?");
}
@Test
void commandRun_successCreateListWithUpsert_usd() throws Exception {
File tmpFile = tmpDir.resolve("newlist.txt").toFile();
Files.asCharSink(tmpFile, UTF_8).write("eth,USD 9999");
runCommandForced("--name=newlist", "--input=" + tmpFile.getPath(), "--upsert");
Optional<PremiumList> list = PremiumListDao.getLatestRevision("newlist");
assertThat(list).isPresent();
assertThat(list.get().getCurrency()).isEqualTo(USD);
assertThat(PremiumListDao.loadAllPremiumEntries("newlist"))
.comparingElementsUsing(immutableObjectCorrespondence("revisionId"))
.containsExactly(PremiumEntry.create(0L, new BigDecimal("9999.00"), "eth"));
}
@Test
void commandRun_successCreateListWithUpsertShortFlag_nonUsdAndComments() throws Exception {
File tmpFile = tmpDir.resolve("jpylist.txt").toFile();
String content =
"""
# Leading comment
# Indented comment
doge,JPY 9090 # inline comment
eth,JPY 100
""";
Files.asCharSink(tmpFile, UTF_8).write(content);
runCommandForced("-i=" + tmpFile.getPath(), "-u");
Optional<PremiumList> list = PremiumListDao.getLatestRevision("jpylist");
assertThat(list).isPresent();
assertThat(list.get().getCurrency()).isEqualTo(JPY);
assertThat(PremiumListDao.getPremiumPrice("jpylist", "doge"))
.hasValue(Money.ofMajor(JPY, 9090));
assertThat(PremiumListDao.getPremiumPrice("jpylist", "eth")).hasValue(Money.ofMajor(JPY, 100));
}
@Test
void commandRun_successUpdateExistingListWithUpsert() throws Exception {
File tmpFile = tmpDir.resolve(String.format("%s.txt", TLD_TEST)).toFile();
Files.asCharSink(tmpFile, UTF_8).write("eth,USD 9999");
runCommandForced("--name=" + TLD_TEST, "--input=" + tmpFile.getPath(), "--upsert");
Optional<PremiumList> list = PremiumListDao.getLatestRevision(TLD_TEST);
assertThat(list).isPresent();
assertThat(list.get().getCurrency()).isEqualTo(USD);
assertThat(PremiumListDao.loadAllPremiumEntries(TLD_TEST))
.comparingElementsUsing(immutableObjectCorrespondence("revisionId"))
.containsExactly(PremiumEntry.create(0L, new BigDecimal("9999.00"), "eth"));
}
@Test
void commandDryRun_upsertNewList_noChangesMade() throws Exception {
File tmpFile = tmpDir.resolve("dryrunlist.txt").toFile();
Files.asCharSink(tmpFile, UTF_8).write("eth,EUR 99.50");
runCommandForced("--name=dryrunlist", "--input=" + tmpFile.getPath(), "--upsert", "--dry_run");
assertThat(PremiumListDao.getLatestRevision("dryrunlist")).isEmpty();
}
@Test
void commandRun_failureUpsertEmptyFile() throws Exception {
File emptyFile = tmpDir.resolve("empty.txt").toFile();
Files.write(new byte[0], emptyFile);
IllegalArgumentException thrown =
assertThrows(
IllegalArgumentException.class,
() -> runCommandForced("--name=empty", "--input=" + emptyFile.getPath(), "--upsert"));
assertThat(thrown).hasMessageThat().isEqualTo("New premium list data cannot be empty");
assertThat(PremiumListDao.getLatestRevision("empty")).isEmpty();
}
@Test
void commandRun_failureUpsertCommentOnlyFile() throws Exception {
File commentFile = tmpDir.resolve("comments.txt").toFile();
Files.asCharSink(commentFile, UTF_8).write("# only comment\n \n");
assertThrows(
IllegalArgumentException.class,
() -> runCommandForced("--name=comments", "--input=" + commentFile.getPath(), "--upsert"));
assertThat(PremiumListDao.getLatestRevision("comments")).isEmpty();
}
@Test
void commandRun_failureUpsertMissingCurrencyToken() throws Exception {
File noCurrencyFile = tmpDir.resolve("nocurrency.txt").toFile();
Files.asCharSink(noCurrencyFile, UTF_8).write("doge,9090\n");
assertThrows(
IllegalArgumentException.class,
() ->
runCommandForced(
"--name=nocurrency", "--input=" + noCurrencyFile.getPath(), "--upsert"));
assertThat(PremiumListDao.getLatestRevision("nocurrency")).isEmpty();
}
@Test
void commandRun_failureUpsertMismatchedCurrenciesAcrossLines() throws Exception {
File mixedFile = tmpDir.resolve("mixed.txt").toFile();
Files.asCharSink(mixedFile, UTF_8).write("doge,EUR 9090\neth,USD 100\n");
IllegalArgumentException thrown =
assertThrows(
IllegalArgumentException.class,
() -> runCommandForced("--name=mixed", "--input=" + mixedFile.getPath(), "--upsert"));
assertThat(thrown).hasMessageThat().isEqualTo("The currency unit must be EUR");
assertThat(PremiumListDao.getLatestRevision("mixed")).isEmpty();
}
@Test
void commandRun_failureUpsertInvalidPriceLine() throws Exception {
File invalidFile = tmpDir.resolve("invalidprice.txt").toFile();
Files.asCharSink(invalidFile, UTF_8).write("doge,USD not_a_price\n");
assertThrows(
IllegalArgumentException.class,
() ->
runCommandForced(
"--name=invalidprice", "--input=" + invalidFile.getPath(), "--upsert"));
assertThat(PremiumListDao.getLatestRevision("invalidprice")).isEmpty();
}
}
@@ -18,6 +18,7 @@ import static com.google.common.truth.Truth.assertThat;
import static google.registry.model.tld.label.ReservationType.FULLY_BLOCKED;
import static google.registry.testing.DatabaseHelper.persistReservedList;
import static google.registry.testing.TestDataHelper.loadFile;
import static google.registry.tools.CreateOrUpdateReservedListCommand.INVALID_FORMAT_ERROR_MESSAGE;
import static google.registry.util.DateTimeUtils.START_INSTANT;
import static java.nio.charset.StandardCharsets.UTF_8;
import static org.junit.jupiter.api.Assertions.assertThrows;
@@ -25,6 +26,7 @@ import static org.junit.jupiter.api.Assertions.assertThrows;
import com.google.common.collect.ImmutableList;
import com.google.common.io.Files;
import google.registry.model.tld.label.ReservedList;
import google.registry.model.tld.label.ReservedListDao;
import java.io.File;
import java.nio.file.Paths;
import org.junit.jupiter.api.BeforeEach;
@@ -161,4 +163,81 @@ class UpdateReservedListCommandTest
assertInStdout("baddies: null -> baddies,FULLY_BLOCKED");
assertInStdout("ford: null -> ford,FULLY_BLOCKED # random comment");
}
@Test
void testSuccess_upsertCreatesNonExistentReservedList() throws Exception {
runCommandForced("--name=xn--q9jyb4c_new-reserved", "--input=" + reservedTermsPath, "--upsert");
assertThat(ReservedList.get("xn--q9jyb4c_new-reserved")).isPresent();
assertThat(ReservedListDao.getLatestRevision("xn--q9jyb4c_new-reserved")).isPresent();
ReservedList reservedList = ReservedList.get("xn--q9jyb4c_new-reserved").get();
assertThat(reservedList.getReservedListEntries()).hasSize(2);
assertThat(reservedList.getReservationInList("baddies")).hasValue(FULLY_BLOCKED);
assertThat(reservedList.getReservationInList("ford")).hasValue(FULLY_BLOCKED);
}
@Test
void testSuccess_upsertShortFlag_defaultsToFileName() throws Exception {
File newReservedFile = tmpDir.resolve("soy_new-reserved.txt").toFile();
String reservedTermsCsv =
loadFile(CreateOrUpdateReservedListCommandTestCase.class, "example_reserved_terms.csv");
Files.asCharSink(newReservedFile, UTF_8).write(reservedTermsCsv);
runCommandForced("-i=" + newReservedFile.getPath(), "-u");
assertThat(ReservedList.get("soy_new-reserved")).isPresent();
ReservedList reservedList = ReservedList.get("soy_new-reserved").get();
assertThat(reservedList.getReservedListEntries()).hasSize(2);
assertThat(reservedList.getReservationInList("baddies")).hasValue(FULLY_BLOCKED);
assertThat(reservedList.getReservationInList("ford")).hasValue(FULLY_BLOCKED);
}
@Test
void testSuccess_upsertUpdatesExistingReservedList() throws Exception {
runSuccessfulUpdateTest(
"--name=xn--q9jyb4c_common-reserved", "--input=" + reservedTermsPath, "--upsert");
}
@Test
void testSuccess_upsertDryRun_doesNotCreateList() throws Exception {
runCommandForced(
"--name=xn--q9jyb4c_dryrun-reserved",
"--input=" + reservedTermsPath,
"--upsert",
"--dry_run");
assertThat(ReservedList.get("xn--q9jyb4c_dryrun-reserved")).isEmpty();
}
@Test
void testFailure_upsertInvalidSyntax_throwsException() throws Exception {
File badFile = tmpDir.resolve("xn--q9jyb4c_bad-reserved.txt").toFile();
Files.asCharSink(badFile, UTF_8).write("baddies,INVALID_TYPE\n");
assertThrows(
IllegalArgumentException.class,
() ->
runCommandForced(
"--name=xn--q9jyb4c_bad-reserved", "--input=" + badFile.getPath(), "--upsert"));
assertThat(ReservedList.get("xn--q9jyb4c_bad-reserved")).isEmpty();
}
@Test
void testSuccess_upsertCreatesReservedListForNonExistentTld() throws Exception {
runCommandForced("--name=newtld_reserved-list", "--input=" + reservedTermsPath, "--upsert");
assertThat(ReservedList.get("newtld_reserved-list")).isPresent();
}
@Test
void testFailure_upsertInvalidListNameFormat() {
for (String invalidName :
ImmutableList.of(
"random-reserved-list", "_missing-tld", "soy_", "soy_too_many", "soy_$weird")) {
command.upsert = false;
IllegalArgumentException thrown =
assertThrows(
IllegalArgumentException.class,
() ->
runCommandForced(
"--name=" + invalidName, "--input=" + reservedTermsPath, "--upsert"));
assertThat(thrown).hasMessageThat().isEqualTo(INVALID_FORMAT_ERROR_MESSAGE);
assertThat(ReservedList.get(invalidName)).isEmpty();
}
}
}