frontend-servlet
/registry-lock-verify
diff --git a/core/src/main/java/google/registry/module/frontend/FrontendRequestComponent.java b/core/src/main/java/google/registry/module/frontend/FrontendRequestComponent.java
index 75b1632dc..9dd327526 100644
--- a/core/src/main/java/google/registry/module/frontend/FrontendRequestComponent.java
+++ b/core/src/main/java/google/registry/module/frontend/FrontendRequestComponent.java
@@ -31,6 +31,7 @@ import google.registry.ui.server.registrar.OteStatusAction;
import google.registry.ui.server.registrar.RegistrarConsoleModule;
import google.registry.ui.server.registrar.RegistrarSettingsAction;
import google.registry.ui.server.registrar.RegistryLockGetAction;
+import google.registry.ui.server.registrar.RegistryLockPostAction;
import google.registry.ui.server.registrar.RegistryLockVerifyAction;
/** Dagger component with per-request lifetime for "default" App Engine module. */
@@ -54,6 +55,8 @@ interface FrontendRequestComponent {
RegistryLockGetAction registryLockGetAction();
+ RegistryLockPostAction registryLockPostAction();
+
RegistryLockVerifyAction registryLockVerifyAction();
@Subcomponent.Builder
diff --git a/core/src/main/java/google/registry/ui/server/registrar/RegistryLockPostAction.java b/core/src/main/java/google/registry/ui/server/registrar/RegistryLockPostAction.java
new file mode 100644
index 000000000..79c03579e
--- /dev/null
+++ b/core/src/main/java/google/registry/ui/server/registrar/RegistryLockPostAction.java
@@ -0,0 +1,214 @@
+// Copyright 2020 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.ui.server.registrar;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.base.Preconditions.checkNotNull;
+import static google.registry.persistence.transaction.TransactionManagerFactory.jpaTm;
+import static google.registry.security.JsonResponseHelper.Status.ERROR;
+import static google.registry.security.JsonResponseHelper.Status.SUCCESS;
+import static google.registry.ui.server.registrar.RegistrarConsoleModule.PARAM_CLIENT_ID;
+import static google.registry.util.PreconditionsUtils.checkArgumentNotNull;
+import static google.registry.util.PreconditionsUtils.checkArgumentPresent;
+
+import com.google.common.base.Strings;
+import com.google.common.base.Throwables;
+import com.google.common.collect.ImmutableList;
+import com.google.common.flogger.FluentLogger;
+import com.google.gson.Gson;
+import google.registry.config.RegistryConfig;
+import google.registry.config.RegistryConfig.Config;
+import google.registry.model.registrar.Registrar;
+import google.registry.model.registrar.RegistrarContact;
+import google.registry.request.Action;
+import google.registry.request.Action.Method;
+import google.registry.request.JsonActionRunner;
+import google.registry.request.auth.Auth;
+import google.registry.request.auth.AuthResult;
+import google.registry.request.auth.AuthenticatedRegistrarAccessor;
+import google.registry.request.auth.AuthenticatedRegistrarAccessor.RegistrarAccessDeniedException;
+import google.registry.request.auth.UserAuthInfo;
+import google.registry.schema.domain.RegistryLock;
+import google.registry.security.JsonResponseHelper;
+import google.registry.tools.DomainLockUtils;
+import google.registry.util.Clock;
+import google.registry.util.EmailMessage;
+import google.registry.util.SendEmailService;
+import java.net.URISyntaxException;
+import java.net.URL;
+import java.util.Map;
+import java.util.Optional;
+import javax.inject.Inject;
+import javax.mail.internet.AddressException;
+import javax.mail.internet.InternetAddress;
+import org.apache.http.client.utils.URIBuilder;
+
+/**
+ * UI action that allows for creating registry locks. Locks / unlocks must be verified separately
+ * before they are written permanently.
+ *
+ * Note: at the moment we have no mechanism for JSON GET/POSTs in the same class or at the same
+ * URL, which is why this is distinct from the {@link RegistryLockGetAction}.
+ */
+@Action(
+ service = Action.Service.DEFAULT,
+ path = RegistryLockPostAction.PATH,
+ method = Method.POST,
+ auth = Auth.AUTH_PUBLIC_LOGGED_IN)
+public class RegistryLockPostAction implements Runnable, JsonActionRunner.JsonAction {
+ public static final String PATH = "/registry-lock-post";
+
+ private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+ private static final Gson GSON = new Gson();
+
+ private static final URL URL_BASE = RegistryConfig.getDefaultServer();
+ private static final String VERIFICATION_EMAIL_TEMPLATE =
+ "Please click the link below to perform the lock / unlock action on domain %s. Note: "
+ + "this code will expire in one hour.\n\n%s";
+
+ private final JsonActionRunner jsonActionRunner;
+ private final AuthResult authResult;
+ private final AuthenticatedRegistrarAccessor registrarAccessor;
+ private final SendEmailService sendEmailService;
+ private final Clock clock;
+ private final InternetAddress gSuiteOutgoingEmailAddress;
+
+ @Inject
+ RegistryLockPostAction(
+ JsonActionRunner jsonActionRunner,
+ AuthResult authResult,
+ AuthenticatedRegistrarAccessor registrarAccessor,
+ SendEmailService sendEmailService,
+ Clock clock,
+ @Config("gSuiteOutgoingEmailAddress") InternetAddress gSuiteOutgoingEmailAddress) {
+ this.jsonActionRunner = jsonActionRunner;
+ this.authResult = authResult;
+ this.registrarAccessor = registrarAccessor;
+ this.sendEmailService = sendEmailService;
+ this.clock = clock;
+ this.gSuiteOutgoingEmailAddress = gSuiteOutgoingEmailAddress;
+ }
+
+ @Override
+ public void run() {
+ jsonActionRunner.run(this);
+ }
+
+ @Override
+ public Map handleJsonRequest(Map input) {
+ try {
+ checkArgumentNotNull(input, "Null JSON");
+ RegistryLockPostInput postInput =
+ GSON.fromJson(GSON.toJsonTree(input), RegistryLockPostInput.class);
+ checkArgument(
+ !Strings.isNullOrEmpty(postInput.clientId),
+ "Missing key for client: %s",
+ PARAM_CLIENT_ID);
+ checkArgument(
+ !Strings.isNullOrEmpty(postInput.fullyQualifiedDomainName),
+ "Missing key for fullyQualifiedDomainName");
+ checkNotNull(postInput.isLock, "Missing key for isLock");
+ checkArgumentPresent(authResult.userAuthInfo(), "User is not logged in");
+
+ boolean isAdmin = authResult.userAuthInfo().get().isUserAdmin();
+ verifyRegistryLockPassword(postInput);
+ jpaTm()
+ .transact(
+ () -> {
+ RegistryLock registryLock =
+ postInput.isLock
+ ? DomainLockUtils.createRegistryLockRequest(
+ postInput.fullyQualifiedDomainName,
+ postInput.clientId,
+ postInput.pocId,
+ isAdmin,
+ clock)
+ : DomainLockUtils.createRegistryUnlockRequest(
+ postInput.fullyQualifiedDomainName, postInput.clientId, isAdmin, clock);
+ sendVerificationEmail(registryLock, postInput.isLock);
+ });
+ String action = postInput.isLock ? "lock" : "unlock";
+ return JsonResponseHelper.create(SUCCESS, String.format("Successful %s", action));
+ } catch (Throwable e) {
+ logger.atWarning().withCause(e).log("Failed to lock/unlock domain with input %s", input);
+ return JsonResponseHelper.create(
+ ERROR,
+ Optional.ofNullable(Throwables.getRootCause(e).getMessage()).orElse("Unspecified error"));
+ }
+ }
+
+ private void sendVerificationEmail(RegistryLock lock, boolean isLock) {
+ try {
+ String url =
+ new URIBuilder()
+ .setScheme("https")
+ .setHost(URL_BASE.getHost())
+ .setPath("registry-lock-verify")
+ .setParameter("lockVerificationCode", lock.getVerificationCode())
+ .setParameter("isLock", String.valueOf(isLock))
+ .build()
+ .toString();
+ String body = String.format(VERIFICATION_EMAIL_TEMPLATE, lock.getDomainName(), url);
+ ImmutableList recipients =
+ ImmutableList.of(
+ new InternetAddress(authResult.userAuthInfo().get().user().getEmail(), true));
+ String action = isLock ? "lock" : "unlock";
+ sendEmailService.sendEmail(
+ EmailMessage.newBuilder()
+ .setBody(body)
+ .setSubject(String.format("Registry %s verification", action))
+ .setRecipients(recipients)
+ .setFrom(gSuiteOutgoingEmailAddress)
+ .build());
+ } catch (AddressException | URISyntaxException e) {
+ throw new RuntimeException(e); // caught above -- this is so we can run in a transaction
+ }
+ }
+
+ private void verifyRegistryLockPassword(RegistryLockPostInput postInput)
+ throws RegistrarAccessDeniedException {
+ // Verify that the user can access the registrar and that the user is either an admin or has
+ // registry lock enabled and provided a correct password
+ checkArgument(authResult.userAuthInfo().isPresent(), "Auth result not present");
+ Registrar registrar = registrarAccessor.getRegistrar(postInput.clientId);
+ checkArgument(
+ registrar.isRegistryLockAllowed(), "Registry lock not allowed for this registrar");
+ UserAuthInfo userAuthInfo = authResult.userAuthInfo().get();
+ if (!userAuthInfo.isUserAdmin()) {
+ checkArgument(!Strings.isNullOrEmpty(postInput.pocId), "Missing key for pocId");
+ checkArgument(!Strings.isNullOrEmpty(postInput.password), "Missing key for password");
+ RegistrarContact registrarContact =
+ registrar.getContacts().stream()
+ .filter(contact -> contact.getEmailAddress().equals(postInput.pocId))
+ .findFirst()
+ .orElseThrow(
+ () ->
+ new IllegalArgumentException(
+ String.format("Unknown registrar POC ID %s", postInput.pocId)));
+ checkArgument(
+ registrarContact.verifyRegistryLockPassword(postInput.password),
+ "Incorrect registry lock password for contact");
+ }
+ }
+
+ /** Value class that represents the expected input body from the UI request. */
+ private static class RegistryLockPostInput {
+ private String clientId;
+ private String fullyQualifiedDomainName;
+ private Boolean isLock;
+ private String pocId;
+ private String password;
+ }
+}
diff --git a/core/src/test/java/google/registry/server/RegistryTestServer.java b/core/src/test/java/google/registry/server/RegistryTestServer.java
index 7214be456..dfe66f4ca 100644
--- a/core/src/test/java/google/registry/server/RegistryTestServer.java
+++ b/core/src/test/java/google/registry/server/RegistryTestServer.java
@@ -84,6 +84,7 @@ public final class RegistryTestServer {
route("/registrar-ote-status", FrontendServlet.class),
route("/registrar-settings", FrontendServlet.class),
route("/registry-lock-get", FrontendServlet.class),
+ route("/registry-lock-post", FrontendServlet.class),
route("/registry-lock-verify", FrontendServlet.class));
private static final ImmutableList> FILTERS = ImmutableList.of(
diff --git a/core/src/test/java/google/registry/ui/server/registrar/RegistryLockPostActionTest.java b/core/src/test/java/google/registry/ui/server/registrar/RegistryLockPostActionTest.java
new file mode 100644
index 000000000..3135c26d0
--- /dev/null
+++ b/core/src/test/java/google/registry/ui/server/registrar/RegistryLockPostActionTest.java
@@ -0,0 +1,408 @@
+// Copyright 2020 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.ui.server.registrar;
+
+import static com.google.common.truth.Truth.assertThat;
+import static google.registry.model.EppResourceUtils.loadByForeignKey;
+import static google.registry.testing.DatastoreHelper.createTld;
+import static google.registry.testing.DatastoreHelper.loadRegistrar;
+import static google.registry.testing.DatastoreHelper.newDomainBase;
+import static google.registry.testing.DatastoreHelper.persistResource;
+import static google.registry.tools.LockOrUnlockDomainCommand.REGISTRY_LOCK_STATUSES;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoMoreInteractions;
+
+import com.google.appengine.api.users.User;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSetMultimap;
+import google.registry.model.domain.DomainBase;
+import google.registry.model.registry.RegistryLockDao;
+import google.registry.persistence.transaction.JpaTestRules;
+import google.registry.persistence.transaction.JpaTestRules.JpaIntegrationTestRule;
+import google.registry.request.JsonActionRunner;
+import google.registry.request.JsonResponse;
+import google.registry.request.ResponseImpl;
+import google.registry.request.auth.AuthLevel;
+import google.registry.request.auth.AuthResult;
+import google.registry.request.auth.AuthenticatedRegistrarAccessor;
+import google.registry.request.auth.AuthenticatedRegistrarAccessor.Role;
+import google.registry.request.auth.UserAuthInfo;
+import google.registry.schema.domain.RegistryLock;
+import google.registry.testing.AppEngineRule;
+import google.registry.testing.FakeClock;
+import google.registry.util.EmailMessage;
+import google.registry.util.SendEmailService;
+import java.util.Map;
+import java.util.UUID;
+import javax.mail.internet.InternetAddress;
+import javax.servlet.http.HttpServletResponse;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnit;
+import org.mockito.junit.MockitoRule;
+
+@RunWith(JUnit4.class)
+public final class RegistryLockPostActionTest {
+
+ private static final String EMAIL_MESSAGE_TEMPLATE =
+ "Please click the link below to perform the lock \\/ unlock action on domain example.tld. "
+ + "Note: this code will expire in one hour.\n\n"
+ + "https:\\/\\/localhost\\/registry-lock-verify\\?lockVerificationCode="
+ + "[0-9a-zA-Z_\\-]+&isLock=(true|false)";
+
+ @Rule public final AppEngineRule appEngineRule = AppEngineRule.builder().withDatastore().build();
+
+ @Rule
+ public final JpaIntegrationTestRule jpaRule =
+ new JpaTestRules.Builder().buildIntegrationTestRule();
+
+ @Rule public final MockitoRule mocks = MockitoJUnit.rule();
+
+ private final User userWithoutPermission =
+ new User("johndoe@theregistrar.com", "gmail.com", "31337");
+ // Marla Singer has registry lock auth permissions
+ private final User userWithLockPermission =
+ new User("Marla.Singer@crr.com", "gmail.com", "31337");
+ private final FakeClock clock = new FakeClock();
+
+ private InternetAddress outgoingAddress;
+ private DomainBase domain;
+ private RegistryLockPostAction action;
+
+ @Mock SendEmailService emailService;
+ @Mock HttpServletResponse mockResponse;
+
+ @Before
+ public void setup() throws Exception {
+ createTld("tld");
+ domain = persistResource(newDomainBase("example.tld"));
+ outgoingAddress = new InternetAddress("domain-registry@example.com");
+
+ action =
+ createAction(
+ AuthResult.create(AuthLevel.USER, UserAuthInfo.create(userWithLockPermission, false)));
+ }
+
+ @Test
+ public void testSuccess_lock() throws Exception {
+ Map response = action.handleJsonRequest(lockRequest());
+ assertSuccess(response, "lock", "Marla.Singer@crr.com");
+ }
+
+ @Test
+ public void testSuccess_unlock() throws Exception {
+ RegistryLockDao.save(
+ createLock().asBuilder().setLockCompletionTimestamp(clock.nowUtc()).build());
+ persistResource(domain.asBuilder().setStatusValues(REGISTRY_LOCK_STATUSES).build());
+ Map response = action.handleJsonRequest(unlockRequest());
+ assertSuccess(response, "unlock", "Marla.Singer@crr.com");
+ }
+
+ @Test
+ public void testSuccess_unlock_adminUnlockingAdmin() throws Exception {
+ RegistryLockDao.save(
+ createLock()
+ .asBuilder()
+ .isSuperuser(true)
+ .setLockCompletionTimestamp(clock.nowUtc())
+ .build());
+ persistResource(domain.asBuilder().setStatusValues(REGISTRY_LOCK_STATUSES).build());
+ action =
+ createAction(
+ AuthResult.create(AuthLevel.USER, UserAuthInfo.create(userWithoutPermission, true)));
+ Map response = action.handleJsonRequest(unlockRequest());
+ assertSuccess(response, "unlock", "johndoe@theregistrar.com");
+ }
+
+ @Test
+ public void testFailure_unlock_noLock() {
+ persistResource(domain.asBuilder().setStatusValues(REGISTRY_LOCK_STATUSES).build());
+ Map response = action.handleJsonRequest(unlockRequest());
+ assertFailureWithMessage(response, "No lock object for domain example.tld");
+ }
+
+ @Test
+ public void testFailure_unlock_alreadyUnlocked() {
+ persistResource(domain.asBuilder().setStatusValues(REGISTRY_LOCK_STATUSES).build());
+ RegistryLockDao.save(
+ createLock()
+ .asBuilder()
+ .setLockCompletionTimestamp(clock.nowUtc())
+ .setUnlockRequestTimestamp(clock.nowUtc())
+ .build());
+ Map response = action.handleJsonRequest(unlockRequest());
+ assertFailureWithMessage(response, "A pending unlock action already exists for example.tld");
+ }
+
+ @Test
+ public void testFailure_unlock_nonAdminUnlockingAdmin() {
+ RegistryLockDao.save(
+ createLock()
+ .asBuilder()
+ .isSuperuser(true)
+ .setLockCompletionTimestamp(clock.nowUtc())
+ .build());
+ persistResource(domain.asBuilder().setStatusValues(REGISTRY_LOCK_STATUSES).build());
+ Map response = action.handleJsonRequest(unlockRequest());
+ assertFailureWithMessage(
+ response, "Non-admin user cannot unlock admin-locked domain example.tld");
+ }
+
+ @Test
+ public void testSuccess_adminUser() throws Exception {
+ // Admin user should be able to lock/unlock regardless
+ action =
+ createAction(
+ AuthResult.create(AuthLevel.USER, UserAuthInfo.create(userWithoutPermission, true)));
+ Map response = action.handleJsonRequest(lockRequest());
+ assertSuccess(response, "lock", "johndoe@theregistrar.com");
+ }
+
+ @Test
+ public void testFailure_noInput() {
+ Map response = action.handleJsonRequest(null);
+ assertFailureWithMessage(response, "Null JSON");
+ }
+
+ @Test
+ public void testFailure_noClientId() {
+ Map response = action.handleJsonRequest(ImmutableMap.of());
+ assertFailureWithMessage(response, "Missing key for client: clientId");
+ }
+
+ @Test
+ public void testFailure_emptyClientId() {
+ Map response = action.handleJsonRequest(ImmutableMap.of("clientId", ""));
+ assertFailureWithMessage(response, "Missing key for client: clientId");
+ }
+
+ @Test
+ public void testFailure_noDomainName() {
+ Map response =
+ action.handleJsonRequest(
+ ImmutableMap.of("clientId", "TheRegistrar", "password", "hi", "isLock", true));
+ assertFailureWithMessage(response, "Missing key for fullyQualifiedDomainName");
+ }
+
+ @Test
+ public void testFailure_noLockParam() {
+ Map response =
+ action.handleJsonRequest(
+ ImmutableMap.of(
+ "clientId", "TheRegistrar",
+ "fullyQualifiedDomainName", "example.tld",
+ "password", "hi"));
+ assertFailureWithMessage(response, "Missing key for isLock");
+ }
+
+ @Test
+ public void testFailure_notAllowedOnRegistrar() {
+ persistResource(
+ loadRegistrar("TheRegistrar").asBuilder().setRegistryLockAllowed(false).build());
+ Map response = action.handleJsonRequest(lockRequest());
+ assertFailureWithMessage(response, "Registry lock not allowed for this registrar");
+ }
+
+ @Test
+ public void testFailure_noPassword() {
+ Map response =
+ action.handleJsonRequest(
+ ImmutableMap.of(
+ "clientId", "TheRegistrar",
+ "fullyQualifiedDomainName", "example.tld",
+ "isLock", true,
+ "pocId", "Marla.Singer@crr.com"));
+ assertFailureWithMessage(response, "Missing key for password");
+ }
+
+ @Test
+ public void testFailure_notEnabledForRegistrarContact() {
+ Map response =
+ action.handleJsonRequest(
+ ImmutableMap.of(
+ "clientId", "TheRegistrar",
+ "fullyQualifiedDomainName", "example.tld",
+ "isLock", true,
+ "pocId", "johndoe@theregistrar.com",
+ "password", "hi"));
+ assertFailureWithMessage(response, "Incorrect registry lock password for contact");
+ }
+
+ @Test
+ public void testFailure_badPassword() {
+ Map response =
+ action.handleJsonRequest(
+ ImmutableMap.of(
+ "clientId", "TheRegistrar",
+ "fullyQualifiedDomainName", "example.tld",
+ "isLock", true,
+ "pocId", "Marla.Singer@crr.com",
+ "password", "badPassword"));
+ assertFailureWithMessage(response, "Incorrect registry lock password for contact");
+ }
+
+ @Test
+ public void testFailure_invalidDomain() {
+ Map response =
+ action.handleJsonRequest(
+ ImmutableMap.of(
+ "clientId", "TheRegistrar",
+ "fullyQualifiedDomainName", "bad.tld",
+ "isLock", true,
+ "pocId", "Marla.Singer@crr.com",
+ "password", "hi"));
+ assertFailureWithMessage(response, "Unknown domain bad.tld");
+ }
+
+ @Test
+ public void testFailure_noPocId() {
+ Map response =
+ action.handleJsonRequest(
+ ImmutableMap.of(
+ "clientId", "TheRegistrar",
+ "fullyQualifiedDomainName", "bad.tld",
+ "isLock", true,
+ "password", "hi"));
+ assertFailureWithMessage(response, "Missing key for pocId");
+ }
+
+ @Test
+ public void testFailure_invalidPocId() {
+ Map response =
+ action.handleJsonRequest(
+ ImmutableMap.of(
+ "clientId", "TheRegistrar",
+ "fullyQualifiedDomainName", "bad.tld",
+ "isLock", true,
+ "pocId", "someotherpoc@crr.com",
+ "password", "hi"));
+ assertFailureWithMessage(response, "Unknown registrar POC ID someotherpoc@crr.com");
+ }
+
+ @Test
+ public void testSuccess_previousLockUnlocked() throws Exception {
+ RegistryLockDao.save(
+ createLock()
+ .asBuilder()
+ .setLockCompletionTimestamp(clock.nowUtc().minusMinutes(1))
+ .setUnlockRequestTimestamp(clock.nowUtc().minusMinutes(1))
+ .setUnlockCompletionTimestamp(clock.nowUtc().minusMinutes(1))
+ .build());
+
+ Map response = action.handleJsonRequest(lockRequest());
+ assertSuccess(response, "lock", "Marla.Singer@crr.com");
+ }
+
+ @Test
+ public void testSuccess_previousLockExpired() throws Exception {
+ RegistryLock previousLock = RegistryLockDao.save(createLock());
+ previousLock = RegistryLockDao.getByVerificationCode(previousLock.getVerificationCode()).get();
+ clock.setTo(previousLock.getLockRequestTimestamp().plusHours(2));
+ Map response = action.handleJsonRequest(lockRequest());
+ assertSuccess(response, "lock", "Marla.Singer@crr.com");
+ }
+
+ @Test
+ public void testFailure_alreadyPendingLock() {
+ RegistryLockDao.save(createLock());
+ Map response = action.handleJsonRequest(lockRequest());
+ assertFailureWithMessage(
+ response, "A pending or completed lock action already exists for example.tld");
+ }
+
+ @Test
+ public void testFailure_alreadyLocked() {
+ persistResource(domain.asBuilder().setStatusValues(REGISTRY_LOCK_STATUSES).build());
+ Map response = action.handleJsonRequest(lockRequest());
+ assertFailureWithMessage(response, "Domain example.tld is already locked");
+ }
+
+ @Test
+ public void testFailure_alreadyUnlocked() {
+ Map response = action.handleJsonRequest(unlockRequest());
+ assertFailureWithMessage(response, "Domain example.tld is already unlocked");
+ }
+
+ private ImmutableMap lockRequest() {
+ return fullRequest(true);
+ }
+
+ private ImmutableMap unlockRequest() {
+ return fullRequest(false);
+ }
+
+ private ImmutableMap fullRequest(boolean lock) {
+ return ImmutableMap.of(
+ "isLock", lock,
+ "clientId", "TheRegistrar",
+ "fullyQualifiedDomainName", "example.tld",
+ "pocId", "Marla.Singer@crr.com",
+ "password", "hi");
+ }
+
+ private RegistryLock createLock() {
+ DomainBase domain = loadByForeignKey(DomainBase.class, "example.tld", clock.nowUtc()).get();
+ return new RegistryLock.Builder()
+ .setDomainName("example.tld")
+ .isSuperuser(false)
+ .setVerificationCode(UUID.randomUUID().toString())
+ .setRegistrarId("TheRegistrar")
+ .setRepoId(domain.getRepoId())
+ .setRegistrarPocId("Marla.Singer@crr.com")
+ .build();
+ }
+
+ private void assertSuccess(Map response, String lockAction, String recipient)
+ throws Exception {
+ assertThat(response)
+ .containsExactly(
+ "status", "SUCCESS",
+ "results", ImmutableList.of(),
+ "message", String.format("Successful %s", lockAction));
+ verifyEmail(recipient);
+ }
+
+ private void assertFailureWithMessage(Map response, String message) {
+ assertThat(response)
+ .containsExactly("status", "ERROR", "results", ImmutableList.of(), "message", message);
+ verifyNoMoreInteractions(emailService);
+ }
+
+ private void verifyEmail(String recipient) throws Exception {
+ ArgumentCaptor emailCaptor = ArgumentCaptor.forClass(EmailMessage.class);
+ verify(emailService).sendEmail(emailCaptor.capture());
+ EmailMessage sentMessage = emailCaptor.getValue();
+ assertThat(sentMessage.subject()).matches("Registry (un)?lock verification");
+ assertThat(sentMessage.body()).matches(EMAIL_MESSAGE_TEMPLATE);
+ assertThat(sentMessage.from()).isEqualTo(new InternetAddress("domain-registry@example.com"));
+ assertThat(sentMessage.recipients()).containsExactly(new InternetAddress(recipient));
+ }
+
+ private RegistryLockPostAction createAction(AuthResult authResult) {
+ AuthenticatedRegistrarAccessor registrarAccessor =
+ AuthenticatedRegistrarAccessor.createForTesting(
+ ImmutableSetMultimap.of("TheRegistrar", Role.OWNER, "NewRegistrar", Role.OWNER));
+ JsonActionRunner jsonActionRunner =
+ new JsonActionRunner(ImmutableMap.of(), new JsonResponse(new ResponseImpl(mockResponse)));
+ return new RegistryLockPostAction(
+ jsonActionRunner, authResult, registrarAccessor, emailService, clock, outgoingAddress);
+ }
+}
diff --git a/core/src/test/resources/google/registry/module/frontend/frontend_routing.txt b/core/src/test/resources/google/registry/module/frontend/frontend_routing.txt
index 6a7f9066e..57babedbe 100644
--- a/core/src/test/resources/google/registry/module/frontend/frontend_routing.txt
+++ b/core/src/test/resources/google/registry/module/frontend/frontend_routing.txt
@@ -6,4 +6,5 @@ PATH CLASS METHODS OK AUTH_METHODS
/registrar-ote-status OteStatusAction POST n API,LEGACY USER PUBLIC
/registrar-settings RegistrarSettingsAction POST n API,LEGACY USER PUBLIC
/registry-lock-get RegistryLockGetAction GET n API,LEGACY USER PUBLIC
+/registry-lock-post RegistryLockPostAction POST n API,LEGACY USER PUBLIC
/registry-lock-verify RegistryLockVerifyAction GET n API,LEGACY USER PUBLIC