diff --git a/core/src/main/java/google/registry/model/registry/label/BaseDomainLabelList.java b/core/src/main/java/google/registry/model/registry/label/BaseDomainLabelList.java index 833af9d19..aeb1e9092 100644 --- a/core/src/main/java/google/registry/model/registry/label/BaseDomainLabelList.java +++ b/core/src/main/java/google/registry/model/registry/label/BaseDomainLabelList.java @@ -121,7 +121,7 @@ public abstract class BaseDomainLabelList, R extends Dom * (sans comment) and the comment (in that order). If the line was blank or empty, then this * method returns an empty list. */ - protected static List splitOnComment(String line) { + public static List splitOnComment(String line) { String comment = ""; int index = line.indexOf('#'); if (index != -1) { diff --git a/core/src/main/java/google/registry/schema/tld/ReservedList.java b/core/src/main/java/google/registry/schema/tld/ReservedList.java index c3897d157..86a9a27ed 100644 --- a/core/src/main/java/google/registry/schema/tld/ReservedList.java +++ b/core/src/main/java/google/registry/schema/tld/ReservedList.java @@ -14,13 +14,20 @@ package google.registry.schema.tld; +import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Preconditions.checkState; +import static com.google.common.collect.ImmutableList.sortedCopyOf; +import static com.google.common.collect.ImmutableList.toImmutableList; +import static google.registry.util.DomainNameUtils.canonicalizeDomainName; +import com.google.common.base.Joiner; +import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import google.registry.model.CreateAutoTimestamp; import google.registry.model.ImmutableObject; import google.registry.model.registry.label.ReservationType; import java.util.Map; +import java.util.stream.Stream; import javax.annotation.Nullable; import javax.persistence.CollectionTable; import javax.persistence.Column; @@ -114,6 +121,22 @@ public class ReservedList extends ImmutableObject { /** Constructs a {@link ReservedList} object. */ public static ReservedList create( String name, Boolean shouldPublish, Map labelsToReservations) { + ImmutableList invalidLabels = + labelsToReservations.entrySet().parallelStream() + .flatMap( + entry -> { + String label = entry.getKey(); + if (label.equals(canonicalizeDomainName(label))) { + return Stream.empty(); + } else { + return Stream.of(label); + } + }) + .collect(toImmutableList()); + checkArgument( + invalidLabels.isEmpty(), + "Label(s) [%s] must be in puny-coded, lower-case form", + Joiner.on(",").join(sortedCopyOf(invalidLabels))); return new ReservedList(name, shouldPublish, labelsToReservations); } diff --git a/core/src/main/java/google/registry/schema/tld/ReservedListDao.java b/core/src/main/java/google/registry/schema/tld/ReservedListDao.java new file mode 100644 index 000000000..d80a7f38a --- /dev/null +++ b/core/src/main/java/google/registry/schema/tld/ReservedListDao.java @@ -0,0 +1,47 @@ +// Copyright 2019 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.schema.tld; + +import static google.registry.model.transaction.TransactionManagerFactory.jpaTm; + +/** Data access object class for {@link ReservedList} */ +public class ReservedListDao { + + /** Persist a new reserved list to Cloud SQL. */ + public static void save(ReservedList reservedList) { + jpaTm().transact(() -> jpaTm().getEntityManager().persist(reservedList)); + } + + /** + * Returns whether the reserved list of the given name exists. + * + *

This means that at least one reserved list revision must exist for the given name. + */ + public static boolean checkExists(String reservedListName) { + return jpaTm() + .transact( + () -> + jpaTm() + .getEntityManager() + .createQuery("SELECT 1 FROM ReservedList WHERE name = :name", Integer.class) + .setParameter("name", reservedListName) + .setMaxResults(1) + .getResultList() + .size() + > 0); + } + + private ReservedListDao() {} +} diff --git a/core/src/main/java/google/registry/tools/CreateOrUpdateReservedListCommand.java b/core/src/main/java/google/registry/tools/CreateOrUpdateReservedListCommand.java index bceb1e590..00d78390d 100644 --- a/core/src/main/java/google/registry/tools/CreateOrUpdateReservedListCommand.java +++ b/core/src/main/java/google/registry/tools/CreateOrUpdateReservedListCommand.java @@ -14,9 +14,23 @@ package google.registry.tools; +import static com.google.common.base.Preconditions.checkArgument; +import static google.registry.model.registry.label.BaseDomainLabelList.splitOnComment; +import static google.registry.util.DomainNameUtils.canonicalizeDomainName; + import com.beust.jcommander.Parameter; +import com.google.common.base.Splitter; +import com.google.common.collect.HashMultiset; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.Maps; +import com.google.common.collect.Multiset; +import com.google.common.flogger.FluentLogger; +import google.registry.model.registry.label.ReservationType; +import google.registry.schema.tld.ReservedList.ReservedEntry; import google.registry.tools.params.PathParameter; import java.nio.file.Path; +import java.util.List; +import java.util.Map; import javax.annotation.Nullable; /** @@ -25,6 +39,8 @@ import javax.annotation.Nullable; */ public abstract class CreateOrUpdateReservedListCommand extends MutatingCommand { + static final FluentLogger logger = FluentLogger.forEnclosingClass(); + @Nullable @Parameter( names = {"-n", "--name"}, @@ -45,4 +61,81 @@ public abstract class CreateOrUpdateReservedListCommand extends MutatingCommand "Whether the list is published to the concatenated list on Drive (defaults to true).", arity = 1) Boolean shouldPublish; + + @Parameter( + names = {"--also_cloud_sql"}, + description = + "Persist reserved list to Cloud SQL in addition to Datastore; defaults to false.") + boolean alsoCloudSql; + + google.registry.schema.tld.ReservedList cloudSqlReservedList; + + abstract void saveToCloudSql(); + + @Override + protected String execute() throws Exception { + // Save the list to Datastore and output its response. + String output = super.execute(); + logger.atInfo().log(output); + + String cloudSqlMessage; + if (alsoCloudSql) { + cloudSqlMessage = + String.format( + "Saved reserved list %s with %d entries", + name, cloudSqlReservedList.getLabelsToReservations().size()); + try { + logger.atInfo().log("Saving reserved list to Cloud SQL for TLD %s", name); + saveToCloudSql(); + logger.atInfo().log(cloudSqlMessage); + } catch (Throwable e) { + cloudSqlMessage = + "Unexpected error saving reserved list to Cloud SQL from nomulus tool command"; + logger.atSevere().withCause(e).log(cloudSqlMessage); + } + } else { + cloudSqlMessage = "Persisting reserved list to Cloud SQL is not enabled"; + } + return cloudSqlMessage; + } + + /** Turns the list CSV data into a map of labels to {@link ReservedEntry}. */ + static ImmutableMap parseToReservationsByLabels(Iterable lines) { + Map labelsToEntries = Maps.newHashMap(); + Multiset duplicateLabels = HashMultiset.create(); + for (String originalLine : lines) { + List lineAndComment = splitOnComment(originalLine); + if (lineAndComment.isEmpty()) { + continue; + } + String line = lineAndComment.get(0); + String comment = lineAndComment.get(1); + List parts = Splitter.on(',').trimResults().splitToList(line); + checkArgument( + parts.size() == 2 || parts.size() == 3, + "Could not parse line in reserved list: %s", + originalLine); + String label = parts.get(0); + checkArgument( + label.equals(canonicalizeDomainName(label)), + "Label '%s' must be in puny-coded, lower-case form", + label); + ReservationType reservationType = ReservationType.valueOf(parts.get(1)); + ReservedEntry reservedEntry = ReservedEntry.create(reservationType, comment); + // Check if the label was already processed for this list (which is an error), and if so, + // accumulate it so that a list of all duplicates can be thrown. + if (labelsToEntries.containsKey(label)) { + duplicateLabels.add(label, duplicateLabels.contains(label) ? 1 : 2); + } else { + labelsToEntries.put(label, reservedEntry); + } + } + if (!duplicateLabels.isEmpty()) { + throw new IllegalStateException( + String.format( + "Reserved list cannot contain duplicate labels. Dupes (with counts) were: %s", + duplicateLabels)); + } + return ImmutableMap.copyOf(labelsToEntries); + } } diff --git a/core/src/main/java/google/registry/tools/CreateReservedListCommand.java b/core/src/main/java/google/registry/tools/CreateReservedListCommand.java index fc0dbafe4..0fd45c87b 100644 --- a/core/src/main/java/google/registry/tools/CreateReservedListCommand.java +++ b/core/src/main/java/google/registry/tools/CreateReservedListCommand.java @@ -16,6 +16,7 @@ package google.registry.tools; import static com.google.common.base.Preconditions.checkArgument; import static google.registry.model.registry.Registries.assertTldExists; +import static google.registry.model.transaction.TransactionManagerFactory.jpaTm; import static google.registry.util.ListNamingUtils.convertFilePathToName; import static java.nio.charset.StandardCharsets.UTF_8; import static org.joda.time.DateTimeZone.UTC; @@ -26,6 +27,7 @@ import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Splitter; import com.google.common.base.Strings; import google.registry.model.registry.label.ReservedList; +import google.registry.schema.tld.ReservedListDao; import java.nio.file.Files; import java.util.List; import org.joda.time.DateTime; @@ -54,15 +56,35 @@ final class CreateReservedListCommand extends CreateOrUpdateReservedListCommand validateListName(name); } DateTime now = DateTime.now(UTC); + List allLines = Files.readAllLines(input, UTF_8); + boolean shouldPublish = this.shouldPublish == null || this.shouldPublish; ReservedList reservedList = new ReservedList.Builder() .setName(name) - .setReservedListMapFromLines(Files.readAllLines(input, UTF_8)) - .setShouldPublish(shouldPublish == null || shouldPublish) + .setReservedListMapFromLines(allLines) + .setShouldPublish(shouldPublish) .setCreationTime(now) .setLastUpdateTime(now) .build(); stageEntityChange(null, reservedList); + if (alsoCloudSql) { + cloudSqlReservedList = + google.registry.schema.tld.ReservedList.create( + name, shouldPublish, parseToReservationsByLabels(allLines)); + } + } + + @Override + void saveToCloudSql() { + jpaTm() + .transact( + () -> { + checkArgument( + !ReservedListDao.checkExists(cloudSqlReservedList.getName()), + "A reserved list of this name already exists: %s.", + cloudSqlReservedList.getName()); + ReservedListDao.save(cloudSqlReservedList); + }); } private static void validateListName(String name) { diff --git a/core/src/main/java/google/registry/tools/UpdateReservedListCommand.java b/core/src/main/java/google/registry/tools/UpdateReservedListCommand.java index 027b85d19..a15797063 100644 --- a/core/src/main/java/google/registry/tools/UpdateReservedListCommand.java +++ b/core/src/main/java/google/registry/tools/UpdateReservedListCommand.java @@ -15,14 +15,17 @@ package google.registry.tools; import static com.google.common.base.Preconditions.checkArgument; +import static google.registry.model.transaction.TransactionManagerFactory.jpaTm; import static google.registry.util.ListNamingUtils.convertFilePathToName; import static java.nio.charset.StandardCharsets.UTF_8; import com.beust.jcommander.Parameters; import com.google.common.base.Strings; import google.registry.model.registry.label.ReservedList; +import google.registry.schema.tld.ReservedListDao; import google.registry.util.SystemClock; import java.nio.file.Files; +import java.util.List; import java.util.Optional; /** Command to safely update {@link ReservedList} on Datastore. */ @@ -32,18 +35,38 @@ final class UpdateReservedListCommand extends CreateOrUpdateReservedListCommand @Override protected void init() throws Exception { name = Strings.isNullOrEmpty(name) ? convertFilePathToName(input) : name; + // TODO(shicong): Read existing entry from Cloud SQL Optional existing = ReservedList.get(name); checkArgument( existing.isPresent(), "Could not update reserved list %s because it doesn't exist.", name); + boolean shouldPublish = + this.shouldPublish == null ? existing.get().getShouldPublish() : this.shouldPublish; + List allLines = Files.readAllLines(input, UTF_8); ReservedList.Builder updated = existing .get() .asBuilder() - .setReservedListMapFromLines(Files.readAllLines(input, UTF_8)) - .setLastUpdateTime(new SystemClock().nowUtc()); - if (shouldPublish != null) { - updated.setShouldPublish(shouldPublish); - } + .setReservedListMapFromLines(allLines) + .setLastUpdateTime(new SystemClock().nowUtc()) + .setShouldPublish(shouldPublish); stageEntityChange(existing.get(), updated.build()); + if (alsoCloudSql) { + cloudSqlReservedList = + google.registry.schema.tld.ReservedList.create( + name, shouldPublish, parseToReservationsByLabels(allLines)); + } + } + + @Override + void saveToCloudSql() { + jpaTm() + .transact( + () -> { + checkArgument( + ReservedListDao.checkExists(cloudSqlReservedList.getName()), + "A reserved list of this name doesn't exist: %s.", + cloudSqlReservedList.getName()); + ReservedListDao.save(cloudSqlReservedList); + }); } } diff --git a/core/src/test/java/google/registry/schema/integration/SqlIntegrationTestSuite.java b/core/src/test/java/google/registry/schema/integration/SqlIntegrationTestSuite.java index 382f7b0af..9f08300a9 100644 --- a/core/src/test/java/google/registry/schema/integration/SqlIntegrationTestSuite.java +++ b/core/src/test/java/google/registry/schema/integration/SqlIntegrationTestSuite.java @@ -26,7 +26,10 @@ import google.registry.persistence.UpdateAutoTimestampConverterTest; import google.registry.persistence.ZonedDateTimeConverterTest; import google.registry.schema.cursor.CursorDaoTest; import google.registry.schema.tld.PremiumListDaoTest; +import google.registry.schema.tld.ReservedListDaoTest; import google.registry.schema.tmch.ClaimsListDaoTest; +import google.registry.tools.CreateReservedListCommandTest; +import google.registry.tools.UpdateReservedListCommandTest; import google.registry.ui.server.registrar.RegistryLockGetActionTest; import org.junit.runner.RunWith; import org.junit.runners.Suite; @@ -47,6 +50,7 @@ import org.junit.runners.Suite.SuiteClasses; BloomFilterConverterTest.class, ClaimsListDaoTest.class, CreateAutoTimestampConverterTest.class, + CreateReservedListCommandTest.class, CurrencyUnitConverterTest.class, CursorDaoTest.class, DateTimeConverterTest.class, @@ -56,7 +60,9 @@ import org.junit.runners.Suite.SuiteClasses; PremiumListDaoTest.class, RegistryLockDaoTest.class, RegistryLockGetActionTest.class, + ReservedListDaoTest.class, UpdateAutoTimestampConverterTest.class, + UpdateReservedListCommandTest.class, ZonedDateTimeConverterTest.class }) public class SqlIntegrationTestSuite {} diff --git a/core/src/test/java/google/registry/schema/tld/ReservedListDaoTest.java b/core/src/test/java/google/registry/schema/tld/ReservedListDaoTest.java new file mode 100644 index 000000000..e72547b9c --- /dev/null +++ b/core/src/test/java/google/registry/schema/tld/ReservedListDaoTest.java @@ -0,0 +1,69 @@ +// Copyright 2019 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.schema.tld; + +import static com.google.common.truth.Truth.assertThat; +import static google.registry.model.transaction.TransactionManagerFactory.jpaTm; + +import com.google.common.collect.ImmutableMap; +import google.registry.model.registry.label.ReservationType; +import google.registry.model.transaction.JpaTransactionManagerRule; +import google.registry.schema.tld.ReservedList.ReservedEntry; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +/** Unit tests for {@link ReservedListDao}. */ +@RunWith(JUnit4.class) +public class ReservedListDaoTest { + @Rule + public final JpaTransactionManagerRule jpaTmRule = + new JpaTransactionManagerRule.Builder().build(); + + private static final ImmutableMap TEST_RESERVATIONS = + ImmutableMap.of( + "food", + ReservedEntry.create(ReservationType.RESERVED_FOR_SPECIFIC_USE, null), + "music", + ReservedEntry.create(ReservationType.FULLY_BLOCKED, "fully blocked")); + + @Test + public void save_worksSuccessfully() { + ReservedList reservedList = ReservedList.create("testname", false, TEST_RESERVATIONS); + ReservedListDao.save(reservedList); + jpaTm() + .transact( + () -> { + ReservedList persistedList = + jpaTm() + .getEntityManager() + .createQuery("FROM ReservedList WHERE name = :name", ReservedList.class) + .setParameter("name", "testname") + .getSingleResult(); + assertThat(persistedList.getLabelsToReservations()) + .containsExactlyEntriesIn(TEST_RESERVATIONS); + assertThat(persistedList.getCreationTimestamp()) + .isEqualTo(jpaTmRule.getTxnClock().nowUtc()); + }); + } + + @Test + public void checkExists_worksSuccessfully() { + assertThat(ReservedListDao.checkExists("testlist")).isFalse(); + ReservedListDao.save(ReservedList.create("testlist", false, TEST_RESERVATIONS)); + assertThat(ReservedListDao.checkExists("testlist")).isTrue(); + } +} diff --git a/core/src/test/java/google/registry/schema/tld/ReservedListTest.java b/core/src/test/java/google/registry/schema/tld/ReservedListTest.java index 5d3643b37..e72efd4e8 100644 --- a/core/src/test/java/google/registry/schema/tld/ReservedListTest.java +++ b/core/src/test/java/google/registry/schema/tld/ReservedListTest.java @@ -15,6 +15,8 @@ package google.registry.schema.tld; import static com.google.common.truth.Truth.assertThat; +import static google.registry.model.registry.label.ReservationType.FULLY_BLOCKED; +import static google.registry.testing.JUnitBackports.assertThrows; import com.google.common.collect.ImmutableMap; import google.registry.model.registry.label.ReservationType; @@ -50,4 +52,34 @@ public class ReservedListTest { ReservedEntry.create( ReservationType.RESERVED_FOR_ANCHOR_TENANT, "reserved for anchor tenant")); } + + @Test + public void create_throwsExceptionWhenLabelIsNotLowercase() { + Exception e = + assertThrows( + IllegalArgumentException.class, + () -> + ReservedList.create( + "UPPER.tld", + true, + ImmutableMap.of("UPPER", ReservedEntry.create(FULLY_BLOCKED, "")))); + assertThat(e) + .hasMessageThat() + .contains("Label(s) [UPPER] must be in puny-coded, lower-case form"); + } + + @Test + public void create_labelMustBePunyCoded() { + Exception e = + assertThrows( + IllegalArgumentException.class, + () -> + ReservedList.create( + "lower.みんな", + true, + ImmutableMap.of("みんな", ReservedEntry.create(FULLY_BLOCKED, "")))); + assertThat(e) + .hasMessageThat() + .contains("Label(s) [みんな] must be in puny-coded, lower-case form"); + } } diff --git a/core/src/test/java/google/registry/tools/CreateOrUpdateReservedListCommandTest.java b/core/src/test/java/google/registry/tools/CreateOrUpdateReservedListCommandTest.java new file mode 100644 index 000000000..74271224c --- /dev/null +++ b/core/src/test/java/google/registry/tools/CreateOrUpdateReservedListCommandTest.java @@ -0,0 +1,126 @@ +// Copyright 2019 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.registry.label.ReservationType.ALLOWED_IN_SUNRISE; +import static google.registry.model.registry.label.ReservationType.FULLY_BLOCKED; +import static google.registry.testing.JUnitBackports.assertThrows; +import static google.registry.tools.CreateOrUpdateReservedListCommand.parseToReservationsByLabels; + +import com.google.common.collect.ImmutableList; +import google.registry.model.registry.label.ReservationType; +import google.registry.schema.tld.ReservedList.ReservedEntry; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +/** Unit tests for {@link CreateOrUpdateReservedListCommand}. */ +@RunWith(JUnit4.class) +public class CreateOrUpdateReservedListCommandTest { + + @Test + public void parseToReservationsByLabels_worksCorrectly() { + assertThat( + parseToReservationsByLabels( + ImmutableList.of( + "reserveddomain,FULLY_BLOCKED", + "availableinga,ALLOWED_IN_SUNRISE#allowed_in_sunrise", + "fourletterword,FULLY_BLOCKED"))) + .containsExactly( + "reserveddomain", + ReservedEntry.create(ReservationType.FULLY_BLOCKED, ""), + "availableinga", + ReservedEntry.create(ALLOWED_IN_SUNRISE, "allowed_in_sunrise"), + "fourletterword", + ReservedEntry.create(FULLY_BLOCKED, "")); + } + + @Test + public void parseToReservationsByLabels_throwsExceptionForInvalidLabel() { + Throwable thrown = + assertThrows( + IllegalArgumentException.class, + () -> + parseToReservationsByLabels( + ImmutableList.of("reserveddomain,FULLY_BLOCKED", "UPPER,FULLY_BLOCKED"))); + assertThat(thrown) + .hasMessageThat() + .isEqualTo("Label 'UPPER' must be in puny-coded, lower-case form"); + } + + @Test + public void parseToReservationsByLabels_throwsExceptionForNonPunyCodedLabel() { + Throwable thrown = + assertThrows( + IllegalArgumentException.class, + () -> + parseToReservationsByLabels( + ImmutableList.of("reserveddomain,FULLY_BLOCKED", "みんな,FULLY_BLOCKED"))); + assertThat(thrown) + .hasMessageThat() + .isEqualTo("Label 'みんな' must be in puny-coded, lower-case form"); + } + + @Test + public void parseToReservationsByLabels_throwsExceptionForInvalidReservationType() { + Throwable thrown = + assertThrows( + IllegalArgumentException.class, + () -> + parseToReservationsByLabels( + ImmutableList.of( + "reserveddomain,FULLY_BLOCKED", "invalidtype,INVALID_RESERVATION_TYPE"))); + assertThat(thrown) + .hasMessageThat() + .isEqualTo( + "No enum constant" + + " google.registry.model.registry.label.ReservationType.INVALID_RESERVATION_TYPE"); + } + + @Test + public void parseToReservationsByLabels_throwsExceptionForInvalidLines() { + Throwable thrown = + assertThrows( + IllegalArgumentException.class, + () -> + parseToReservationsByLabels( + ImmutableList.of( + "reserveddomain,FULLY_BLOCKED,too,many,parts", + "fourletterword,FULLY_BLOCKED"))); + assertThat(thrown) + .hasMessageThat() + .isEqualTo( + "Could not parse line in reserved list: reserveddomain,FULLY_BLOCKED,too,many,parts"); + } + + @Test + public void parseToReservationsByLabels_throwsExceptionForDuplicateEntries() { + Throwable thrown = + assertThrows( + IllegalStateException.class, + () -> + parseToReservationsByLabels( + ImmutableList.of( + "reserveddomain,FULLY_BLOCKED", + "fourletterword,FULLY_BLOCKED", + "fourletterword,FULLY_BLOCKED"))); + assertThat(thrown) + .hasMessageThat() + .isEqualTo( + "Reserved list cannot contain duplicate labels. Dupes (with counts) were:" + + " [fourletterword x 2]"); + } +} diff --git a/core/src/test/java/google/registry/tools/CreateOrUpdateReservedListCommandTestCase.java b/core/src/test/java/google/registry/tools/CreateOrUpdateReservedListCommandTestCase.java index ed04a5855..a03af7daf 100644 --- a/core/src/test/java/google/registry/tools/CreateOrUpdateReservedListCommandTestCase.java +++ b/core/src/test/java/google/registry/tools/CreateOrUpdateReservedListCommandTestCase.java @@ -14,15 +14,26 @@ package google.registry.tools; +import static com.google.common.truth.Truth.assertThat; +import static google.registry.model.registry.label.ReservationType.FULLY_BLOCKED; +import static google.registry.model.transaction.TransactionManagerFactory.jpaTm; import static google.registry.testing.JUnitBackports.assertThrows; import static google.registry.testing.TestDataHelper.loadFile; import static java.nio.charset.StandardCharsets.UTF_8; import com.beust.jcommander.ParameterException; import com.google.common.io.Files; +import com.google.common.truth.Truth8; +import google.registry.model.registry.label.ReservedList; +import google.registry.model.transaction.JpaTransactionManagerRule; +import google.registry.schema.tld.ReservedList.ReservedEntry; +import google.registry.schema.tld.ReservedListDao; import java.io.File; import java.io.IOException; +import java.util.Map; +import javax.persistence.EntityManager; import org.junit.Before; +import org.junit.Rule; import org.junit.Test; /** @@ -33,6 +44,10 @@ import org.junit.Test; public abstract class CreateOrUpdateReservedListCommandTestCase extends CommandTestCase { + @Rule + public final JpaTransactionManagerRule jpaTmRule = + new JpaTransactionManagerRule.Builder().build(); + String reservedTermsPath; String invalidReservedTermsPath; @@ -64,4 +79,52 @@ public abstract class CreateOrUpdateReservedListCommandTestCase IllegalArgumentException.class, () -> runCommandForced("--name=xn--q9jyb4c-blork", "--input=" + invalidReservedTermsPath)); } + + google.registry.schema.tld.ReservedList createCloudSqlReservedList( + String name, boolean shouldPublish, Map labelsToEntries) { + return google.registry.schema.tld.ReservedList.create(name, shouldPublish, labelsToEntries); + } + + google.registry.schema.tld.ReservedList getCloudSqlReservedList(String name) { + return jpaTm() + .transact( + () -> { + EntityManager em = jpaTm().getEntityManager(); + long revisionId = + em.createQuery( + "SELECT MAX(rl.revisionId) FROM ReservedList rl WHERE name = :name", + Long.class) + .setParameter("name", name) + .getSingleResult(); + return em.createQuery( + "FROM ReservedList rl LEFT JOIN FETCH rl.labelsToReservations WHERE" + + " rl.revisionId = :revisionId", + google.registry.schema.tld.ReservedList.class) + .setParameter("revisionId", revisionId) + .getSingleResult(); + }); + } + + void verifyXnq9jyb4cInCloudSql() { + assertThat(ReservedListDao.checkExists("xn--q9jyb4c_common-reserved")).isTrue(); + google.registry.schema.tld.ReservedList persistedList = + getCloudSqlReservedList("xn--q9jyb4c_common-reserved"); + assertThat(persistedList.getName()).isEqualTo("xn--q9jyb4c_common-reserved"); + assertThat(persistedList.getShouldPublish()).isTrue(); + assertThat(persistedList.getCreationTimestamp()).isEqualTo(jpaTmRule.getTxnClock().nowUtc()); + assertThat(persistedList.getLabelsToReservations()) + .containsExactly( + "baddies", + ReservedEntry.create(FULLY_BLOCKED, ""), + "ford", + ReservedEntry.create(FULLY_BLOCKED, "random comment")); + } + + void verifyXnq9jyb4cInDatastore() { + Truth8.assertThat(ReservedList.get("xn--q9jyb4c_common-reserved")).isPresent(); + ReservedList reservedList = ReservedList.get("xn--q9jyb4c_common-reserved").get(); + assertThat(reservedList.getReservedListEntries()).hasSize(2); + Truth8.assertThat(reservedList.getReservationInList("baddies")).hasValue(FULLY_BLOCKED); + Truth8.assertThat(reservedList.getReservationInList("ford")).hasValue(FULLY_BLOCKED); + } } diff --git a/core/src/test/java/google/registry/tools/CreateReservedListCommandTest.java b/core/src/test/java/google/registry/tools/CreateReservedListCommandTest.java index 6ca3940c5..a72e9136a 100644 --- a/core/src/test/java/google/registry/tools/CreateReservedListCommandTest.java +++ b/core/src/test/java/google/registry/tools/CreateReservedListCommandTest.java @@ -24,8 +24,11 @@ import static google.registry.testing.JUnitBackports.assertThrows; import static google.registry.tools.CreateReservedListCommand.INVALID_FORMAT_ERROR_MESSAGE; import static org.joda.time.DateTimeZone.UTC; +import com.google.common.collect.ImmutableMap; import google.registry.model.registry.Registry; import google.registry.model.registry.label.ReservedList; +import google.registry.schema.tld.ReservedList.ReservedEntry; +import google.registry.schema.tld.ReservedListDao; import org.joda.time.DateTime; import org.junit.Before; import org.junit.Test; @@ -173,6 +176,28 @@ public class CreateReservedListCommandTest extends runNameTestExpectedFailure("soy_$oy", INVALID_FORMAT_ERROR_MESSAGE); } + @Test + public void testSaveToCloudSql_succeeds() throws Exception { + runCommandForced( + "--name=xn--q9jyb4c_common-reserved", "--input=" + reservedTermsPath, "--also_cloud_sql"); + verifyXnq9jyb4cInDatastore(); + verifyXnq9jyb4cInCloudSql(); + } + + @Test + public void testSaveToCloudSql_noExceptionThrownWhenSaveFail() throws Exception { + // Note that, during the dual-write phase, we want to make sure that no exception will be + // thrown if saving reserved list to Cloud SQL fails. + ReservedListDao.save( + createCloudSqlReservedList( + "xn--q9jyb4c_common-reserved", + true, + ImmutableMap.of("testdomain", ReservedEntry.create(FULLY_BLOCKED, "")))); + runCommandForced( + "--name=xn--q9jyb4c_common-reserved", "--input=" + reservedTermsPath, "--also_cloud_sql"); + verifyXnq9jyb4cInDatastore(); + } + private void runNameTestExpectedFailure(String name, String expectedErrorMsg) { IllegalArgumentException thrown = assertThrows( diff --git a/core/src/test/java/google/registry/tools/UpdateReservedListCommandTest.java b/core/src/test/java/google/registry/tools/UpdateReservedListCommandTest.java index cacdd267a..160d04d28 100644 --- a/core/src/test/java/google/registry/tools/UpdateReservedListCommandTest.java +++ b/core/src/test/java/google/registry/tools/UpdateReservedListCommandTest.java @@ -22,14 +22,17 @@ import static google.registry.testing.JUnitBackports.assertThrows; import static google.registry.util.DateTimeUtils.START_OF_TIME; import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; import google.registry.model.registry.label.ReservedList; +import google.registry.schema.tld.ReservedList.ReservedEntry; +import google.registry.schema.tld.ReservedListDao; import org.junit.Test; /** Unit tests for {@link UpdateReservedListCommand}. */ public class UpdateReservedListCommandTest extends CreateOrUpdateReservedListCommandTestCase { - private void populateInitialReservedList(boolean shouldPublish) { + private void populateInitialReservedListInDatastore(boolean shouldPublish) { persistResource( new ReservedList.Builder() .setName("xn--q9jyb4c_common-reserved") @@ -40,6 +43,14 @@ public class UpdateReservedListCommandTest extends .build()); } + private void populateInitialReservedListInCloudSql(boolean shouldPublish) { + ReservedListDao.save( + createCloudSqlReservedList( + "xn--q9jyb4c_common-reserved", + shouldPublish, + ImmutableMap.of("helicopter", ReservedEntry.create(FULLY_BLOCKED, "")))); + } + @Test public void testSuccess() throws Exception { runSuccessfulUpdateTest("--name=xn--q9jyb4c_common-reserved", "--input=" + reservedTermsPath); @@ -52,7 +63,7 @@ public class UpdateReservedListCommandTest extends @Test public void testSuccess_lastUpdateTime_updatedCorrectly() throws Exception { - populateInitialReservedList(true); + populateInitialReservedListInDatastore(true); ReservedList original = ReservedList.get("xn--q9jyb4c_common-reserved").get(); runCommandForced("--input=" + reservedTermsPath); ReservedList updated = ReservedList.get("xn--q9jyb4c_common-reserved").get(); @@ -71,7 +82,7 @@ public class UpdateReservedListCommandTest extends @Test public void testSuccess_shouldPublish_doesntOverrideFalseIfNotSpecified() throws Exception { - populateInitialReservedList(false); + populateInitialReservedListInDatastore(false); runCommandForced("--input=" + reservedTermsPath); assertThat(ReservedList.get("xn--q9jyb4c_common-reserved")).isPresent(); ReservedList reservedList = ReservedList.get("xn--q9jyb4c_common-reserved").get(); @@ -79,7 +90,7 @@ public class UpdateReservedListCommandTest extends } private void runSuccessfulUpdateTest(String... args) throws Exception { - populateInitialReservedList(true); + populateInitialReservedListInDatastore(true); runCommandForced(args); assertThat(ReservedList.get("xn--q9jyb4c_common-reserved")).isPresent(); ReservedList reservedList = ReservedList.get("xn--q9jyb4c_common-reserved").get(); @@ -100,4 +111,25 @@ public class UpdateReservedListCommandTest extends runCommand("--force", "--name=xn--q9jyb4c_poobah", "--input=" + reservedTermsPath)); assertThat(thrown).hasMessageThat().contains(errorMessage); } + + @Test + public void testSaveToCloudSql_succeeds() throws Exception { + populateInitialReservedListInDatastore(true); + populateInitialReservedListInCloudSql(true); + runCommandForced( + "--name=xn--q9jyb4c_common-reserved", "--input=" + reservedTermsPath, "--also_cloud_sql"); + verifyXnq9jyb4cInDatastore(); + verifyXnq9jyb4cInCloudSql(); + } + + @Test + public void testSaveToCloudSql_noExceptionThrownWhenSaveFail() throws Exception { + // Note that, during the dual-write phase, we want to make sure that no exception will be + // thrown if saving reserved list to Cloud SQL fails. + populateInitialReservedListInDatastore(true); + runCommandForced( + "--name=xn--q9jyb4c_common-reserved", "--input=" + reservedTermsPath, "--also_cloud_sql"); + verifyXnq9jyb4cInDatastore(); + assertThat(ReservedListDao.checkExists("xn--q9jyb4c_common-reserved")).isFalse(); + } }