1
0
mirror of https://github.com/google/nomulus synced 2026-07-10 01:56:49 +00:00

Compare commits

...

4 Commits

Author SHA1 Message Date
sarahcaseybot 1248c25041 Add a configureTld command that uses YAML files for configuration (#2117)
* Add a configureTld command that uses YAML

* Add more tests and edge case handling

* Add out of order test and fix wrong inject

* small changes

* Add check for ascii

* Add check for ROID suffix
2023-09-06 16:17:22 -04:00
Pavlo Tkach 001e9363a1 Add billing details (#2136)
This adds functionality to billing details widget on home screen
2023-09-06 14:37:58 -04:00
Weimin Yu 9a6a7116da Disable Hibernate error logging (#2134)
Hibernate logs certain information at the ERROR level, which for the
purpose of troubleshooting is misleading, since most affected operations
succeed after retry. ERROR-level logging should only be added by Nomulus
code.

This PR does two things:
1. Disable all logging in two Hibernate classes: we cannot disable
   logging at a finer granularity, and we cannot preserve lower-level
   logging while disabling ERROR.
2. Adds a DatabaseException which captures all error details that may
   escape the typical loggers' attention: SQLException instances can be
   chained in a different way from Throwable's `getCause()` method.
2023-09-06 13:25:21 -04:00
Pavlo Tkach 335af52112 Allow homepage widgets to rearrange on mobile (#2135) 2023-09-05 20:45:13 -04:00
33 changed files with 1605 additions and 23 deletions
@@ -20,6 +20,7 @@
display: grid;
grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
grid-gap: 20px;
grid-auto-flow: dense;
mat-card {
height: 100%;
@@ -1,13 +1,22 @@
<mat-card>
<mat-card-content>
<div class="console-app__widget">
<div class="console-app__widget_left">
<a
class="console-app__widget_left"
href="{{ driveFolderUrl }}"
(click)="openBillingDetails($event)"
>
<mat-icon class="console-app__widget-icon">account_balance</mat-icon>
<h1 class="console-app__widget-title">Billing Info</h1>
<h4 class="secondary-text text-center">
View important billing and payments information.
<span *ngIf="driveFolderUrl; else noDriveFolderUrl">
View important billing and payments information.
</span>
<ng-template #noDriveFolderUrl>
<span> Your billing folder is pending allocation. </span>
</ng-template>
</h4>
</div>
</a>
</div>
</mat-card-content>
</mat-card>
@@ -13,11 +13,25 @@
// limitations under the License.
import { Component } from '@angular/core';
import { RegistrarService } from 'src/app/registrar/registrar.service';
@Component({
selector: '[app-billing-widget]',
templateUrl: './billing-widget.component.html',
})
export class BillingWidgetComponent {
constructor() {}
constructor(public registrarService: RegistrarService) {}
public get driveFolderUrl(): string {
if (this.registrarService?.registrar.driveFolderId) {
return `https://drive.google.com/drive/folders/${this.registrarService?.registrar.driveFolderId}`;
}
return '';
}
openBillingDetails(e: MouseEvent) {
if (!this.driveFolderUrl) {
e.preventDefault();
}
}
}
@@ -66,6 +66,12 @@ export class RegistrarService implements GlobalLoader {
this.activeRegistrarIdChange.next(registrarId);
}
public get registrar(): Registrar {
return this.registrars.filter(
(r) => r.registrarId === this.activeRegistrarId
)[0];
}
loadingTimeout() {
// TODO: Decide what to do when timeout happens
}
+7
View File
@@ -31,6 +31,13 @@ body {
&__widget {
display: flex;
gap: 10px;
> a {
text-decoration: none;
color: initial;
}
> a[href=""] {
cursor: not-allowed;
}
&-wrapper__wide {
grid-column: span 2;
}
@@ -11,3 +11,7 @@
# Set the default logging level for all loggers to INFO.
.level = INFO
# Turn off logging in Hibernate classes for misleading ERROR-level logs
org.hibernate.engine.jdbc.batch.internal.BatchingBatch.level=OFF
org.hibernate.engine.jdbc.spi.SqlExceptionHelper.level=OFF
@@ -11,3 +11,7 @@
# Set the default logging level for all loggers to INFO.
.level = INFO
# Turn off logging in Hibernate classes for misleading ERROR-level logs
org.hibernate.engine.jdbc.batch.internal.BatchingBatch.level=OFF
org.hibernate.engine.jdbc.spi.SqlExceptionHelper.level=OFF
@@ -11,3 +11,7 @@
# Set the default logging level for all loggers to INFO.
.level = INFO
# Turn off logging in Hibernate classes for misleading ERROR-level logs
org.hibernate.engine.jdbc.batch.internal.BatchingBatch.level=OFF
org.hibernate.engine.jdbc.spi.SqlExceptionHelper.level=OFF
@@ -11,3 +11,7 @@
# Set the default logging level for all loggers to INFO.
.level = INFO
# Turn off logging in Hibernate classes for misleading ERROR-level logs
org.hibernate.engine.jdbc.batch.internal.BatchingBatch.level=OFF
org.hibernate.engine.jdbc.spi.SqlExceptionHelper.level=OFF
@@ -980,7 +980,7 @@ public class Tld extends ImmutableObject implements Buildable, UnsafeSerializabl
return this;
}
private static final Pattern ROID_SUFFIX_PATTERN = Pattern.compile("^[A-Z\\d_]{1,8}$");
public static final Pattern ROID_SUFFIX_PATTERN = Pattern.compile("^[A-Z\\d_]{1,8}$");
public Builder setRoidSuffix(String roidSuffix) {
checkArgument(
@@ -0,0 +1,112 @@
// Copyright 2023 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.persistence.transaction;
import autovalue.shaded.com.google.common.collect.ImmutableList;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Joiner;
import java.sql.SQLException;
import java.util.Optional;
import javax.persistence.PersistenceException;
/**
* Wraps an exception thrown by the JPA framework and captures the SQL error details (state and
* status code) in the {@link #getMessage message}.
*
* <p>The {@link SQLException} class has its own chain of exceptions that describe multiple error
* conditions encontered during a transaction. A typical logger relying on the {@link
* Throwable#getCause() chain of causes} in {@code Throwable} instances cannot capture all details
* of errors thrown from the database drivers. This exception captures all error details in its
* message text.
*
* <p>The {@link TransactionManager} wraps every persistence exception in an instance of this class.
* This allows us to disable logging in specific Hibernate classes that logs at {@code ERROR}-level
* and rethrows. These {@code ERROR} logs are misleading, since the impacted transactions often
* succeeds on retries.
*
* <p>See the {@code logging.properties} files in the {@code env} package for the specific Hibernate
* classes that have logging suppressed.
*/
class DatabaseException extends PersistenceException {
private transient String cachedMessage;
@VisibleForTesting
DatabaseException(Throwable throwable) {
super(throwable);
}
@Override
public String getMessage() {
if (cachedMessage == null) {
cachedMessage = getSqlError(getCause());
}
return cachedMessage;
}
/**
* Throws an unchecked exception on behalf of the {@code original} {@link Throwable}.
*
* <p>If the {@code original Throwable} has at least one {@link SQLException} in its chain of
* causes, a {@link DatabaseException} is thrown; otherwise this does nothing.
*/
static void tryWrapAndThrow(Throwable original) {
Throwable t = original;
do {
if (t instanceof SQLException) {
throw new DatabaseException(original);
}
} while ((t = t.getCause()) != null);
}
/**
* Returns the SQL state and error code of every {@link SQLException} in the chain of causes, or
* empty string if such information is not found.
*/
static String getSqlError(Throwable t) {
ImmutableList.Builder<String> errMessageBuilder = new ImmutableList.Builder<>();
do {
if (t instanceof SQLException) {
SQLException e = (SQLException) t;
getSqlExceptionDetails(e).ifPresent(errMessageBuilder::add);
}
t = t.getCause();
} while (t != null);
return Joiner.on("\n").join(errMessageBuilder.build());
}
static Optional<String> getSqlExceptionDetails(SQLException sqlException) {
ImmutableList.Builder<String> errBuilder = new ImmutableList.Builder<>();
while (sqlException != null) {
if (sqlException.getErrorCode() > 0 || sqlException.getSQLState() != null) {
errBuilder.add(
"\tSQL Error: "
+ sqlException.getErrorCode()
+ ", SQLState: "
+ sqlException.getSQLState()
+ ", message: "
+ sqlException.getMessage()
+ ".");
}
sqlException = sqlException.getNextException();
}
ImmutableList<String> errors = errBuilder.build();
if (errors.isEmpty()) {
return Optional.empty();
}
return Optional.of(Joiner.on("\n").join(errors));
}
}
@@ -19,6 +19,7 @@ import static com.google.common.collect.ImmutableList.toImmutableList;
import static com.google.common.collect.ImmutableMap.toImmutableMap;
import static com.google.common.collect.ImmutableSet.toImmutableSet;
import static google.registry.config.RegistryConfig.getHibernatePerTransactionIsolationEnabled;
import static google.registry.persistence.transaction.DatabaseException.tryWrapAndThrow;
import static google.registry.util.PreconditionsUtils.checkArgumentNotNull;
import static java.util.AbstractMap.SimpleEntry;
import static java.util.stream.Collectors.joining;
@@ -213,6 +214,7 @@ public class JpaTransactionManagerImpl implements JpaTransactionManager {
} catch (Throwable rollbackException) {
logger.atSevere().withCause(rollbackException).log("Rollback failed; suppressing error.");
}
tryWrapAndThrow(e);
throw e;
} finally {
txnInfo.clear();
@@ -0,0 +1,172 @@
// Copyright 2023 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;
import static com.google.common.base.Preconditions.checkArgument;
import static google.registry.model.tld.Tld.Builder.ROID_SUFFIX_PATTERN;
import static google.registry.model.tld.Tlds.getTlds;
import static google.registry.util.ListNamingUtils.convertFilePathToName;
import com.beust.jcommander.Parameter;
import com.beust.jcommander.Parameters;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.common.base.CharMatcher;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.ImmutableSortedMap;
import com.google.common.collect.Sets;
import com.google.common.collect.Sets.SetView;
import google.registry.model.tld.Tld;
import google.registry.model.tld.label.PremiumList;
import google.registry.model.tld.label.PremiumListDao;
import google.registry.tools.params.PathParameter;
import google.registry.util.Idn;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
import javax.inject.Inject;
import javax.inject.Named;
import org.joda.money.CurrencyUnit;
import org.joda.money.Money;
import org.joda.time.DateTime;
import org.yaml.snakeyaml.Yaml;
/** Command to create or update a {@link Tld} using a YAML file. */
@Parameters(separators = " =", commandDescription = "Create or update TLD using YAML")
public class ConfigureTldCommand extends MutatingCommand {
@Parameter(
names = {"-i", "--input"},
description = "Filename of TLD YAML file.",
validateWith = PathParameter.InputFile.class,
required = true)
Path inputFile;
@Inject ObjectMapper mapper;
@Inject
@Named("dnsWriterNames")
Set<String> validDnsWriterNames;
// TODO(sarahbot@): Add a breakglass setting to this tool to indicate when a TLD has been modified
// outside of source control
// TODO(sarahbot@): Add a check for diffs between passed in file and current TLD and exit if there
// is no diff. Treat nulls and empty sets as the same value.
@Override
protected void init() throws Exception {
String name = convertFilePathToName(inputFile);
Map<String, Object> tldData = new Yaml().load(Files.newBufferedReader(inputFile));
checkName(name, tldData);
checkForMissingFields(tldData);
Tld oldTld = getTlds().contains(name) ? Tld.get(name) : null;
Tld newTld = mapper.readValue(inputFile.toFile(), Tld.class);
checkPremiumList(newTld);
checkDnsWriters(newTld);
checkCurrency(newTld);
stageEntityChange(oldTld, newTld);
}
private void checkName(String name, Map<String, Object> tldData) {
checkArgument(CharMatcher.ascii().matchesAllOf(name), "A TLD name must be in plain ASCII");
checkArgument(!Character.isDigit(name.charAt(0)), "TLDs cannot begin with a number");
checkArgument(
tldData.get("tldStr").equals(name),
"The input file name must match the name of the TLD it represents");
checkArgument(
tldData.get("tldUnicode").equals(Idn.toUnicode(name)),
"The value for tldUnicode must equal the unicode representation of the TLD name");
checkArgument(
ROID_SUFFIX_PATTERN.matcher((CharSequence) tldData.get("roidSuffix")).matches(),
"ROID suffix must be in format %s",
ROID_SUFFIX_PATTERN.pattern());
}
private void checkForMissingFields(Map<String, Object> tldData) {
Set<String> tldFields =
Arrays.stream(Tld.class.getDeclaredFields())
.filter(field -> !Modifier.isStatic(field.getModifiers()))
.filter(field -> field.getAnnotation(JsonIgnore.class) == null)
.map(Field::getName)
.collect(Collectors.toSet());
Set<String> missingFields = new HashSet<>();
for (String field : tldFields) {
if (!tldData.containsKey(field)) {
missingFields.add(field);
}
}
checkArgument(
missingFields.isEmpty(),
"The input file is missing data for the following fields: %s",
missingFields);
}
private void checkPremiumList(Tld newTld) {
Optional<String> premiumListName = newTld.getPremiumListName();
if (!premiumListName.isPresent()) return;
Optional<PremiumList> premiumList = PremiumListDao.getLatestRevision(premiumListName.get());
checkArgument(
premiumList.isPresent(),
"The premium list with the name %s does not exist",
premiumListName.get());
checkArgument(
premiumList.get().getCurrency().equals(newTld.getCurrency()),
"The premium list must use the TLD's currency");
}
private void checkDnsWriters(Tld newTld) {
ImmutableSet<String> dnsWriters = newTld.getDnsWriters();
SetView<String> invalidDnsWriters = Sets.difference(dnsWriters, validDnsWriterNames);
checkArgument(
invalidDnsWriters.isEmpty(), "Invalid DNS writer name(s) specified: %s", invalidDnsWriters);
}
private void checkCurrency(Tld newTld) {
CurrencyUnit currencyUnit = newTld.getCurrency();
checkArgument(
currencyUnit.equals(newTld.getCreateBillingCost().getCurrencyUnit()),
"createBillingCost must use the same currency as the TLD");
checkArgument(
currencyUnit.equals(newTld.getRestoreBillingCost().getCurrencyUnit()),
"restoreBillingCost must use the same currency as the TLD");
checkArgument(
currencyUnit.equals(newTld.getServerStatusChangeBillingCost().getCurrencyUnit()),
"serverStatusChangeBillingCost must use the same currency as the TLD");
checkArgument(
currencyUnit.equals(newTld.getRegistryLockOrUnlockBillingCost().getCurrencyUnit()),
"registryLockOrUnlockBillingCost must use the same currency as the TLD");
ImmutableSortedMap<DateTime, Money> renewBillingCostTransitions =
newTld.getRenewBillingCostTransitions();
for (Money renewBillingCost : renewBillingCostTransitions.values()) {
checkArgument(
renewBillingCost.getCurrencyUnit().equals(currencyUnit),
"All Money values in the renewBillingCostTransitions map must use the TLD's currency"
+ " unit");
}
ImmutableSortedMap<DateTime, Money> eapFeeSchedule = newTld.getEapFeeScheduleAsMap();
for (Money eapFee : eapFeeSchedule.values()) {
checkArgument(
eapFee.getCurrencyUnit().equals(currencyUnit),
"All Money values in the eapFeeSchedule map must use the TLD's currency unit");
}
}
}
@@ -25,8 +25,8 @@ import com.google.common.base.Joiner;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.ImmutableSortedMap;
import com.google.common.collect.ImmutableSortedSet;
import com.google.common.collect.Sets;
import com.google.common.collect.Sets.SetView;
import google.registry.model.pricing.StaticPremiumListPricingEngine;
import google.registry.model.tld.Tld;
import google.registry.model.tld.Tld.TldState;
@@ -409,8 +409,7 @@ abstract class CreateOrUpdateTldCommand extends MutatingCommand {
if (dnsWriters != null) {
ImmutableSet<String> dnsWritersSet = ImmutableSet.copyOf(dnsWriters);
ImmutableSortedSet<String> invalidDnsWriters =
ImmutableSortedSet.copyOf(Sets.difference(dnsWritersSet, validDnsWriterNames));
SetView<String> invalidDnsWriters = Sets.difference(dnsWritersSet, validDnsWriterNames);
checkArgument(
invalidDnsWriters.isEmpty(),
"Invalid DNS writer name(s) specified: %s",
@@ -33,6 +33,7 @@ public final class RegistryTool {
.put("canonicalize_labels", CanonicalizeLabelsCommand.class)
.put("check_domain", CheckDomainCommand.class)
.put("check_domain_claims", CheckDomainClaimsCommand.class)
.put("configure_tld", ConfigureTldCommand.class)
.put("convert_idn", ConvertIdnCommand.class)
.put("count_domains", CountDomainsCommand.class)
.put("create_anchor_tenant", CreateAnchorTenantCommand.class)
@@ -90,6 +90,8 @@ interface RegistryToolComponent {
void inject(CheckDomainCommand command);
void inject(ConfigureTldCommand command);
void inject(CountDomainsCommand command);
void inject(CreateAnchorTenantCommand command);
@@ -0,0 +1,119 @@
// Copyright 2023 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.persistence.transaction;
import static com.google.common.truth.Truth.assertThat;
import static com.google.common.truth.Truth8.assertThat;
import static google.registry.persistence.transaction.DatabaseException.getSqlError;
import static google.registry.persistence.transaction.DatabaseException.getSqlExceptionDetails;
import static google.registry.persistence.transaction.DatabaseException.tryWrapAndThrow;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import java.sql.SQLException;
import org.junit.jupiter.api.Test;
/** Unit tests for {@link DatabaseException}. */
public class DatabaseExceptionTest {
@Test
void getSqlError_singleNonSqlException() {
assertThat(getSqlError(new Exception())).isEmpty();
}
@Test
void getSqlExceptionDetails_chainedNoSqlExceptions() {
assertThat(getSqlError(new Exception(new Exception()))).isEmpty();
}
@Test
void getSqlError_sqlExceptionNoDetails() {
assertThat(getSqlError(new java.sql.SQLException())).isEmpty();
}
@Test
void getSqlError_sqlExceptionWithSqlState() {
assertThat(getSqlError(new java.sql.SQLException("msg", "state")))
.contains("\tSQL Error: 0, SQLState: state, message: msg.");
}
@Test
void getSqlError_sqlExceptionWithAllDetails() {
assertThat(getSqlError(new java.sql.SQLException("msg", "state", 1)))
.contains("\tSQL Error: 1, SQLState: state, message: msg.");
}
@Test
void getSqlError_chainedSqlExceptionWithAllDetails() {
SQLException sqlException = new java.sql.SQLException("msg", "state", 1);
assertThat(getSqlError(new Exception("not-captured", sqlException)))
.contains("\tSQL Error: 1, SQLState: state, message: msg.");
}
@Test
void getSqlError_multipleChainedSqlExceptionWithAllDetails() {
SQLException lower = new java.sql.SQLException("lower", "lower-state", 1);
SQLException higher =
new java.sql.SQLException("higher", "higher-state", 2, new Exception(lower));
assertThat(getSqlError(new Exception(higher)))
.contains(
"\tSQL Error: 2, SQLState: higher-state, message: higher.\n"
+ "\tSQL Error: 1, SQLState: lower-state, message: lower.");
}
@Test
void getSqlExceptionDetails_singleNoDetails() {
assertThat(getSqlExceptionDetails(new SQLException())).isEmpty();
}
@Test
void getSqlExceptionDetails_singleWithDetails() {
assertThat(getSqlExceptionDetails(new SQLException("msg", "state", 1)))
.hasValue("\tSQL Error: 1, SQLState: state, message: msg.");
}
@Test
void getSqlExceptionDetails_multipleWithDetails() {
SQLException first = new SQLException("msg", "state", 1);
first.setNextException(new SQLException("msg2", "state2", 2));
assertThat(getSqlExceptionDetails(first))
.hasValue(
"\tSQL Error: 1, SQLState: state, message: msg.\n"
+ "\tSQL Error: 2, SQLState: state2, message: msg2.");
}
@Test
void tryWrapAndThrow_notSQLException() {
RuntimeException orig = new RuntimeException(new Exception());
tryWrapAndThrow(orig);
}
@Test
void tryWrapAndThrow_hasSQLException() {
Throwable orig = new Throwable(new SQLException());
assertThrows(DatabaseException.class, () -> tryWrapAndThrow(orig));
}
@Test
void getMessage_cachedMessageReused() {
SQLException sqlException = mock(SQLException.class);
DatabaseException databaseException = new DatabaseException(sqlException);
databaseException.getMessage();
databaseException.getMessage();
verify(sqlException, times(1)).getCause();
}
}
@@ -0,0 +1,160 @@
// Copyright 2023 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.persistence.transaction;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.truth.Truth.assertThat;
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
import static java.nio.charset.StandardCharsets.UTF_8;
import static java.util.logging.Logger.getLogger;
import com.google.common.testing.TestLogHandler;
import google.registry.persistence.transaction.JpaTestExtensions.JpaUnitTestExtension;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.util.logging.Level;
import java.util.logging.LogManager;
import java.util.logging.Logger;
import javax.persistence.Entity;
import javax.persistence.Id;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;
/**
* Verifies that specific {@code ERROR}-level logging by Hibernate does not appear in the log files.
*
* <p>Please refer to the class javadoc of {@link DatabaseException} for more information.
*/
public class HibernateLoggingSuppressionTest {
private static final String LOG_SUPPRESSION_TARGET =
"org.hibernate.engine.jdbc.spi.SqlExceptionHelper";
// The line that should be added to the `logging.properties` file.
private static final String LOGGING_PROPERTIES_LINE = LOG_SUPPRESSION_TARGET + ".level=OFF\n";
@RegisterExtension
public final JpaUnitTestExtension jpa =
new JpaTestExtensions.Builder()
.withoutCannedData()
.withEntityClass(TestEntity.class)
.buildUnitTestExtension();
private Logger logger;
private TestLogHandler testLogHandler;
@BeforeEach
void setup() {
testLogHandler = new TestLogHandler();
logger = getLogger(LOG_SUPPRESSION_TARGET);
logger.addHandler(testLogHandler);
}
@AfterEach
void teardown() {
logger.removeHandler(testLogHandler);
}
// Updates logging configs to suppress logging. Call `revertSuppressionOfHibernateLogs` to revert.
void suppressHibernateLogs() throws IOException {
try (ByteArrayInputStream additionalProperties =
new ByteArrayInputStream(LOGGING_PROPERTIES_LINE.getBytes(UTF_8))) {
LogManager.getLogManager()
.updateConfiguration(
additionalProperties,
x ->
(o, n) -> {
if (!x.startsWith(LOG_SUPPRESSION_TARGET)) {
return o;
}
checkArgument(o == null, "Cannot override old value in this test");
return n;
});
}
}
void revertSuppressionOfHibernateLogs() throws IOException {
try (ByteArrayInputStream additionalProperties =
new ByteArrayInputStream(LOGGING_PROPERTIES_LINE.getBytes(UTF_8))) {
LogManager.getLogManager()
.updateConfiguration(
additionalProperties,
x ->
(o, n) -> {
if (!x.startsWith(LOG_SUPPRESSION_TARGET)) {
return o;
}
return null;
});
}
}
@Test
void noLoggingSuppression() {
try {
tm().transact(() -> tm().insert(new TestEntity(1, 1)));
tm().transact(() -> tm().insert(new TestEntity(1, 1)));
} catch (DatabaseException e) {
assertThat(e).hasMessageThat().contains("SQLState: ");
}
assertThat(
testLogHandler.getStoredLogRecords().stream()
.anyMatch(
logRecord ->
logRecord.getLevel().equals(Level.SEVERE)
&& logRecord.getMessage().contains("duplicate key")))
.isTrue();
}
@Test
void withLoggingSuppression() throws Exception {
suppressHibernateLogs();
try {
tm().transact(() -> tm().insert(new TestEntity(1, 1)));
tm().transact(() -> tm().insert(new TestEntity(1, 1)));
} catch (DatabaseException e) {
assertThat(e).hasMessageThat().contains("SQLState: ");
}
assertThat(
testLogHandler.getStoredLogRecords().stream()
.anyMatch(
logRecord ->
logRecord.getLevel().equals(Level.SEVERE)
&& logRecord.getMessage().contains("duplicate key")))
.isFalse();
revertSuppressionOfHibernateLogs();
}
@Entity
static class TestEntity {
@Id long id;
int value;
// For Hibernate
TestEntity() {}
TestEntity(long id, int value) {
this.id = id;
this.value = value;
}
TestEntity incValue() {
this.value++;
return this;
}
}
}
@@ -64,8 +64,9 @@ public class JpaTransactionManagerExtensionTest {
TestEntity testEntity = new TestEntity("foo", "bar");
assertThat(
assertThrows(
PersistenceException.class,
() -> replicaTm().transact(() -> replicaTm().put(testEntity))))
PersistenceException.class,
() -> replicaTm().transact(() -> replicaTm().put(testEntity)))
.getCause())
.hasMessageThat()
.isEqualTo("Error while committing the transaction");
}
@@ -49,6 +49,7 @@ import javax.persistence.EntityManager;
import javax.persistence.Id;
import javax.persistence.IdClass;
import javax.persistence.OptimisticLockException;
import javax.persistence.PersistenceException;
import javax.persistence.RollbackException;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;
@@ -353,7 +354,11 @@ class JpaTransactionManagerImplTest {
tm().transact(() -> tm().insert(theEntity));
assertThat(existsInDb(theEntity)).isTrue();
assertThat(loadByKey(theEntityKey)).isEqualTo(theEntity);
assertThrows(RollbackException.class, () -> tm().transact(() -> tm().insert(theEntity)));
assertThat(
assertThrows(
PersistenceException.class, () -> tm().transact(() -> tm().insert(theEntity)))
.getCause())
.isInstanceOf(RollbackException.class);
}
@Test
@@ -389,7 +394,8 @@ class JpaTransactionManagerImplTest {
void saveAllNew_rollsBackWhenFailure() {
moreEntities.forEach(entity -> assertThat(tm().transact(() -> tm().exists(entity))).isFalse());
insertInDb(moreEntities.get(0));
assertThrows(RollbackException.class, () -> insertInDb(moreEntities));
assertThat(assertThrows(PersistenceException.class, () -> insertInDb(moreEntities)).getCause())
.isInstanceOf(RollbackException.class);
assertThat(tm().transact(() -> tm().exists(moreEntities.get(0)))).isTrue();
assertThat(tm().transact(() -> tm().exists(moreEntities.get(1)))).isFalse();
assertThat(tm().transact(() -> tm().exists(moreEntities.get(2)))).isFalse();
@@ -0,0 +1,448 @@
// Copyright 2023 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;
import static com.google.common.truth.Truth.assertThat;
import static google.registry.model.domain.token.AllocationToken.TokenType.DEFAULT_PROMO;
import static google.registry.testing.DatabaseHelper.createTld;
import static google.registry.testing.DatabaseHelper.persistPremiumList;
import static google.registry.testing.DatabaseHelper.persistResource;
import static google.registry.testing.TestDataHelper.loadFile;
import static google.registry.tldconfig.idn.IdnTableEnum.EXTENDED_LATIN;
import static google.registry.tldconfig.idn.IdnTableEnum.JA;
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;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.exc.InvalidFormatException;
import com.fasterxml.jackson.databind.exc.UnrecognizedPropertyException;
import com.google.common.base.Ascii;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.common.io.Files;
import google.registry.model.EntityYamlUtils;
import google.registry.model.domain.token.AllocationToken;
import google.registry.model.tld.Tld;
import google.registry.model.tld.label.PremiumList;
import google.registry.model.tld.label.PremiumListDao;
import java.io.File;
import org.joda.money.Money;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.testcontainers.shaded.com.google.common.collect.ImmutableMap;
/** Unit tests for {@link ConfigureTldCommand} */
public class ConfigureTldCommandTest extends CommandTestCase<ConfigureTldCommand> {
PremiumList premiumList;
ObjectMapper objectMapper = EntityYamlUtils.createObjectMapper();
@BeforeEach
void beforeEach() {
command.mapper = objectMapper;
premiumList = persistPremiumList("test", USD, "silver,USD 50", "gold,USD 80");
command.validDnsWriterNames = ImmutableSet.of("VoidDnsWriter", "FooDnsWriter");
}
private void testTldConfiguredSuccessfully(Tld tld, String filename)
throws JsonProcessingException {
String yaml = objectMapper.writeValueAsString(tld);
assertThat(yaml).isEqualTo(loadFile(getClass(), filename));
}
@Test
void testSuccess_createNewTld() throws Exception {
File tldFile = tmpDir.resolve("tld.yaml").toFile();
Files.asCharSink(tldFile, UTF_8).write(loadFile(getClass(), "tld.yaml"));
runCommandForced("--input=" + tldFile);
Tld tld = Tld.get("tld");
assertThat(tld).isNotNull();
assertThat(tld.getDriveFolderId()).isEqualTo("driveFolder");
assertThat(tld.getCreateBillingCost()).isEqualTo(Money.of(USD, 25));
testTldConfiguredSuccessfully(tld, "tld.yaml");
}
@Test
void testSuccess_updateTld() throws Exception {
Tld tld = createTld("tld");
assertThat(tld.getCreateBillingCost()).isEqualTo(Money.of(USD, 13));
File tldFile = tmpDir.resolve("tld.yaml").toFile();
Files.asCharSink(tldFile, UTF_8).write(loadFile(getClass(), "tld.yaml"));
runCommandForced("--input=" + tldFile);
Tld updatedTld = Tld.get("tld");
assertThat(updatedTld.getCreateBillingCost()).isEqualTo(Money.of(USD, 25));
testTldConfiguredSuccessfully(updatedTld, "tld.yaml");
}
@Test
void testSuccess_outOfOrderFieldsOnCreate() throws Exception {
File tldFile = tmpDir.resolve("outoforderfields.yaml").toFile();
Files.asCharSink(tldFile, UTF_8).write(loadFile(getClass(), "outoforderfields.yaml"));
runCommandForced("--input=" + tldFile);
Tld tld = Tld.get("outoforderfields");
// Cannot test that created TLD converted to YAML is equal to original YAML since the created
// TLD's YAML will contain the fields in the correct order
assertThat(tld).isNotNull();
assertThat(tld.getDriveFolderId()).isEqualTo("driveFolder");
assertThat(tld.getCreateBillingCost()).isEqualTo(Money.of(USD, 25));
assertThat(tld.getPremiumListName().get()).isEqualTo("test");
}
@Test
void testSuccess_outOfOrderFieldsOnUpdate() throws Exception {
Tld tld = createTld("outoforderfields");
assertThat(tld.getCreateBillingCost()).isEqualTo(Money.of(USD, 13));
File tldFile = tmpDir.resolve("outoforderfields.yaml").toFile();
Files.asCharSink(tldFile, UTF_8).write(loadFile(getClass(), "outoforderfields.yaml"));
runCommandForced("--input=" + tldFile);
Tld updatedTld = Tld.get("outoforderfields");
// Cannot test that created TLD converted to YAML is equal to original YAML since the created
// TLD's YAML will contain the fields in the correct order
assertThat(updatedTld.getCreateBillingCost()).isEqualTo(Money.of(USD, 25));
}
@Test
void testFailure_fileMissingNullableFieldsOnCreate() throws Exception {
File tldFile = tmpDir.resolve("missingnullablefields.yaml").toFile();
Files.asCharSink(tldFile, UTF_8).write(loadFile(getClass(), "missingnullablefields.yaml"));
IllegalArgumentException thrown =
assertThrows(IllegalArgumentException.class, () -> runCommandForced("--input=" + tldFile));
assertThat(thrown.getMessage())
.isEqualTo(
"The input file is missing data for the following fields: [tldStateTransitions,"
+ " premiumListName, currency, numDnsPublishLocks]");
}
@Test
void testFailure_fileMissingNullableFieldOnUpdate() throws Exception {
Tld tld = createTld("missingnullablefields");
persistResource(
tld.asBuilder().setNumDnsPublishLocks(5).build()); // numDnsPublishLocks is nullable
File tldFile = tmpDir.resolve("missingnullablefields.yaml").toFile();
Files.asCharSink(tldFile, UTF_8)
.write(
loadFile(
getClass(), "missingnullablefields.yaml")); // file is missing numDnsPublishLocks
IllegalArgumentException thrown =
assertThrows(IllegalArgumentException.class, () -> runCommandForced("--input=" + tldFile));
assertThat(thrown.getMessage())
.isEqualTo(
"The input file is missing data for the following fields: [tldStateTransitions,"
+ " premiumListName, currency, numDnsPublishLocks]");
}
@Test
void testSuccess_nullableFieldsAllNullOnCreate() throws Exception {
File tldFile = tmpDir.resolve("nullablefieldsallnull.yaml").toFile();
Files.asCharSink(tldFile, UTF_8).write(loadFile(getClass(), "nullablefieldsallnull.yaml"));
runCommandForced("--input=" + tldFile);
Tld tld = Tld.get("nullablefieldsallnull");
assertThat(tld).isNotNull();
assertThat(tld.getDriveFolderId()).isEqualTo(null);
assertThat(tld.getCreateBillingCost()).isEqualTo(Money.of(USD, 25));
// cannot test that created TLD converted to YAML is equal to original YAML since the created
// TLD's YAML will contain empty sets for some of the null fields
assertThat(tld.getIdnTables()).isEmpty();
assertThat(tld.getDefaultPromoTokens()).isEmpty();
}
@Test
void testSuccess_nullableFieldsAllNullOnUpdate() throws Exception {
Tld tld = createTld("nullablefieldsallnull");
persistResource(
tld.asBuilder().setIdnTables(ImmutableSet.of(JA)).setDriveFolderId("drive").build());
File tldFile = tmpDir.resolve("nullablefieldsallnull.yaml").toFile();
Files.asCharSink(tldFile, UTF_8).write(loadFile(getClass(), "nullablefieldsallnull.yaml"));
runCommandForced("--input=" + tldFile);
Tld updatedTld = Tld.get("nullablefieldsallnull");
assertThat(updatedTld).isNotNull();
assertThat(updatedTld.getDriveFolderId()).isEqualTo(null);
assertThat(updatedTld.getCreateBillingCost()).isEqualTo(Money.of(USD, 25));
// cannot test that created TLD converted to YAML is equal to original YAML since the created
// TLD's YAML will contain empty sets for some of the null fields
assertThat(updatedTld.getIdnTables()).isEmpty();
assertThat(updatedTld.getDefaultPromoTokens()).isEmpty();
}
@Test
void testFailure_fileContainsExtraFields() throws Exception {
File tldFile = tmpDir.resolve("extrafield.yaml").toFile();
Files.asCharSink(tldFile, UTF_8).write(loadFile(getClass(), "extrafield.yaml"));
assertThrows(UnrecognizedPropertyException.class, () -> runCommandForced("--input=" + tldFile));
}
@Test
void testFailure_fileNameDoesNotMatchTldName() throws Exception {
File tldFile = tmpDir.resolve("othertld.yaml").toFile();
Files.asCharSink(tldFile, UTF_8).write(loadFile(getClass(), "tld.yaml"));
IllegalArgumentException thrown =
assertThrows(IllegalArgumentException.class, () -> runCommandForced("--input=" + tldFile));
assertThat(thrown.getMessage())
.isEqualTo("The input file name must match the name of the TLD it represents");
}
@Test
void testFailure_tldUnicodeDoesNotMatch() throws Exception {
File tldFile = tmpDir.resolve("badunicode.yaml").toFile();
Files.asCharSink(tldFile, UTF_8).write(loadFile(getClass(), "badunicode.yaml"));
IllegalArgumentException thrown =
assertThrows(IllegalArgumentException.class, () -> runCommandForced("--input=" + tldFile));
assertThat(thrown.getMessage())
.isEqualTo(
"The value for tldUnicode must equal the unicode representation of the TLD name");
}
@Test
void testFailure_tldUpperCase() throws Exception {
String name = "TLD";
File tldFile = tmpDir.resolve("TLD.yaml").toFile();
Files.asCharSink(tldFile, UTF_8)
.write(
loadFile(
getClass(),
"wildcard.yaml",
ImmutableMap.of(
"TLDSTR", name, "TLDUNICODE", name, "ROIDSUFFIX", Ascii.toUpperCase(name))));
IllegalArgumentException thrown =
assertThrows(IllegalArgumentException.class, () -> runCommandForced("--input=" + tldFile));
assertThat(thrown.getMessage())
.isEqualTo(
"The value for tldUnicode must equal the unicode representation of the TLD name");
}
@Test
void testSuccess_asciiNameWithOptionalPunycode() throws Exception {
String name = "xn--q9jyb4c";
File tldFile = tmpDir.resolve(name + ".yaml").toFile();
String fileContents =
loadFile(
getClass(),
"wildcard.yaml",
ImmutableMap.of(
"TLDSTR",
"\"" + name + "\"",
"TLDUNICODE",
"\"みんな\"",
"ROIDSUFFIX",
"\"Q9JYB4C\""));
Files.asCharSink(tldFile, UTF_8).write(fileContents);
runCommandForced("--input=" + tldFile);
Tld tld = Tld.get(name);
assertThat(tld).isNotNull();
assertThat(tld.getDriveFolderId()).isEqualTo("driveFolder");
assertThat(tld.getCreateBillingCost()).isEqualTo(Money.of(USD, 25));
String yaml = objectMapper.writeValueAsString(tld);
assertThat(yaml).isEqualTo(fileContents);
}
@Test
void testFailure_punycodeDoesNotMatch() throws Exception {
String name = "xn--q9jyb4c";
File tldFile = tmpDir.resolve(name + ".yaml").toFile();
String fileContents =
loadFile(
getClass(),
"wildcard.yaml",
ImmutableMap.of(
"TLDSTR",
"\"" + name + "\"",
"TLDUNICODE",
"\"yoyo\"",
"ROIDSUFFIX",
"\"Q9JYB4C\""));
Files.asCharSink(tldFile, UTF_8).write(fileContents);
IllegalArgumentException thrown =
assertThrows(IllegalArgumentException.class, () -> runCommandForced("--input=" + tldFile));
assertThat(thrown.getMessage())
.isEqualTo(
"The value for tldUnicode must equal the unicode representation of the TLD name");
}
@Test
void testFailure_punycodeName() throws Exception {
String name = "みんな";
File tldFile = tmpDir.resolve(name + ".yaml").toFile();
String fileContents =
loadFile(
getClass(),
"wildcard.yaml",
ImmutableMap.of(
"TLDSTR",
"\"" + name + "\"",
"TLDUNICODE",
"\"みんな\"",
"ROIDSUFFIX",
"\"Q9JYB4C\""));
Files.asCharSink(tldFile, UTF_8).write(fileContents);
IllegalArgumentException thrown =
assertThrows(IllegalArgumentException.class, () -> runCommandForced("--input=" + tldFile));
assertThat(thrown.getMessage()).isEqualTo("A TLD name must be in plain ASCII");
}
@Test
void testFailure_invalidRoidSuffix() throws Exception {
String name = "tld";
File tldFile = tmpDir.resolve("tld.yaml").toFile();
Files.asCharSink(tldFile, UTF_8)
.write(
loadFile(
getClass(),
"wildcard.yaml",
ImmutableMap.of(
"TLDSTR", name, "TLDUNICODE", name, "ROIDSUFFIX", "TLLLLLLLLLLLLLLLLLLLLLLD")));
IllegalArgumentException thrown =
assertThrows(IllegalArgumentException.class, () -> runCommandForced("--input=" + tldFile));
assertThat(thrown.getMessage()).isEqualTo("ROID suffix must be in format ^[A-Z\\d_]{1,8}$");
}
@Test
void testFailure_invalidIdnTable() throws Exception {
File tldFile = tmpDir.resolve("badidn.yaml").toFile();
Files.asCharSink(tldFile, UTF_8).write(loadFile(getClass(), "badidn.yaml"));
assertThrows(InvalidFormatException.class, () -> runCommandForced("--input=" + tldFile));
}
@Test
void testFailure_tldNameStartsWithNumber() throws Exception {
File tldFile = tmpDir.resolve("1tld.yaml").toFile();
Files.asCharSink(tldFile, UTF_8).write(loadFile(getClass(), "1tld.yaml"));
IllegalArgumentException thrown =
assertThrows(IllegalArgumentException.class, () -> runCommandForced("--input=" + tldFile));
assertThat(thrown.getMessage()).isEqualTo("TLDs cannot begin with a number");
}
@Test
void testFailure_invalidDnsWriter() throws Exception {
command.validDnsWriterNames = ImmutableSet.of("foo");
File tldFile = tmpDir.resolve("tld.yaml").toFile();
Files.asCharSink(tldFile, UTF_8).write(loadFile(getClass(), "tld.yaml"));
IllegalArgumentException thrown =
assertThrows(IllegalArgumentException.class, () -> runCommandForced("--input=" + tldFile));
assertThat(thrown.getMessage())
.isEqualTo("Invalid DNS writer name(s) specified: [VoidDnsWriter]");
}
@Test
void testFailure_mismatchedCurrencyUnitsOnCreate() throws Exception {
File tldFile = tmpDir.resolve("wrongcurrency.yaml").toFile();
Files.asCharSink(tldFile, UTF_8)
.write(
loadFile(
getClass(),
"wrongcurrency.yaml",
ImmutableMap.of("RESTORECURRENCY", "EUR", "RENEWCURRENCY", "USD")));
IllegalArgumentException thrown =
assertThrows(IllegalArgumentException.class, () -> runCommandForced("--input=" + tldFile));
assertThat(thrown.getMessage())
.isEqualTo("restoreBillingCost must use the same currency as the TLD");
File tldFile2 = tmpDir.resolve("wrongcurrency.yaml").toFile();
Files.asCharSink(tldFile2, UTF_8)
.write(
loadFile(
getClass(),
"wrongcurrency.yaml",
ImmutableMap.of("RESTORECURRENCY", "USD", "RENEWCURRENCY", "EUR")));
thrown =
assertThrows(IllegalArgumentException.class, () -> runCommandForced("--input=" + tldFile2));
assertThat(thrown.getMessage())
.isEqualTo(
"All Money values in the renewBillingCostTransitions map must use the TLD's currency"
+ " unit");
}
@Test
void testFailure_mismatchedCurrencyUnitsOnUpdate() throws Exception {
createTld("wrongcurreency");
File tldFile = tmpDir.resolve("wrongcurrency.yaml").toFile();
Files.asCharSink(tldFile, UTF_8)
.write(
loadFile(
getClass(),
"wrongcurrency.yaml",
ImmutableMap.of("RESTORECURRENCY", "EUR", "RENEWCURRENCY", "USD")));
IllegalArgumentException thrown =
assertThrows(IllegalArgumentException.class, () -> runCommandForced("--input=" + tldFile));
assertThat(thrown.getMessage())
.isEqualTo("restoreBillingCost must use the same currency as the TLD");
File tldFile2 = tmpDir.resolve("wrongcurrency.yaml").toFile();
Files.asCharSink(tldFile2, UTF_8)
.write(
loadFile(
getClass(),
"wrongcurrency.yaml",
ImmutableMap.of("RESTORECURRENCY", "USD", "RENEWCURRENCY", "EUR")));
thrown =
assertThrows(IllegalArgumentException.class, () -> runCommandForced("--input=" + tldFile2));
assertThat(thrown.getMessage())
.isEqualTo(
"All Money values in the renewBillingCostTransitions map must use the TLD's currency"
+ " unit");
}
@Test
void testSuccess_emptyStringClearsDefaultPromoTokens() throws Exception {
Tld tld = createTld("tld");
AllocationToken defaultToken =
persistResource(
new AllocationToken.Builder()
.setToken("bbbbb")
.setTokenType(DEFAULT_PROMO)
.setAllowedTlds(ImmutableSet.of("tld"))
.build());
persistResource(
tld.asBuilder().setDefaultPromoTokens(ImmutableList.of(defaultToken.createVKey())).build());
File tldFile = tmpDir.resolve("tld.yaml").toFile();
Files.asCharSink(tldFile, UTF_8).write(loadFile(getClass(), "tld.yaml"));
runCommandForced("--input=" + tldFile);
Tld updatedTld = Tld.get("tld");
assertThat(updatedTld.getDefaultPromoTokens()).isEmpty();
testTldConfiguredSuccessfully(updatedTld, "tld.yaml");
}
@Test
void testSuccess_emptyStringClearsIdnTables() throws Exception {
Tld tld = createTld("tld");
persistResource(tld.asBuilder().setIdnTables(ImmutableSet.of(EXTENDED_LATIN, JA)).build());
File tldFile = tmpDir.resolve("tld.yaml").toFile();
Files.asCharSink(tldFile, UTF_8).write(loadFile(getClass(), "tld.yaml"));
runCommandForced("--input=" + tldFile);
Tld updatedTld = Tld.get("tld");
assertThat(updatedTld.getIdnTables()).isEmpty();
testTldConfiguredSuccessfully(updatedTld, "tld.yaml");
}
@Test
void testFailure_premiumListDoesNotExist() throws Exception {
PremiumListDao.delete(premiumList);
File tldFile = tmpDir.resolve("tld.yaml").toFile();
Files.asCharSink(tldFile, UTF_8).write(loadFile(getClass(), "tld.yaml"));
IllegalArgumentException thrown =
assertThrows(IllegalArgumentException.class, () -> runCommandForced("--input=" + tldFile));
assertThat(thrown.getMessage()).isEqualTo("The premium list with the name test does not exist");
}
@Test
void testFailure_premiumListWrongCurrency() throws Exception {
PremiumListDao.delete(premiumList);
persistPremiumList("test", JPY, "bronze,JPY 80");
File tldFile = tmpDir.resolve("tld.yaml").toFile();
Files.asCharSink(tldFile, UTF_8).write(loadFile(getClass(), "tld.yaml"));
IllegalArgumentException thrown =
assertThrows(IllegalArgumentException.class, () -> runCommandForced("--input=" + tldFile));
assertThat(thrown.getMessage()).isEqualTo("The premium list must use the TLD's currency");
}
}
@@ -632,7 +632,7 @@ class CreateTldCommandTest extends CommandTestCase<CreateTldCommand> {
"xn--q9jyb4c", "--roid_suffix=Q9JYB4C", "--dns_writers=Invalid,Deadbeef"));
assertThat(thrown)
.hasMessageThat()
.contains("Invalid DNS writer name(s) specified: [Deadbeef, Invalid]");
.contains("Invalid DNS writer name(s) specified: [Invalid, Deadbeef]");
}
@Test
@@ -16,11 +16,18 @@ package google.registry.tools;
import static google.registry.testing.DatabaseHelper.createTld;
import static google.registry.testing.DatabaseHelper.createTlds;
import static google.registry.testing.DatabaseHelper.persistPremiumList;
import static google.registry.testing.DatabaseHelper.persistResource;
import static google.registry.testing.TestDataHelper.loadFile;
import static org.joda.money.CurrencyUnit.USD;
import static org.junit.jupiter.api.Assertions.assertThrows;
import com.beust.jcommander.ParameterException;
import google.registry.model.EntityYamlUtils;
import google.registry.model.tld.Tld;
import google.registry.model.tld.label.PremiumList;
import org.joda.money.Money;
import org.joda.time.Duration;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
@@ -34,8 +41,16 @@ class GetTldCommandTest extends CommandTestCase<GetTldCommand> {
@Test
void testSuccess() throws Exception {
createTld("xn--q9jyb4c");
runCommand("xn--q9jyb4c");
Tld tld = createTld("tld");
PremiumList premiumList = persistPremiumList("test", USD, "silver,USD 50", "gold,USD 80");
persistResource(
tld.asBuilder()
.setDnsAPlusAaaaTtl(Duration.millis(900))
.setDriveFolderId("driveFolder")
.setCreateBillingCost(Money.of(USD, 25))
.setPremiumList(premiumList)
.build());
runCommand("tld");
assertInStdout(loadFile(getClass(), "tld.yaml"));
}
@@ -0,0 +1,55 @@
addGracePeriodLength: 432000000
allowedFullyQualifiedHostNames: []
allowedRegistrantContactIds: []
anchorTenantAddGracePeriodLength: 2592000000
autoRenewGracePeriodLength: 3888000000
automaticTransferLength: 432000000
claimsPeriodEnd: "294247-01-10T04:00:54.775Z"
createBillingCost:
currency: "USD"
amount: 25.00
creationTime: "2022-09-01T00:00:00.000Z"
currency: "USD"
defaultPromoTokens: []
dnsAPlusAaaaTtl: 900
dnsDsTtl: null
dnsNsTtl: null
dnsPaused: false
dnsWriters:
- "VoidDnsWriter"
driveFolderId: "driveFolder"
eapFeeSchedule:
"1970-01-01T00:00:00.000Z":
currency: "USD"
amount: 0.00
escrowEnabled: false
idnTables: []
invoicingEnabled: false
lordnUsername: null
numDnsPublishLocks: 1
pendingDeleteLength: 432000000
premiumListName: "test"
pricingEngineClassName: "google.registry.model.pricing.StaticPremiumListPricingEngine"
redemptionGracePeriodLength: 2592000000
registryLockOrUnlockBillingCost:
currency: "USD"
amount: 0.00
renewBillingCostTransitions:
"1970-01-01T00:00:00.000Z":
currency: "USD"
amount: 11.00
renewGracePeriodLength: 432000000
reservedListNames: []
restoreBillingCost:
currency: "USD"
amount: 17.00
roidSuffix: "1TLD"
serverStatusChangeBillingCost:
currency: "USD"
amount: 19.00
tldStateTransitions:
"1970-01-01T00:00:00.000Z": "GENERAL_AVAILABILITY"
tldStr: "1tld"
tldType: "REAL"
tldUnicode: "1tld"
transferGracePeriodLength: 432000000
@@ -0,0 +1,56 @@
addGracePeriodLength: 432000000
allowedFullyQualifiedHostNames: []
allowedRegistrantContactIds: []
anchorTenantAddGracePeriodLength: 2592000000
autoRenewGracePeriodLength: 3888000000
automaticTransferLength: 432000000
claimsPeriodEnd: "294247-01-10T04:00:54.775Z"
createBillingCost:
currency: "USD"
amount: 25.00
creationTime: "2022-09-01T00:00:00.000Z"
currency: "USD"
defaultPromoTokens: []
dnsAPlusAaaaTtl: 900
dnsDsTtl: null
dnsNsTtl: null
dnsPaused: false
dnsWriters:
- "VoidDnsWriter"
driveFolderId: "driveFolder"
eapFeeSchedule:
"1970-01-01T00:00:00.000Z":
currency: "USD"
amount: 0.00
escrowEnabled: false
idnTables:
- "foo"
invoicingEnabled: false
lordnUsername: null
numDnsPublishLocks: 1
pendingDeleteLength: 432000000
premiumListName: "test"
pricingEngineClassName: "google.registry.model.pricing.StaticPremiumListPricingEngine"
redemptionGracePeriodLength: 2592000000
registryLockOrUnlockBillingCost:
currency: "USD"
amount: 0.00
renewBillingCostTransitions:
"1970-01-01T00:00:00.000Z":
currency: "USD"
amount: 11.00
renewGracePeriodLength: 432000000
reservedListNames: []
restoreBillingCost:
currency: "USD"
amount: 17.00
roidSuffix: "BADIDN"
serverStatusChangeBillingCost:
currency: "USD"
amount: 19.00
tldStateTransitions:
"1970-01-01T00:00:00.000Z": "GENERAL_AVAILABILITY"
tldStr: "badidn"
tldType: "REAL"
tldUnicode: "badidn"
transferGracePeriodLength: 432000000
@@ -0,0 +1,55 @@
addGracePeriodLength: 432000000
allowedFullyQualifiedHostNames: []
allowedRegistrantContactIds: []
anchorTenantAddGracePeriodLength: 2592000000
autoRenewGracePeriodLength: 3888000000
automaticTransferLength: 432000000
claimsPeriodEnd: "294247-01-10T04:00:54.775Z"
createBillingCost:
currency: "USD"
amount: 25.00
creationTime: "2022-09-01T00:00:00.000Z"
currency: "USD"
defaultPromoTokens: []
dnsAPlusAaaaTtl: 900
dnsDsTtl: null
dnsNsTtl: null
dnsPaused: false
dnsWriters:
- "VoidDnsWriter"
driveFolderId: "driveFolder"
eapFeeSchedule:
"1970-01-01T00:00:00.000Z":
currency: "USD"
amount: 0.00
escrowEnabled: false
idnTables: []
invoicingEnabled: false
lordnUsername: null
numDnsPublishLocks: 1
pendingDeleteLength: 432000000
premiumListName: "test"
pricingEngineClassName: "google.registry.model.pricing.StaticPremiumListPricingEngine"
redemptionGracePeriodLength: 2592000000
registryLockOrUnlockBillingCost:
currency: "USD"
amount: 0.00
renewBillingCostTransitions:
"1970-01-01T00:00:00.000Z":
currency: "USD"
amount: 11.00
renewGracePeriodLength: 432000000
reservedListNames: []
restoreBillingCost:
currency: "USD"
amount: 17.00
roidSuffix: "TLD"
serverStatusChangeBillingCost:
currency: "USD"
amount: 19.00
tldStateTransitions:
"1970-01-01T00:00:00.000Z": "GENERAL_AVAILABILITY"
tldStr: "badunicode"
tldType: "REAL"
tldUnicode: "tld"
transferGracePeriodLength: 432000000
@@ -0,0 +1,56 @@
addGracePeriodLength: 432000000
allowedFullyQualifiedHostNames: []
allowedRegistrantContactIds: []
anchorTenantAddGracePeriodLength: 2592000000
autoRenewGracePeriodLength: 3888000000
automaticTransferLength: 432000000
claimsPeriodEnd: "294247-01-10T04:00:54.775Z"
createBillingCost:
currency: "USD"
amount: 25.00
creationTime: "2022-09-01T00:00:00.000Z"
currency: "USD"
defaultPromoTokens: []
dnsAPlusAaaaTtl: 900
dnsDsTtl: null
dnsNsTtl: null
dnsPaused: false
dnsWriters:
- "VoidDnsWriter"
driveFolderId: "driveFolder"
eapFeeSchedule:
"1970-01-01T00:00:00.000Z":
currency: "USD"
amount: 0.00
escrowEnabled: false
idnTables: []
invoicingEnabled: false
lordnUsername: null
numDnsPublishLocks: 1
pendingDeleteLength: 432000000
premiumListName: "test"
pricingEngineClassName: "google.registry.model.pricing.StaticPremiumListPricingEngine"
redemptionGracePeriodLength: 2592000000
registryLockOrUnlockBillingCost:
currency: "USD"
amount: 0.00
renewBillingCostTransitions:
"1970-01-01T00:00:00.000Z":
currency: "USD"
amount: 11.00
renewGracePeriodLength: 432000000
reservedListNames: []
restoreBillingCost:
currency: "USD"
amount: 17.00
roidSuffix: "EXTRA"
serverStatusChangeBillingCost:
currency: "USD"
amount: 19.00
tldStateTransitions:
"1970-01-01T00:00:00.000Z": "GENERAL_AVAILABILITY"
tldStr: "extrafield"
tldType: "REAL"
tldUnicode: "extrafield"
transferGracePeriodLength: 432000000
extraField: "hello"
@@ -0,0 +1,50 @@
addGracePeriodLength: 432000000
allowedFullyQualifiedHostNames: []
allowedRegistrantContactIds: []
anchorTenantAddGracePeriodLength: 2592000000
automaticTransferLength: 432000000
autoRenewGracePeriodLength: 3888000000
claimsPeriodEnd: "294247-01-10T04:00:54.775Z"
createBillingCost:
currency: "USD"
amount: 25.00
creationTime: "2022-09-01T00:00:00.000Z"
defaultPromoTokens: []
dnsAPlusAaaaTtl: null
dnsDsTtl: null
dnsNsTtl: null
dnsPaused: false
dnsWriters:
- "VoidDnsWriter"
driveFolderId: "driveFolder"
eapFeeSchedule:
"1970-01-01T00:00:00.000Z":
currency: "USD"
amount: 0.00
escrowEnabled: false
idnTables: []
invoicingEnabled: false
lordnUsername: null
pendingDeleteLength: 432000000
pricingEngineClassName: "google.registry.model.pricing.StaticPremiumListPricingEngine"
redemptionGracePeriodLength: 2592000000
registryLockOrUnlockBillingCost:
currency: "USD"
amount: 0.00
renewBillingCostTransitions:
"1970-01-01T00:00:00.000Z":
currency: "USD"
amount: 11.00
renewGracePeriodLength: 432000000
reservedListNames: []
restoreBillingCost:
currency: "USD"
amount: 17.00
roidSuffix: "NONULLS"
serverStatusChangeBillingCost:
currency: "USD"
amount: 19.00
tldStr: "missingnullablefields"
tldType: "REAL"
tldUnicode: "missingnullablefields"
transferGracePeriodLength: 432000000
@@ -0,0 +1,55 @@
tldStr: "nullablefieldsallnull"
roidSuffix: "NULLS"
pricingEngineClassName: null
dnsWriters:
- "VoidDnsWriter"
numDnsPublishLocks: 1
dnsAPlusAaaaTtl: null
dnsNsTtl: null
dnsDsTtl: null
tldUnicode: "nullablefieldsallnull"
driveFolderId: null
tldType: "REAL"
invoicingEnabled: false
tldStateTransitions:
"1970-01-01T00:00:00.000Z": "GENERAL_AVAILABILITY"
creationTime: "2022-09-01T00:00:00.000Z"
reservedListNames: null
premiumListName: null
escrowEnabled: false
dnsPaused: false
addGracePeriodLength: 432000000
anchorTenantAddGracePeriodLength: 2592000000
autoRenewGracePeriodLength: 3888000000
redemptionGracePeriodLength: 2592000000
renewGracePeriodLength: 432000000
transferGracePeriodLength: 432000000
automaticTransferLength: 432000000
pendingDeleteLength: 432000000
currency: "USD"
createBillingCost:
currency: "USD"
amount: 25.00
restoreBillingCost:
currency: "USD"
amount: 17.00
serverStatusChangeBillingCost:
currency: "USD"
amount: 19.00
registryLockOrUnlockBillingCost:
currency: "USD"
amount: 0.00
renewBillingCostTransitions:
"1970-01-01T00:00:00.000Z":
currency: "USD"
amount: 11.00
lordnUsername: null
claimsPeriodEnd: "294247-01-10T04:00:54.775Z"
allowedRegistrantContactIds: null
allowedFullyQualifiedHostNames: null
defaultPromoTokens: null
idnTables: null
eapFeeSchedule:
"1970-01-01T00:00:00.000Z":
currency: "USD"
amount: 0.00
@@ -0,0 +1,55 @@
pricingEngineClassName: "google.registry.model.pricing.StaticPremiumListPricingEngine"
numDnsPublishLocks: 1
creationTime: "2022-09-01T00:00:00.000Z"
reservedListNames: []
dnsPaused: false
tldType: "REAL"
escrowEnabled: false
anchorTenantAddGracePeriodLength: 2592000000
dnsNsTtl: null
tldStr: "outoforderfields"
roidSuffix: "TLD"
dnsWriters:
- "VoidDnsWriter"
dnsAPlusAaaaTtl: 900
dnsDsTtl: null
tldUnicode: "outoforderfields"
driveFolderId: "driveFolder"
invoicingEnabled: false
tldStateTransitions:
"1970-01-01T00:00:00.000Z": "GENERAL_AVAILABILITY"
premiumListName: "test"
addGracePeriodLength: 432000000
autoRenewGracePeriodLength: 3888000000
redemptionGracePeriodLength: 2592000000
renewGracePeriodLength: 432000000
transferGracePeriodLength: 432000000
automaticTransferLength: 432000000
pendingDeleteLength: 432000000
currency: "USD"
createBillingCost:
currency: "USD"
amount: 25.00
restoreBillingCost:
currency: "USD"
amount: 17.00
serverStatusChangeBillingCost:
currency: "USD"
amount: 19.00
registryLockOrUnlockBillingCost:
currency: "USD"
amount: 0.00
renewBillingCostTransitions:
"1970-01-01T00:00:00.000Z":
currency: "USD"
amount: 11.00
lordnUsername: null
claimsPeriodEnd: "294247-01-10T04:00:54.775Z"
allowedRegistrantContactIds: []
allowedFullyQualifiedHostNames: []
defaultPromoTokens: []
idnTables: []
eapFeeSchedule:
"1970-01-01T00:00:00.000Z":
currency: "USD"
amount: 0.00
@@ -7,17 +7,17 @@ automaticTransferLength: 432000000
claimsPeriodEnd: "294247-01-10T04:00:54.775Z"
createBillingCost:
currency: "USD"
amount: 13.00
amount: 25.00
creationTime: "2022-09-01T00:00:00.000Z"
currency: "USD"
defaultPromoTokens: []
dnsAPlusAaaaTtl: null
dnsAPlusAaaaTtl: 900
dnsDsTtl: null
dnsNsTtl: null
dnsPaused: false
dnsWriters:
- "VoidDnsWriter"
driveFolderId: null
driveFolderId: "driveFolder"
eapFeeSchedule:
"1970-01-01T00:00:00.000Z":
currency: "USD"
@@ -28,7 +28,7 @@ invoicingEnabled: false
lordnUsername: null
numDnsPublishLocks: 1
pendingDeleteLength: 432000000
premiumListName: "xn--q9jyb4c"
premiumListName: "test"
pricingEngineClassName: "google.registry.model.pricing.StaticPremiumListPricingEngine"
redemptionGracePeriodLength: 2592000000
registryLockOrUnlockBillingCost:
@@ -43,13 +43,13 @@ reservedListNames: []
restoreBillingCost:
currency: "USD"
amount: 17.00
roidSuffix: "Q9JYB4C"
roidSuffix: "TLD"
serverStatusChangeBillingCost:
currency: "USD"
amount: 19.00
tldStateTransitions:
"1970-01-01T00:00:00.000Z": "GENERAL_AVAILABILITY"
tldStr: "xn--q9jyb4c"
tldStr: "tld"
tldType: "REAL"
tldUnicode: "みんな"
transferGracePeriodLength: 432000000
tldUnicode: "tld"
transferGracePeriodLength: 432000000
@@ -0,0 +1,55 @@
addGracePeriodLength: 432000000
allowedFullyQualifiedHostNames: []
allowedRegistrantContactIds: []
anchorTenantAddGracePeriodLength: 2592000000
autoRenewGracePeriodLength: 3888000000
automaticTransferLength: 432000000
claimsPeriodEnd: "294247-01-10T04:00:54.775Z"
createBillingCost:
currency: "USD"
amount: 25.00
creationTime: "2022-09-01T00:00:00.000Z"
currency: "USD"
defaultPromoTokens: []
dnsAPlusAaaaTtl: 900
dnsDsTtl: null
dnsNsTtl: null
dnsPaused: false
dnsWriters:
- "VoidDnsWriter"
driveFolderId: "driveFolder"
eapFeeSchedule:
"1970-01-01T00:00:00.000Z":
currency: "USD"
amount: 0.00
escrowEnabled: false
idnTables: []
invoicingEnabled: false
lordnUsername: null
numDnsPublishLocks: 1
pendingDeleteLength: 432000000
premiumListName: "test"
pricingEngineClassName: "google.registry.model.pricing.StaticPremiumListPricingEngine"
redemptionGracePeriodLength: 2592000000
registryLockOrUnlockBillingCost:
currency: "USD"
amount: 0.00
renewBillingCostTransitions:
"1970-01-01T00:00:00.000Z":
currency: "USD"
amount: 11.00
renewGracePeriodLength: 432000000
reservedListNames: []
restoreBillingCost:
currency: "USD"
amount: 17.00
roidSuffix: %ROIDSUFFIX%
serverStatusChangeBillingCost:
currency: "USD"
amount: 19.00
tldStateTransitions:
"1970-01-01T00:00:00.000Z": "GENERAL_AVAILABILITY"
tldStr: %TLDSTR%
tldType: "REAL"
tldUnicode: %TLDUNICODE%
transferGracePeriodLength: 432000000
@@ -0,0 +1,55 @@
tldStr: "wrongcurrency"
roidSuffix: "WRONGCUR"
pricingEngineClassName: "google.registry.model.pricing.StaticPremiumListPricingEngine"
dnsWriters:
- "VoidDnsWriter"
numDnsPublishLocks: 1
dnsAPlusAaaaTtl: 900
dnsNsTtl: null
dnsDsTtl: null
tldUnicode: "wrongcurrency"
driveFolderId: "driveFolder"
tldType: "REAL"
invoicingEnabled: false
tldStateTransitions:
"1970-01-01T00:00:00.000Z": "GENERAL_AVAILABILITY"
creationTime: "2022-09-01T00:00:00.000Z"
reservedListNames: []
premiumListName: "test"
escrowEnabled: false
dnsPaused: false
addGracePeriodLength: 432000000
anchorTenantAddGracePeriodLength: 2592000000
autoRenewGracePeriodLength: 3888000000
redemptionGracePeriodLength: 2592000000
renewGracePeriodLength: 432000000
transferGracePeriodLength: 432000000
automaticTransferLength: 432000000
pendingDeleteLength: 432000000
currency: "USD"
createBillingCost:
currency: "USD"
amount: 25.00
restoreBillingCost:
currency: %RESTORECURRENCY%
amount: 70.00
serverStatusChangeBillingCost:
currency: "USD"
amount: 19.00
registryLockOrUnlockBillingCost:
currency: "USD"
amount: 0.00
renewBillingCostTransitions:
"1970-01-01T00:00:00.000Z":
currency: %RENEWCURRENCY%
amount: 11.00
lordnUsername: null
claimsPeriodEnd: "294247-01-10T04:00:54.775Z"
allowedRegistrantContactIds: []
allowedFullyQualifiedHostNames: []
defaultPromoTokens: []
idnTables: []
eapFeeSchedule:
"1970-01-01T00:00:00.000Z":
currency: "USD"
amount: 0.00