Add a nomulus tool command to enqueue a poll message (#1441)

* Add a nomulus tool command to enqueue a poll message
This commit is contained in:
Ben McIlwain 2021-12-02 13:06:22 -05:00 committed by GitHub
parent 9275a4f078
commit 18d051ef87
5 changed files with 235 additions and 2 deletions

View file

@ -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.
*
* <p>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 <code>clientId</code> is provided.
*
* <p>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<DomainBase> 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);
});
}
}

View file

@ -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(),

View file

@ -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)

View file

@ -111,6 +111,8 @@ interface RegistryToolComponent {
void inject(EncryptEscrowDepositCommand command);
void inject(EnqueuePollMessageCommand command);
void inject(GenerateAllocationTokensCommand command);
void inject(GenerateDnsReportCommand command);

View file

@ -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<EnqueuePollMessageCommand> {
@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");
}
}