diff --git a/core/src/main/java/google/registry/persistence/CreateAutoTimestampConverter.java b/core/src/main/java/google/registry/persistence/CreateAutoTimestampConverter.java index 613c679b0..942248995 100644 --- a/core/src/main/java/google/registry/persistence/CreateAutoTimestampConverter.java +++ b/core/src/main/java/google/registry/persistence/CreateAutoTimestampConverter.java @@ -33,8 +33,7 @@ public class CreateAutoTimestampConverter @Override public Timestamp convertToDatabaseColumn(CreateAutoTimestamp entity) { - DateTime dateTime = - firstNonNull(((CreateAutoTimestamp) entity).getTimestamp(), jpaTm().getTransactionTime()); + DateTime dateTime = firstNonNull(entity.getTimestamp(), jpaTm().getTransactionTime()); return Timestamp.from(DateTimeUtils.toZonedDateTime(dateTime).toInstant()); } diff --git a/core/src/main/java/google/registry/schema/tld/PremiumList.java b/core/src/main/java/google/registry/schema/tld/PremiumList.java index 18da33bfa..a455c05a2 100644 --- a/core/src/main/java/google/registry/schema/tld/PremiumList.java +++ b/core/src/main/java/google/registry/schema/tld/PremiumList.java @@ -16,20 +16,24 @@ package google.registry.schema.tld; import static com.google.common.base.Preconditions.checkState; +import google.registry.model.CreateAutoTimestamp; +import google.registry.persistence.CreateAutoTimestampConverter; import java.math.BigDecimal; -import java.time.ZonedDateTime; import java.util.Map; import javax.persistence.CollectionTable; import javax.persistence.Column; +import javax.persistence.Convert; import javax.persistence.ElementCollection; import javax.persistence.Entity; import javax.persistence.GeneratedValue; import javax.persistence.GenerationType; import javax.persistence.Id; +import javax.persistence.Index; import javax.persistence.JoinColumn; import javax.persistence.MapKeyColumn; import javax.persistence.Table; import org.joda.money.CurrencyUnit; +import org.joda.time.DateTime; /** * A list of premium prices for domain names. @@ -40,33 +44,35 @@ import org.joda.money.CurrencyUnit; * This is fine though, because we only use the list with the highest revisionId. */ @Entity -@Table(name = "PremiumList") +@Table(indexes = {@Index(columnList = "name", name = "premiumlist_name_idx")}) public class PremiumList { + @Column(nullable = false) + private String name; + @Id @GeneratedValue(strategy = GenerationType.IDENTITY) - @Column(name = "revision_id") + @Column(nullable = false) private Long revisionId; - @Column(name = "creation_timestamp", nullable = false) - private ZonedDateTime creationTimestamp; + @Column(nullable = false) + @Convert(converter = CreateAutoTimestampConverter.class) + private CreateAutoTimestamp creationTimestamp = CreateAutoTimestamp.create(null); - @Column(name = "currency", nullable = false) + @Column(nullable = false) private CurrencyUnit currency; @ElementCollection @CollectionTable( name = "PremiumEntry", - joinColumns = @JoinColumn(name = "revision_id", referencedColumnName = "revision_id")) - @MapKeyColumn(name = "domain_label") + joinColumns = @JoinColumn(name = "revisionId", referencedColumnName = "revisionId")) + @MapKeyColumn(name = "domainLabel") @Column(name = "price", nullable = false) private Map labelsToPrices; - private PremiumList( - ZonedDateTime creationTimestamp, - CurrencyUnit currency, - Map labelsToPrices) { - this.creationTimestamp = creationTimestamp; + private PremiumList(String name, CurrencyUnit currency, Map labelsToPrices) { + // TODO(mcilwain): Generate the Bloom filter and set it here. + this.name = name; this.currency = currency; this.labelsToPrices = labelsToPrices; } @@ -74,13 +80,15 @@ public class PremiumList { // Hibernate requires this default constructor. private PremiumList() {} - // TODO(mcilwain): Change creationTimestamp to Joda DateTime. /** Constructs a {@link PremiumList} object. */ public static PremiumList create( - ZonedDateTime creationTimestamp, - CurrencyUnit currency, - Map labelsToPrices) { - return new PremiumList(creationTimestamp, currency, labelsToPrices); + String name, CurrencyUnit currency, Map labelsToPrices) { + return new PremiumList(name, currency, labelsToPrices); + } + + /** Returns the name of the premium list, which is usually also a TLD string. */ + public String getName() { + return name; } /** Returns the ID of this revision, or throws if null. */ @@ -91,8 +99,8 @@ public class PremiumList { } /** Returns the creation time of this revision of the premium list. */ - public ZonedDateTime getCreationTimestamp() { - return creationTimestamp; + public DateTime getCreationTimestamp() { + return creationTimestamp.getTimestamp(); } /** Returns a {@link Map} of domain labels to prices. */ diff --git a/core/src/main/java/google/registry/schema/tld/PremiumListDao.java b/core/src/main/java/google/registry/schema/tld/PremiumListDao.java new file mode 100644 index 000000000..8392ea76a --- /dev/null +++ b/core/src/main/java/google/registry/schema/tld/PremiumListDao.java @@ -0,0 +1,56 @@ +// 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.base.Preconditions.checkArgument; +import static google.registry.model.transaction.TransactionManagerFactory.jpaTm; + +/** Data access object class for {@link PremiumList}. */ +public class PremiumListDao { + + /** Persist a new premium list to Cloud SQL. */ + public static void saveNew(PremiumList premiumList) { + jpaTm() + .transact( + () -> { + checkArgument( + !checkExists(premiumList.getName()), + "A premium list of this name already exists: %s.", + premiumList.getName()); + jpaTm().getEntityManager().persist(premiumList); + }); + } + + /** + * Returns whether the premium list of the given name exists. + * + *

This means that at least one premium list revision must exist for the given name. + */ + public static boolean checkExists(String premiumListName) { + return jpaTm() + .transact( + () -> + jpaTm() + .getEntityManager() + .createQuery("SELECT 1 FROM PremiumList WHERE name = :name", Integer.class) + .setParameter("name", premiumListName) + .setMaxResults(1) + .getResultList() + .size() + > 0); + } + + private PremiumListDao() {} +} diff --git a/core/src/main/java/google/registry/tools/CreateOrUpdatePremiumListCommand.java b/core/src/main/java/google/registry/tools/CreateOrUpdatePremiumListCommand.java index 069bbfc68..7524422ce 100644 --- a/core/src/main/java/google/registry/tools/CreateOrUpdatePremiumListCommand.java +++ b/core/src/main/java/google/registry/tools/CreateOrUpdatePremiumListCommand.java @@ -16,6 +16,7 @@ package google.registry.tools; import static com.google.common.base.Strings.isNullOrEmpty; import static google.registry.security.JsonHttp.JSON_SAFETY_PREFIX; +import static google.registry.tools.server.CreateOrUpdatePremiumListAction.ALSO_CLOUD_SQL_PARAM; import static google.registry.tools.server.CreateOrUpdatePremiumListAction.INPUT_PARAM; import static google.registry.tools.server.CreateOrUpdatePremiumListAction.NAME_PARAM; import static google.registry.util.ListNamingUtils.convertFilePathToName; @@ -57,6 +58,12 @@ abstract class CreateOrUpdatePremiumListCommand extends ConfirmingCommand required = true) Path inputFile; + @Parameter( + names = {"--also_cloud_sql"}, + description = + "Persist premium list to Cloud SQL in addition to Datastore; defaults to false.") + boolean alsoCloudSql; + protected AppEngineConnection connection; protected int inputLineCount; @@ -67,7 +74,7 @@ abstract class CreateOrUpdatePremiumListCommand extends ConfirmingCommand abstract String getCommandPath(); - ImmutableMap getParameterMap() { + ImmutableMap getParameterMap() { return ImmutableMap.of(); } @@ -88,14 +95,15 @@ abstract class CreateOrUpdatePremiumListCommand extends ConfirmingCommand @Override public String execute() throws Exception { - ImmutableMap.Builder params = new ImmutableMap.Builder<>(); + ImmutableMap.Builder params = new ImmutableMap.Builder<>(); params.put(NAME_PARAM, name); + params.put(ALSO_CLOUD_SQL_PARAM, Boolean.toString(alsoCloudSql)); String inputFileContents = new String(Files.readAllBytes(inputFile), UTF_8); String requestBody = Joiner.on('&').withKeyValueSeparator("=").join( ImmutableMap.of(INPUT_PARAM, URLEncoder.encode(inputFileContents, UTF_8.toString()))); - ImmutableMap extraParams = getParameterMap(); + ImmutableMap extraParams = getParameterMap(); if (extraParams != null) { params.putAll(extraParams); } @@ -110,7 +118,7 @@ abstract class CreateOrUpdatePremiumListCommand extends ConfirmingCommand // TODO(user): refactor this behavior into a better general-purpose // response validation that can be re-used across the new client/server commands. - String extractServerResponse(String response) { + private String extractServerResponse(String response) { Map responseMap = toMap(JSONValue.parse(stripJsonPrefix(response))); // TODO(user): consider using jart's FormField Framework. @@ -127,7 +135,7 @@ abstract class CreateOrUpdatePremiumListCommand extends ConfirmingCommand } // TODO(user): figure out better place to put this method to make it re-usable - static String stripJsonPrefix(String json) { + private static String stripJsonPrefix(String json) { Verify.verify(json.startsWith(JSON_SAFETY_PREFIX)); return json.substring(JSON_SAFETY_PREFIX.length()); } diff --git a/core/src/main/java/google/registry/tools/CreatePremiumListCommand.java b/core/src/main/java/google/registry/tools/CreatePremiumListCommand.java index 449525633..3eb0a85e6 100644 --- a/core/src/main/java/google/registry/tools/CreatePremiumListCommand.java +++ b/core/src/main/java/google/registry/tools/CreatePremiumListCommand.java @@ -36,12 +36,11 @@ public class CreatePremiumListCommand extends CreateOrUpdatePremiumListCommand { } @Override - ImmutableMap getParameterMap() { + ImmutableMap getParameterMap() { if (override) { - return ImmutableMap.of("override", override); + return ImmutableMap.of("override", "true"); } else { return ImmutableMap.of(); } } - } diff --git a/core/src/main/java/google/registry/tools/server/CreateOrUpdatePremiumListAction.java b/core/src/main/java/google/registry/tools/server/CreateOrUpdatePremiumListAction.java index 1e0957126..f67b3440d 100644 --- a/core/src/main/java/google/registry/tools/server/CreateOrUpdatePremiumListAction.java +++ b/core/src/main/java/google/registry/tools/server/CreateOrUpdatePremiumListAction.java @@ -14,17 +14,28 @@ package google.registry.tools.server; +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.collect.ImmutableSet.toImmutableSet; import static com.google.common.flogger.LazyArgs.lazy; +import com.google.common.base.Splitter; import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSet; +import com.google.common.collect.ImmutableSortedSet; +import com.google.common.collect.Iterables; +import com.google.common.collect.Maps; import com.google.common.flogger.FluentLogger; +import google.registry.model.registry.label.PremiumList; +import google.registry.model.registry.label.PremiumList.PremiumListEntry; import google.registry.request.JsonResponse; import google.registry.request.Parameter; +import java.math.BigDecimal; +import java.util.List; +import java.util.Map; import javax.inject.Inject; +import org.joda.money.CurrencyUnit; -/** - * Abstract base class for actions that update premium lists. - */ +/** Abstract base class for actions that update premium lists. */ public abstract class CreateOrUpdatePremiumListAction implements Runnable { private static final FluentLogger logger = FluentLogger.forEnclosingClass(); @@ -33,24 +44,70 @@ public abstract class CreateOrUpdatePremiumListAction implements Runnable { public static final String NAME_PARAM = "name"; public static final String INPUT_PARAM = "inputData"; + public static final String ALSO_CLOUD_SQL_PARAM = "alsoCloudSql"; @Inject JsonResponse response; - @Inject @Parameter("premiumListName") String name; - @Inject @Parameter(INPUT_PARAM) String inputData; + + @Inject + @Parameter("premiumListName") + String name; + + @Inject + @Parameter(INPUT_PARAM) + String inputData; + + @Inject + @Parameter(ALSO_CLOUD_SQL_PARAM) + boolean alsoCloudSql; @Override public void run() { try { - savePremiumList(); + saveToDatastore(); } catch (IllegalArgumentException e) { logger.atInfo().withCause(e).log( "Usage error in attempting to save premium list from nomulus tool command"); response.setPayload(ImmutableMap.of("error", e.toString(), "status", "error")); + return; } catch (Exception e) { logger.atSevere().withCause(e).log( - "Unexpected error saving premium list from nomulus tool command"); + "Unexpected error saving premium list to Datastore from nomulus tool command"); response.setPayload(ImmutableMap.of("error", e.toString(), "status", "error")); + return; } + + if (alsoCloudSql) { + try { + saveToCloudSql(); + } catch (Throwable e) { + logger.atSevere().withCause(e).log( + "Unexpected error saving premium list to Cloud SQL from nomulus tool command"); + response.setPayload(ImmutableMap.of("error", e.toString(), "status", "error")); + return; + } + } + } + + google.registry.schema.tld.PremiumList parseInputToPremiumList() { + List inputDataPreProcessed = + Splitter.on('\n').omitEmptyStrings().splitToList(inputData); + + ImmutableMap prices = + new PremiumList.Builder().setName(name).build().parse(inputDataPreProcessed); + ImmutableSet currencies = + prices.values().stream() + .map(e -> e.getValue().getCurrencyUnit()) + .distinct() + .collect(toImmutableSet()); + checkArgument( + currencies.size() == 1, + "The Cloud SQL schema requires exactly one currency, but got: %s", + ImmutableSortedSet.copyOf(currencies)); + CurrencyUnit currency = Iterables.getOnlyElement(currencies); + + Map priceAmounts = + Maps.transformValues(prices, ple -> ple.getValue().getAmount()); + return google.registry.schema.tld.PremiumList.create(name, currency, priceAmounts); } /** Logs the premium list data at INFO, truncated if too long. */ @@ -64,6 +121,9 @@ public abstract class CreateOrUpdatePremiumListAction implements Runnable { : (inputData.substring(0, MAX_LOGGING_PREMIUM_LIST_LENGTH) + ""))); } - /** Creates a new premium list or updates an existing one. */ - protected abstract void savePremiumList(); + /** Saves the premium list to Datastore. */ + protected abstract void saveToDatastore(); + + /** Saves the premium list to Cloud SQL. */ + protected abstract void saveToCloudSql(); } diff --git a/core/src/main/java/google/registry/tools/server/CreatePremiumListAction.java b/core/src/main/java/google/registry/tools/server/CreatePremiumListAction.java index 13663f2d3..f60b8311f 100644 --- a/core/src/main/java/google/registry/tools/server/CreatePremiumListAction.java +++ b/core/src/main/java/google/registry/tools/server/CreatePremiumListAction.java @@ -27,6 +27,7 @@ import google.registry.model.registry.label.PremiumList; import google.registry.request.Action; import google.registry.request.Parameter; import google.registry.request.auth.Auth; +import google.registry.schema.tld.PremiumListDao; import java.util.List; import javax.inject.Inject; @@ -50,7 +51,7 @@ public class CreatePremiumListAction extends CreateOrUpdatePremiumListAction { @Inject CreatePremiumListAction() {} @Override - protected void savePremiumList() { + protected void saveToDatastore() { checkArgument( !doesPremiumListExist(name), "A premium list of this name already exists: %s.", name); if (!override) { @@ -71,4 +72,22 @@ public class CreatePremiumListAction extends CreateOrUpdatePremiumListAction { logger.atInfo().log(message); response.setPayload(ImmutableMap.of("status", "success", "message", message)); } + + @Override + protected void saveToCloudSql() { + if (!override) { + assertTldExists(name); + } + logger.atInfo().log("Saving premium list to Cloud SQL for TLD %s", name); + // TODO(mcilwain): Call logInputData() here once Datastore persistence is removed. + + google.registry.schema.tld.PremiumList premiumList = parseInputToPremiumList(); + PremiumListDao.saveNew(premiumList); + + String message = + String.format( + "Saved premium list %s with %d entries", name, premiumList.getLabelsToPrices().size()); + logger.atInfo().log(message); + // TODO(mcilwain): Call response.setPayload(...) here once Datastore persistence is removed. + } } diff --git a/core/src/main/java/google/registry/tools/server/ToolsServerModule.java b/core/src/main/java/google/registry/tools/server/ToolsServerModule.java index 778bea0fa..2ce413048 100644 --- a/core/src/main/java/google/registry/tools/server/ToolsServerModule.java +++ b/core/src/main/java/google/registry/tools/server/ToolsServerModule.java @@ -60,6 +60,12 @@ public class ToolsServerModule { return extractRequiredParameter(req, CreatePremiumListAction.INPUT_PARAM); } + @Provides + @Parameter("alsoCloudSql") + static boolean provideAlsoCloudSql(HttpServletRequest req) { + return extractBooleanParameter(req, CreatePremiumListAction.ALSO_CLOUD_SQL_PARAM); + } + @Provides @Parameter("premiumListName") static String provideName(HttpServletRequest req) { diff --git a/core/src/main/java/google/registry/tools/server/UpdatePremiumListAction.java b/core/src/main/java/google/registry/tools/server/UpdatePremiumListAction.java index 566606920..ec87e6301 100644 --- a/core/src/main/java/google/registry/tools/server/UpdatePremiumListAction.java +++ b/core/src/main/java/google/registry/tools/server/UpdatePremiumListAction.java @@ -46,7 +46,7 @@ public class UpdatePremiumListAction extends CreateOrUpdatePremiumListAction { @Inject UpdatePremiumListAction() {} @Override - protected void savePremiumList() { + protected void saveToDatastore() { Optional existingPremiumList = PremiumList.getUncached(name); checkArgument( existingPremiumList.isPresent(), @@ -67,4 +67,11 @@ public class UpdatePremiumListAction extends CreateOrUpdatePremiumListAction { logger.atInfo().log(message); response.setPayload(ImmutableMap.of("status", "success", "message", message)); } + + // TODO(mcilwain): Implement this in a subsequent PR. + @Override + protected void saveToCloudSql() { + throw new UnsupportedOperationException( + "Updating of premium lists in Cloud SQL is not supported yet"); + } } diff --git a/core/src/test/java/google/registry/schema/tld/PremiumListDaoTest.java b/core/src/test/java/google/registry/schema/tld/PremiumListDaoTest.java new file mode 100644 index 000000000..f08689e69 --- /dev/null +++ b/core/src/test/java/google/registry/schema/tld/PremiumListDaoTest.java @@ -0,0 +1,89 @@ +// 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 static google.registry.testing.JUnitBackports.assertThrows; + +import com.google.common.collect.ImmutableMap; +import google.registry.model.transaction.JpaTransactionManagerRule; +import java.math.BigDecimal; +import javax.persistence.PersistenceException; +import org.joda.money.CurrencyUnit; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +/** Unit tests for {@link PremiumListDao}. */ +@RunWith(JUnit4.class) +public class PremiumListDaoTest { + + @Rule + public final JpaTransactionManagerRule jpaTmRule = + new JpaTransactionManagerRule.Builder().withEntityClass(PremiumList.class).build(); + + private static final ImmutableMap TEST_PRICES = + ImmutableMap.of( + "silver", + BigDecimal.valueOf(10.23), + "gold", + BigDecimal.valueOf(1305.47), + "palladium", + BigDecimal.valueOf(1552.78)); + + @Test + public void saveNew_worksSuccessfully() { + PremiumList premiumList = PremiumList.create("testname", CurrencyUnit.USD, TEST_PRICES); + PremiumListDao.saveNew(premiumList); + jpaTm() + .transact( + () -> { + PremiumList persistedList = + jpaTm() + .getEntityManager() + .createQuery( + "SELECT pl FROM PremiumList pl WHERE pl.name = :name", PremiumList.class) + .setParameter("name", "testname") + .getSingleResult(); + assertThat(persistedList.getLabelsToPrices()).containsExactlyEntriesIn(TEST_PRICES); + assertThat(persistedList.getCreationTimestamp()) + .isEqualTo(jpaTmRule.getTxnClock().nowUtc()); + }); + } + + @Test + public void saveNew_throwsWhenPremiumListAlreadyExists() { + PremiumListDao.saveNew(PremiumList.create("testlist", CurrencyUnit.USD, TEST_PRICES)); + PersistenceException thrown = + assertThrows( + PersistenceException.class, + () -> + PremiumListDao.saveNew( + PremiumList.create("testlist", CurrencyUnit.USD, TEST_PRICES))); + assertThat(thrown) + .hasCauseThat() + .hasMessageThat() + .contains("A premium list of this name already exists"); + } + + @Test + public void checkExists_worksSuccessfully() { + assertThat(PremiumListDao.checkExists("testlist")).isFalse(); + PremiumListDao.saveNew(PremiumList.create("testlist", CurrencyUnit.USD, TEST_PRICES)); + assertThat(PremiumListDao.checkExists("testlist")).isTrue(); + } +} diff --git a/core/src/test/java/google/registry/tools/CreatePremiumListCommandTest.java b/core/src/test/java/google/registry/tools/CreatePremiumListCommandTest.java index 0f587c8a0..6a45785b7 100644 --- a/core/src/test/java/google/registry/tools/CreatePremiumListCommandTest.java +++ b/core/src/test/java/google/registry/tools/CreatePremiumListCommandTest.java @@ -67,7 +67,13 @@ public class CreatePremiumListCommandTest verifySentParams( connection, servletPath, - ImmutableMap.of("name", "foo", "inputData", generateInputData(premiumTermsPath))); + ImmutableMap.of( + "name", + "foo", + "inputData", + generateInputData(premiumTermsPath), + "alsoCloudSql", + "false")); } @Test @@ -78,7 +84,28 @@ public class CreatePremiumListCommandTest connection, servletPath, ImmutableMap.of( - "name", "example_premium_terms", "inputData", generateInputData(premiumTermsPath))); + "name", + "example_premium_terms", + "inputData", + generateInputData(premiumTermsPath), + "alsoCloudSql", + "false")); + } + + @Test + public void testRun_alsoCloudSql() throws Exception { + runCommandForced("-i=" + premiumTermsPath, "-n=foo", "--also_cloud_sql"); + assertInStdout("Successfully"); + verifySentParams( + connection, + servletPath, + ImmutableMap.of( + "name", + "foo", + "inputData", + generateInputData(premiumTermsPath), + "alsoCloudSql", + "true")); } @Test diff --git a/core/src/test/java/google/registry/tools/UpdatePremiumListCommandTest.java b/core/src/test/java/google/registry/tools/UpdatePremiumListCommandTest.java index 376b96239..d0d5c89ad 100644 --- a/core/src/test/java/google/registry/tools/UpdatePremiumListCommandTest.java +++ b/core/src/test/java/google/registry/tools/UpdatePremiumListCommandTest.java @@ -57,7 +57,13 @@ public class UpdatePremiumListCommandTest verifySentParams( connection, servletPath, - ImmutableMap.of("name", "foo", "inputData", generateInputData(premiumTermsPath))); + ImmutableMap.of( + "name", + "foo", + "inputData", + generateInputData(premiumTermsPath), + "alsoCloudSql", + "false")); } @Test @@ -68,6 +74,11 @@ public class UpdatePremiumListCommandTest connection, servletPath, ImmutableMap.of( - "name", "example_premium_terms", "inputData", generateInputData(premiumTermsPath))); + "name", + "example_premium_terms", + "inputData", + generateInputData(premiumTermsPath), + "alsoCloudSql", + "false")); } } diff --git a/core/src/test/java/google/registry/tools/server/CreateOrUpdatePremiumListActionTest.java b/core/src/test/java/google/registry/tools/server/CreateOrUpdatePremiumListActionTest.java new file mode 100644 index 000000000..456cdd16c --- /dev/null +++ b/core/src/test/java/google/registry/tools/server/CreateOrUpdatePremiumListActionTest.java @@ -0,0 +1,70 @@ +// Copyright 2017 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.server; + +import static com.google.common.truth.Truth.assertThat; +import static com.google.common.truth.Truth8.assertThat; +import static google.registry.testing.JUnitBackports.assertThrows; + +import google.registry.schema.tld.PremiumList; +import google.registry.testing.AppEngineRule; +import google.registry.testing.FakeJsonResponse; +import java.math.BigDecimal; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +/** Unit tests for {@link CreateOrUpdatePremiumListAction}. */ +@RunWith(JUnit4.class) +public class CreateOrUpdatePremiumListActionTest { + + @Rule public final AppEngineRule appEngine = AppEngineRule.builder().withDatastore().build(); + + private CreatePremiumListAction action; + private FakeJsonResponse response; + + @Before + public void init() { + action = new CreatePremiumListAction(); + response = new FakeJsonResponse(); + action.response = response; + action.name = "testlist"; + } + + @Test + public void parseInputToPremiumList_works() { + action.inputData = "foo,USD 99.50\n" + "bar,USD 30\n" + "baz,USD 10\n"; + PremiumList premiumList = action.parseInputToPremiumList(); + assertThat(premiumList.getName()).isEqualTo("testlist"); + assertThat(premiumList.getLabelsToPrices()) + .containsExactly("foo", twoDigits(99.50), "bar", twoDigits(30), "baz", twoDigits(10)); + } + + @Test + public void parseInputToPremiumList_throwsOnInconsistentCurrencies() { + action.inputData = "foo,USD 99.50\n" + "bar,USD 30\n" + "baz,JPY 990\n"; + IllegalArgumentException thrown = + assertThrows(IllegalArgumentException.class, () -> action.parseInputToPremiumList()); + assertThat(thrown) + .hasMessageThat() + .isEqualTo("The Cloud SQL schema requires exactly one currency, but got: [JPY, USD]"); + } + + private static BigDecimal twoDigits(double num) { + return BigDecimal.valueOf((long) (num * 100.0), 2); + } +} diff --git a/db/src/main/resources/sql/flyway/V5__update_premium_list.sql b/db/src/main/resources/sql/flyway/V5__update_premium_list.sql new file mode 100644 index 000000000..02be595b7 --- /dev/null +++ b/db/src/main/resources/sql/flyway/V5__update_premium_list.sql @@ -0,0 +1,17 @@ +-- 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. + +alter table "PremiumList" add column if not exists name text not null; + +create index if not exists premiumlist_name_idx ON "PremiumList" (name); diff --git a/db/src/main/resources/sql/schema/db-schema.sql.generated b/db/src/main/resources/sql/schema/db-schema.sql.generated index 6f9ddbbbd..982ca1ea4 100644 --- a/db/src/main/resources/sql/schema/db-schema.sql.generated +++ b/db/src/main/resources/sql/schema/db-schema.sql.generated @@ -132,6 +132,7 @@ revision_id bigserial not null, creation_timestamp timestamptz not null, currency bytea not null, + name text not null, primary key (revision_id) ); @@ -157,6 +158,7 @@ alter table if exists "Domain_GracePeriod" add constraint UK_4ps2u4y8i5r91wu2n1x2xea28 unique (grace_periods_id); +create index premiumlist_name_idx on "PremiumList" (name); alter table if exists "RegistryLock" add constraint idx_registry_lock_repo_id_revision_id unique (repo_id, revision_id); diff --git a/db/src/main/resources/sql/schema/nomulus.golden.sql b/db/src/main/resources/sql/schema/nomulus.golden.sql index c20712d86..df7ec8e4c 100644 --- a/db/src/main/resources/sql/schema/nomulus.golden.sql +++ b/db/src/main/resources/sql/schema/nomulus.golden.sql @@ -91,7 +91,8 @@ CREATE TABLE public."PremiumEntry" ( CREATE TABLE public."PremiumList" ( revision_id bigint NOT NULL, creation_timestamp timestamp with time zone NOT NULL, - currency bytea NOT NULL + currency bytea NOT NULL, + name text NOT NULL ); @@ -227,6 +228,13 @@ ALTER TABLE ONLY public."RegistryLock" CREATE INDEX idx_registry_lock_verification_code ON public."RegistryLock" USING btree (verification_code); +-- +-- Name: premiumlist_name_idx; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX premiumlist_name_idx ON public."PremiumList" USING btree (name); + + -- -- Name: ClaimsEntry fk6sc6at5hedffc0nhdcab6ivuq; Type: FK CONSTRAINT; Schema: public; Owner: - --