diff --git a/core/src/main/java/google/registry/tools/EnqueuePollMessageCommand.java b/core/src/main/java/google/registry/tools/EnqueuePollMessageCommand.java new file mode 100644 index 000000000..ccfc68c09 --- /dev/null +++ b/core/src/main/java/google/registry/tools/EnqueuePollMessageCommand.java @@ -0,0 +1,95 @@ +// Copyright 2021 The Nomulus Authors. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package google.registry.tools; + +import static com.google.common.base.Preconditions.checkArgument; +import static google.registry.model.EppResourceUtils.loadByForeignKey; +import static google.registry.model.reporting.HistoryEntry.Type.SYNTHETIC; +import static google.registry.persistence.transaction.TransactionManagerFactory.tm; + +import com.beust.jcommander.Parameter; +import com.beust.jcommander.Parameters; +import google.registry.model.domain.DomainBase; +import google.registry.model.domain.DomainHistory; +import google.registry.model.poll.PollMessage; +import google.registry.model.reporting.HistoryEntry; +import java.util.Optional; + +/** + * Tool to enqueue a poll message for a registrar. + * + *

The poll message in question must correspond to an existing domain owing to schema + * limitations, but does not necessarily need to correspond to the owner of that domain if an + * alternative clientId is provided. + * + *

If general broadcast messages are being sent to registrars (e.g. a notice of an upcoming + * maintenance window), then it is recommended to use a well-known registry-owned domain for the + * enqueueing of these poll messages, such as the nic domain. + */ +@Parameters(separators = " =", commandDescription = "Enqueue a poll message for a domain") +class EnqueuePollMessageCommand extends MutatingCommand { + + @Parameter( + names = {"-m", "--message"}, + description = "The poll message to enqueue", + required = true) + String message; + + @Parameter( + names = {"-d", "--domain"}, + description = "The domain name to enqueue the poll message for", + required = true) + String domainName; + + @Parameter( + names = {"-c", "--client"}, + description = + "Client identifier of the registrar to send the poll message to, if not the owning" + + " registrar of the domain") + String clientId; + + @Override + protected final void init() { + tm().transact( + () -> { + Optional domainOpt = + loadByForeignKey(DomainBase.class, domainName, tm().getTransactionTime()); + checkArgument( + domainOpt.isPresent(), "Domain %s doesn't exist or isn't active", domainName); + DomainBase domain = domainOpt.get(); + String registrarId = + Optional.ofNullable(clientId).orElse(domain.getCurrentSponsorRegistrarId()); + HistoryEntry historyEntry = + new DomainHistory.Builder() + .setDomain(domain) + .setType(SYNTHETIC) + .setBySuperuser(true) + .setReason("Manual enqueueing of poll message") + .setModificationTime(tm().getTransactionTime()) + .setRequestedByRegistrar(false) + .setRegistrarId(registrarId) + .build(); + PollMessage.OneTime pollMessage = + new PollMessage.OneTime.Builder() + .setRegistrarId(registrarId) + .setParent(historyEntry) + .setEventTime(tm().getTransactionTime()) + .setMsg(message) + .build(); + stageEntityChange(null, historyEntry); + stageEntityChange(null, pollMessage); + }); + } +} diff --git a/core/src/main/java/google/registry/tools/MutatingCommand.java b/core/src/main/java/google/registry/tools/MutatingCommand.java index 10c1d6d17..d088c6f62 100644 --- a/core/src/main/java/google/registry/tools/MutatingCommand.java +++ b/core/src/main/java/google/registry/tools/MutatingCommand.java @@ -103,7 +103,8 @@ public abstract class MutatingCommand extends ConfirmingCommand implements Comma * workaround to handle cases when a SqlEntity instance does not have a primary key before being * persisted. */ - private EntityChange(ImmutableObject oldEntity, ImmutableObject newEntity, VKey vkey) { + private EntityChange( + @Nullable ImmutableObject oldEntity, @Nullable ImmutableObject newEntity, VKey vkey) { type = ChangeType.get(oldEntity != null, newEntity != null); if (type == ChangeType.UPDATE) { checkArgument( @@ -127,7 +128,7 @@ public abstract class MutatingCommand extends ConfirmingCommand implements Comma } /** Returns a human-readable ID string for the entity being changed. */ - public String getEntityId() { + String getEntityId() { return String.format( "%s@%s", key.getOfyKey().getKind(), diff --git a/core/src/main/java/google/registry/tools/RegistryTool.java b/core/src/main/java/google/registry/tools/RegistryTool.java index b7bd5886f..50ff73cbb 100644 --- a/core/src/main/java/google/registry/tools/RegistryTool.java +++ b/core/src/main/java/google/registry/tools/RegistryTool.java @@ -62,6 +62,7 @@ public final class RegistryTool { .put("delete_reserved_list", DeleteReservedListCommand.class) .put("delete_tld", DeleteTldCommand.class) .put("encrypt_escrow_deposit", EncryptEscrowDepositCommand.class) + .put("enqueue_poll_message", EnqueuePollMessageCommand.class) .put("execute_epp", ExecuteEppCommand.class) .put("generate_allocation_tokens", GenerateAllocationTokensCommand.class) .put("generate_dns_report", GenerateDnsReportCommand.class) diff --git a/core/src/main/java/google/registry/tools/RegistryToolComponent.java b/core/src/main/java/google/registry/tools/RegistryToolComponent.java index eb8f46278..ebb02a72b 100644 --- a/core/src/main/java/google/registry/tools/RegistryToolComponent.java +++ b/core/src/main/java/google/registry/tools/RegistryToolComponent.java @@ -111,6 +111,8 @@ interface RegistryToolComponent { void inject(EncryptEscrowDepositCommand command); + void inject(EnqueuePollMessageCommand command); + void inject(GenerateAllocationTokensCommand command); void inject(GenerateDnsReportCommand command); diff --git a/core/src/test/java/google/registry/tools/EnqueuePollMessageCommandTest.java b/core/src/test/java/google/registry/tools/EnqueuePollMessageCommandTest.java new file mode 100644 index 000000000..5c1817149 --- /dev/null +++ b/core/src/test/java/google/registry/tools/EnqueuePollMessageCommandTest.java @@ -0,0 +1,134 @@ +// Copyright 2021 The Nomulus Authors. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package google.registry.tools; + +import static com.google.common.truth.Truth.assertThat; +import static google.registry.model.reporting.HistoryEntry.Type.SYNTHETIC; +import static google.registry.testing.DatabaseHelper.assertPollMessages; +import static google.registry.testing.DatabaseHelper.createTld; +import static google.registry.testing.DatabaseHelper.getOnlyHistoryEntryOfType; +import static google.registry.testing.DatabaseHelper.persistActiveDomain; +import static google.registry.testing.HistoryEntrySubject.assertAboutHistoryEntries; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import com.beust.jcommander.ParameterException; +import google.registry.model.domain.DomainBase; +import google.registry.model.ofy.Ofy; +import google.registry.model.poll.PollMessage; +import google.registry.model.reporting.HistoryEntry; +import google.registry.testing.DualDatabaseTest; +import google.registry.testing.InjectExtension; +import google.registry.testing.TestOfyAndSql; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.extension.RegisterExtension; + +/** Unit tests for {@link EnqueuePollMessageCommand}. */ +@DualDatabaseTest +class EnqueuePollMessageCommandTest extends CommandTestCase { + + @RegisterExtension final InjectExtension inject = new InjectExtension(); + + private DomainBase domain; + + @BeforeEach + void beforeEach() { + createTld("tld"); + inject.setStaticField(Ofy.class, "clock", fakeClock); + domain = persistActiveDomain("example.tld"); + fakeClock.advanceOneMilli(); + } + + @TestOfyAndSql + void testSuccess_domainAndMessage() throws Exception { + runCommandForced("--domain=example.tld", "--message=This domain is bad"); + + HistoryEntry synthetic = getOnlyHistoryEntryOfType(domain, SYNTHETIC); + assertAboutHistoryEntries() + .that(synthetic) + .bySuperuser(true) + .and() + .hasMetadataReason("Manual enqueueing of poll message") + .and() + .hasNoXml() + .and() + .hasRegistrarId("TheRegistrar") + .and() + .hasModificationTime(fakeClock.nowUtc()) + .and() + .hasMetadataRequestedByRegistrar(false); + assertPollMessages( + "TheRegistrar", + new PollMessage.OneTime.Builder() + .setParent(synthetic) + .setMsg("This domain is bad") + .setRegistrarId("TheRegistrar") + .setEventTime(fakeClock.nowUtc()) + .build()); + } + + @TestOfyAndSql + void testSuccess_specifyClientId() throws Exception { + runCommandForced( + "--domain=example.tld", "--message=This domain needs work", "--client=NewRegistrar"); + + HistoryEntry synthetic = getOnlyHistoryEntryOfType(domain, SYNTHETIC); + assertAboutHistoryEntries() + .that(synthetic) + .bySuperuser(true) + .and() + .hasMetadataReason("Manual enqueueing of poll message") + .and() + .hasNoXml() + .and() + .hasRegistrarId("NewRegistrar") + .and() + .hasModificationTime(fakeClock.nowUtc()) + .and() + .hasMetadataRequestedByRegistrar(false); + assertPollMessages( + "NewRegistrar", + new PollMessage.OneTime.Builder() + .setParent(synthetic) + .setMsg("This domain needs work") + .setRegistrarId("NewRegistrar") + .setEventTime(fakeClock.nowUtc()) + .build()); + } + + @TestOfyAndSql + void testNonexistentDomain() throws Exception { + IllegalArgumentException thrown = + assertThrows( + IllegalArgumentException.class, + () -> runCommandForced("--domain=example2.tld", "--message=This domain needs help")); + assertThat(thrown) + .hasMessageThat() + .isEqualTo("Domain example2.tld doesn't exist or isn't active"); + } + + @TestOfyAndSql + void testDomainIsRequired() { + ParameterException thrown = + assertThrows(ParameterException.class, () -> runCommandForced("--message=Foo bar")); + assertThat(thrown).hasMessageThat().contains("The following option is required: -d, --domain"); + } + + @TestOfyAndSql + void testMessageIsRequired() { + ParameterException thrown = + assertThrows(ParameterException.class, () -> runCommandForced("--domain=example.tld")); + assertThat(thrown).hasMessageThat().contains("The following option is required: -m, --message"); + } +}