diff --git a/core/src/main/java/google/registry/env/common/default/WEB-INF/web.xml b/core/src/main/java/google/registry/env/common/default/WEB-INF/web.xml index b2417bec1..175dc53c5 100644 --- a/core/src/main/java/google/registry/env/common/default/WEB-INF/web.xml +++ b/core/src/main/java/google/registry/env/common/default/WEB-INF/web.xml @@ -61,6 +61,11 @@ /registry-lock-get + + frontend-servlet + /registry-lock-post + + 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