diff --git a/core/src/main/java/google/registry/model/registry/RegistryLockDao.java b/core/src/main/java/google/registry/model/registry/RegistryLockDao.java index 327103c20..f30d9e2bd 100644 --- a/core/src/main/java/google/registry/model/registry/RegistryLockDao.java +++ b/core/src/main/java/google/registry/model/registry/RegistryLockDao.java @@ -30,7 +30,7 @@ public final class RegistryLockDao { * code (there may be two instances of the same code in the database--one after lock object * creation and one after verification. */ - public static RegistryLock getByVerificationCode(String verificationCode) { + public static Optional getByVerificationCode(String verificationCode) { return jpaTm() .transact( () -> { @@ -42,9 +42,8 @@ public final class RegistryLockDao { Long.class) .setParameter("verificationCode", verificationCode) .getSingleResult(); - // TODO(gbrodman): Don't throw NPE here. Maybe NoResultException fits better? - checkNotNull(revisionId, "No registry lock with this code"); - return em.find(RegistryLock.class, revisionId); + return Optional.ofNullable(revisionId) + .map(revision -> em.find(RegistryLock.class, revision)); }); } @@ -67,8 +66,8 @@ public final class RegistryLockDao { } /** - * Returns the most recent lock object for a given repo ID (i.e. a domain) or empty if this domain - * hasn't been locked before. + * Returns the most recent lock object for a given domain specified by repo ID, or empty if this + * domain hasn't been locked before. */ public static Optional getMostRecentByRepoId(String repoId) { return jpaTm() @@ -86,6 +85,28 @@ public final class RegistryLockDao { .findFirst()); } + /** + * Returns the most recent verified lock object for a given domain specified by repo ID, or empty + * if no lock has ever been finalized for this domain. This is different from {@link + * #getMostRecentByRepoId(String)} in that it only returns verified locks. + */ + public static Optional getMostRecentVerifiedLockByRepoId(String repoId) { + return jpaTm() + .transact( + () -> + jpaTm() + .getEntityManager() + .createQuery( + "SELECT lock FROM RegistryLock lock WHERE lock.repoId = :repoId AND" + + " lock.lockCompletionTimestamp IS NOT NULL ORDER BY lock.revisionId" + + " DESC", + RegistryLock.class) + .setParameter("repoId", repoId) + .setMaxResults(1) + .getResultStream() + .findFirst()); + } + public static RegistryLock save(RegistryLock registryLock) { checkNotNull(registryLock, "Null registry lock cannot be saved"); return jpaTm().transact(() -> jpaTm().getEntityManager().merge(registryLock)); diff --git a/core/src/main/java/google/registry/schema/domain/RegistryLock.java b/core/src/main/java/google/registry/schema/domain/RegistryLock.java index 2ce84a139..dd2ccb963 100644 --- a/core/src/main/java/google/registry/schema/domain/RegistryLock.java +++ b/core/src/main/java/google/registry/schema/domain/RegistryLock.java @@ -15,6 +15,7 @@ package google.registry.schema.domain; import static com.google.common.base.Preconditions.checkArgument; +import static google.registry.util.DateTimeUtils.isBeforeOrAt; import static google.registry.util.DateTimeUtils.toZonedDateTime; import static google.registry.util.PreconditionsUtils.checkArgumentNotNull; @@ -22,6 +23,7 @@ import google.registry.model.Buildable; import google.registry.model.CreateAutoTimestamp; import google.registry.model.ImmutableObject; import google.registry.model.UpdateAutoTimestamp; +import google.registry.util.Clock; import google.registry.util.DateTimeUtils; import java.time.ZonedDateTime; import java.util.Optional; @@ -182,6 +184,20 @@ public final class RegistryLock extends ImmutableObject implements Buildable { return lockCompletionTimestamp != null && unlockCompletionTimestamp == null; } + /** Returns true iff the lock was requested >= 1 hour ago and has not been verified. */ + public boolean isLockRequestExpired(Clock clock) { + return !getLockCompletionTimestamp().isPresent() + && isBeforeOrAt(getLockRequestTimestamp(), clock.nowUtc().minusHours(1)); + } + + /** Returns true iff the unlock was requested >= 1 hour ago and has not been verified. */ + public boolean isUnlockRequestExpired(Clock clock) { + Optional unlockRequestTimestamp = getUnlockRequestTimestamp(); + return unlockRequestTimestamp.isPresent() + && !getUnlockCompletionTimestamp().isPresent() + && isBeforeOrAt(unlockRequestTimestamp.get(), clock.nowUtc().minusHours(1)); + } + @Override public Builder asBuilder() { return new Builder(clone(this)); diff --git a/core/src/main/java/google/registry/tools/DomainLockUtils.java b/core/src/main/java/google/registry/tools/DomainLockUtils.java new file mode 100644 index 000000000..0465465ec --- /dev/null +++ b/core/src/main/java/google/registry/tools/DomainLockUtils.java @@ -0,0 +1,269 @@ +// 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.tools; + +import static com.google.common.base.Preconditions.checkArgument; +import static google.registry.model.EppResourceUtils.loadByForeignKeyCached; +import static google.registry.model.ofy.ObjectifyService.ofy; +import static google.registry.persistence.transaction.TransactionManagerFactory.jpaTm; +import static google.registry.persistence.transaction.TransactionManagerFactory.tm; +import static google.registry.tools.LockOrUnlockDomainCommand.REGISTRY_LOCK_STATUSES; + +import com.google.common.collect.ImmutableSet; +import com.google.common.collect.Sets; +import com.googlecode.objectify.Key; +import google.registry.model.billing.BillingEvent; +import google.registry.model.billing.BillingEvent.Reason; +import google.registry.model.domain.DomainBase; +import google.registry.model.registry.Registry; +import google.registry.model.registry.RegistryLockDao; +import google.registry.model.reporting.HistoryEntry; +import google.registry.schema.domain.RegistryLock; +import google.registry.util.Clock; +import java.util.Optional; +import java.util.UUID; +import javax.annotation.Nullable; + +/** + * Utility class for validating and applying {@link RegistryLock}s. + * + *

For both locks and unlocks, a lock must be requested via the createRegistry*Requst methods + * then verified through the verifyAndApply* methods. These methods will verify that the domain in + * question is in a lock/unlockable state and will return the lock object. + */ +public final class DomainLockUtils { + + private DomainLockUtils() {} + + public static RegistryLock createRegistryLockRequest( + String domainName, + String registrarId, + @Nullable String registrarPocId, + boolean isAdmin, + Clock clock) { + DomainBase domainBase = getDomain(domainName, clock); + verifyDomainNotLocked(domainBase); + + // Multiple pending actions are not allowed + RegistryLockDao.getMostRecentByRepoId(domainBase.getRepoId()) + .ifPresent( + previousLock -> + checkArgument( + previousLock.isLockRequestExpired(clock) + || previousLock.getUnlockCompletionTimestamp().isPresent(), + "A pending or completed lock action already exists for %s", + previousLock.getDomainName())); + + RegistryLock lock = + new RegistryLock.Builder() + .setVerificationCode(UUID.randomUUID().toString()) + .setDomainName(domainName) + .setRepoId(domainBase.getRepoId()) + .setRegistrarId(registrarId) + .setRegistrarPocId(registrarPocId) + .isSuperuser(isAdmin) + .build(); + return RegistryLockDao.save(lock); + } + + public static RegistryLock createRegistryUnlockRequest( + String domainName, String registrarId, boolean isAdmin, Clock clock) { + DomainBase domainBase = getDomain(domainName, clock); + Optional lockOptional = + RegistryLockDao.getMostRecentVerifiedLockByRepoId(domainBase.getRepoId()); + + RegistryLock.Builder newLockBuilder; + if (isAdmin) { + // Admins should always be able to unlock domains in case we get in a bad state + // TODO(b/147411297): Remove the admin checks / failsafes once we have migrated existing + // locked domains to have lock objects + newLockBuilder = + lockOptional + .map(RegistryLock::asBuilder) + .orElse( + new RegistryLock.Builder() + .setRepoId(domainBase.getRepoId()) + .setDomainName(domainName) + .setLockCompletionTimestamp(clock.nowUtc()) + .setRegistrarId(registrarId)); + } else { + verifyDomainLocked(domainBase); + RegistryLock lock = + lockOptional.orElseThrow( + () -> + new IllegalArgumentException( + String.format("No lock object for domain %s", domainName))); + checkArgument( + lock.isLocked(), "Lock object for domain %s is not currently locked", domainName); + checkArgument( + !lock.getUnlockRequestTimestamp().isPresent() || lock.isUnlockRequestExpired(clock), + "A pending unlock action already exists for %s", + domainName); + checkArgument( + lock.getRegistrarId().equals(registrarId), + "Lock object does not have registrar ID %s", + registrarId); + checkArgument( + !lock.isSuperuser(), "Non-admin user cannot unlock admin-locked domain %s", domainName); + newLockBuilder = lock.asBuilder(); + } + RegistryLock newLock = + newLockBuilder + .setVerificationCode(UUID.randomUUID().toString()) + .isSuperuser(isAdmin) + .setUnlockRequestTimestamp(clock.nowUtc()) + .setRegistrarId(registrarId) + .build(); + return RegistryLockDao.save(newLock); + } + + public static RegistryLock verifyAndApplyLock( + String verificationCode, boolean isAdmin, Clock clock) { + return jpaTm() + .transact( + () -> { + RegistryLock lock = getByVerificationCode(verificationCode); + + checkArgument( + !lock.getLockCompletionTimestamp().isPresent(), + "Domain %s is already locked", + lock.getDomainName()); + + checkArgument( + !lock.isLockRequestExpired(clock), + "The pending lock has expired; please try again"); + + checkArgument( + !lock.isSuperuser() || isAdmin, "Non-admin user cannot complete admin lock"); + + RegistryLock newLock = + RegistryLockDao.save( + lock.asBuilder().setLockCompletionTimestamp(clock.nowUtc()).build()); + tm().transact(() -> applyLockStatuses(newLock, clock)); + return newLock; + }); + } + + public static RegistryLock verifyAndApplyUnlock( + String verificationCode, boolean isAdmin, Clock clock) { + return jpaTm() + .transact( + () -> { + RegistryLock lock = getByVerificationCode(verificationCode); + checkArgument( + !lock.getUnlockCompletionTimestamp().isPresent(), + "Domain %s is already unlocked", + lock.getDomainName()); + + checkArgument( + !lock.isUnlockRequestExpired(clock), + "The pending unlock has expired; please try again"); + + checkArgument( + isAdmin || !lock.isSuperuser(), "Non-admin user cannot complete admin unlock"); + + RegistryLock newLock = + RegistryLockDao.save( + lock.asBuilder().setUnlockCompletionTimestamp(clock.nowUtc()).build()); + tm().transact(() -> removeLockStatuses(newLock, isAdmin, clock)); + return newLock; + }); + } + + private static void verifyDomainNotLocked(DomainBase domainBase) { + checkArgument( + !domainBase.getStatusValues().containsAll(REGISTRY_LOCK_STATUSES), + "Domain %s is already locked", + domainBase.getFullyQualifiedDomainName()); + } + + private static void verifyDomainLocked(DomainBase domainBase) { + checkArgument( + !Sets.intersection(domainBase.getStatusValues(), REGISTRY_LOCK_STATUSES).isEmpty(), + "Domain %s is already unlocked", + domainBase.getFullyQualifiedDomainName()); + } + + private static DomainBase getDomain(String domainName, Clock clock) { + return loadByForeignKeyCached(DomainBase.class, domainName, clock.nowUtc()) + .orElseThrow( + () -> new IllegalArgumentException(String.format("Unknown domain %s", domainName))); + } + + private static RegistryLock getByVerificationCode(String verificationCode) { + return RegistryLockDao.getByVerificationCode(verificationCode) + .orElseThrow( + () -> + new IllegalArgumentException( + String.format("Invalid verification code %s", verificationCode))); + } + + private static void applyLockStatuses(RegistryLock lock, Clock clock) { + DomainBase domain = getDomain(lock.getDomainName(), clock); + verifyDomainNotLocked(domain); + + DomainBase newDomain = + domain + .asBuilder() + .setStatusValues( + ImmutableSet.copyOf(Sets.union(domain.getStatusValues(), REGISTRY_LOCK_STATUSES))) + .build(); + saveEntities(newDomain, lock, clock); + } + + private static void removeLockStatuses(RegistryLock lock, boolean isAdmin, Clock clock) { + DomainBase domain = getDomain(lock.getDomainName(), clock); + if (!isAdmin) { + verifyDomainLocked(domain); + } + + DomainBase newDomain = + domain + .asBuilder() + .setStatusValues( + ImmutableSet.copyOf( + Sets.difference(domain.getStatusValues(), REGISTRY_LOCK_STATUSES))) + .build(); + saveEntities(newDomain, lock, clock); + } + + private static void saveEntities(DomainBase domain, RegistryLock lock, Clock clock) { + String reason = "Lock or unlock of a domain through a RegistryLock operation"; + HistoryEntry historyEntry = + new HistoryEntry.Builder() + .setClientId(domain.getCurrentSponsorClientId()) + .setBySuperuser(lock.isSuperuser()) + .setRequestedByRegistrar(!lock.isSuperuser()) + .setType(HistoryEntry.Type.DOMAIN_UPDATE) + .setModificationTime(clock.nowUtc()) + .setParent(Key.create(domain)) + .setReason(reason) + .build(); + ofy().save().entities(domain, historyEntry); + if (!lock.isSuperuser()) { // admin actions shouldn't affect billing + BillingEvent.OneTime oneTime = + new BillingEvent.OneTime.Builder() + .setReason(Reason.SERVER_STATUS) + .setTargetId(domain.getForeignKey()) + .setClientId(domain.getCurrentSponsorClientId()) + .setCost(Registry.get(domain.getTld()).getServerStatusChangeCost()) + .setEventTime(clock.nowUtc()) + .setBillingTime(clock.nowUtc()) + .setParent(historyEntry) + .build(); + ofy().save().entity(oneTime); + } + } +} diff --git a/core/src/main/java/google/registry/tools/LockDomainCommand.java b/core/src/main/java/google/registry/tools/LockDomainCommand.java index 35c03b505..8e61e5b2c 100644 --- a/core/src/main/java/google/registry/tools/LockDomainCommand.java +++ b/core/src/main/java/google/registry/tools/LockDomainCommand.java @@ -14,25 +14,19 @@ package google.registry.tools; -import static com.google.common.collect.ImmutableList.toImmutableList; import static google.registry.model.EppResourceUtils.loadByForeignKey; -import static google.registry.util.PreconditionsUtils.checkArgumentPresent; -import static org.joda.time.DateTimeZone.UTC; import com.beust.jcommander.Parameters; -import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableSet; import com.google.common.collect.Sets; import com.google.common.flogger.FluentLogger; -import com.google.template.soy.data.SoyMapData; import google.registry.model.domain.DomainBase; import google.registry.model.eppcommon.StatusValue; -import google.registry.tools.soy.DomainUpdateSoyInfo; -import java.util.Optional; +import google.registry.schema.domain.RegistryLock; import org.joda.time.DateTime; /** - * A command to registry lock domain names via EPP. + * A command to registry lock domain names. * *

A registry lock consists of server-side statuses preventing deletes, updates, and transfers. */ @@ -42,41 +36,35 @@ public class LockDomainCommand extends LockOrUnlockDomainCommand { private static final FluentLogger logger = FluentLogger.forEnclosingClass(); @Override - protected void initMutatingEppToolCommand() { + protected ImmutableSet getRelevantDomains() { // Project all domains as of the same time so that argument order doesn't affect behavior. - DateTime now = DateTime.now(UTC); + DateTime now = clock.nowUtc(); + ImmutableSet.Builder relevantDomains = new ImmutableSet.Builder<>(); for (String domain : getDomains()) { - Optional domainBase = loadByForeignKey(DomainBase.class, domain, now); - checkArgumentPresent(domainBase, "Domain '%s' does not exist or is deleted", domain); + DomainBase domainBase = + loadByForeignKey(DomainBase.class, domain, now) + .orElseThrow( + () -> + new IllegalArgumentException( + String.format("Domain '%s' does not exist or is deleted", domain))); ImmutableSet statusesToAdd = - Sets.difference(REGISTRY_LOCK_STATUSES, domainBase.get().getStatusValues()) - .immutableCopy(); + Sets.difference(REGISTRY_LOCK_STATUSES, domainBase.getStatusValues()).immutableCopy(); if (statusesToAdd.isEmpty()) { logger.atInfo().log("Domain '%s' is already locked and needs no updates.", domain); continue; } - - setSoyTemplate(DomainUpdateSoyInfo.getInstance(), DomainUpdateSoyInfo.DOMAINUPDATE); - addSoyRecord( - clientId, - new SoyMapData( - "domain", domain, - "add", true, - "addNameservers", ImmutableList.of(), - "addAdmins", ImmutableList.of(), - "addTechs", ImmutableList.of(), - "addStatuses", - statusesToAdd.stream().map(StatusValue::getXmlName).collect(toImmutableList()), - "remove", false, - "removeNameservers", ImmutableList.of(), - "removeAdmins", ImmutableList.of(), - "removeTechs", ImmutableList.of(), - "removeStatuses", ImmutableList.of(), - "change", false, - "secdns", false, - "addDsRecords", ImmutableList.of(), - "removeDsRecords", ImmutableList.of(), - "removeAllDsRecords", false)); + relevantDomains.add(domain); } + return relevantDomains.build(); + } + + @Override + protected RegistryLock createLock(String domain) { + return DomainLockUtils.createRegistryLockRequest(domain, clientId, null, true, clock); + } + + @Override + protected void finalizeLockOrUnlockRequest(RegistryLock lock) { + DomainLockUtils.verifyAndApplyLock(lock.getVerificationCode(), true, clock); } } diff --git a/core/src/main/java/google/registry/tools/LockOrUnlockDomainCommand.java b/core/src/main/java/google/registry/tools/LockOrUnlockDomainCommand.java index ae03e7360..43b59b5f6 100644 --- a/core/src/main/java/google/registry/tools/LockOrUnlockDomainCommand.java +++ b/core/src/main/java/google/registry/tools/LockOrUnlockDomainCommand.java @@ -22,16 +22,23 @@ import static google.registry.util.CollectionUtils.findDuplicates; import com.beust.jcommander.Parameter; import com.google.common.base.Joiner; +import com.google.common.base.Throwables; import com.google.common.collect.ImmutableSet; +import com.google.common.flogger.FluentLogger; import google.registry.config.RegistryConfig.Config; import google.registry.model.eppcommon.StatusValue; +import google.registry.schema.domain.RegistryLock; +import google.registry.util.Clock; import java.util.List; import javax.inject.Inject; /** Shared base class for commands to registry lock or unlock a domain via EPP. */ -public abstract class LockOrUnlockDomainCommand extends MutatingEppToolCommand { +public abstract class LockOrUnlockDomainCommand extends ConfirmingCommand + implements CommandWithCloudSql { - protected static final ImmutableSet REGISTRY_LOCK_STATUSES = + private static final FluentLogger logger = FluentLogger.forEnclosingClass(); + + static final ImmutableSet REGISTRY_LOCK_STATUSES = ImmutableSet.of( SERVER_DELETE_PROHIBITED, SERVER_TRANSFER_PROHIBITED, SERVER_UPDATE_PROHIBITED); @@ -48,23 +55,52 @@ public abstract class LockOrUnlockDomainCommand extends MutatingEppToolCommand { @Config("registryAdminClientId") String registryAdminClientId; + @Inject Clock clock; + + protected ImmutableSet relevantDomains = ImmutableSet.of(); + protected ImmutableSet getDomains() { return ImmutableSet.copyOf(mainParameters); } @Override - protected void initEppToolCommand() throws Exception { - // Superuser status is required to update registry lock statuses. - superuser = true; - + protected void init() { // Default clientId to the registry registrar account if otherwise unspecified. if (clientId == null) { clientId = registryAdminClientId; } String duplicates = Joiner.on(", ").join(findDuplicates(mainParameters)); checkArgument(duplicates.isEmpty(), "Duplicate domain arguments found: '%s'", duplicates); - initMutatingEppToolCommand(); System.out.println( "== ENSURE THAT YOU HAVE AUTHENTICATED THE REGISTRAR BEFORE RUNNING THIS COMMAND =="); + relevantDomains = getRelevantDomains(); } + + @Override + protected String execute() { + int failures = 0; + for (String domain : relevantDomains) { + try { + RegistryLock lock = createLock(domain); + finalizeLockOrUnlockRequest(lock); + } catch (Throwable t) { + Throwable rootCause = Throwables.getRootCause(t); + logger.atSevere().withCause(rootCause).log("Error when (un)locking domain %s.", domain); + failures++; + } + } + if (failures == 0) { + return String.format("Successfully locked/unlocked %d domains.", relevantDomains.size()); + } else { + return String.format( + "Successfully locked/unlocked %d domains with %d failures.", + relevantDomains.size() - failures, failures); + } + } + + protected abstract ImmutableSet getRelevantDomains(); + + protected abstract RegistryLock createLock(String domain); + + protected abstract void finalizeLockOrUnlockRequest(RegistryLock lock); } diff --git a/core/src/main/java/google/registry/tools/UnlockDomainCommand.java b/core/src/main/java/google/registry/tools/UnlockDomainCommand.java index fa21c7090..77d6ae85d 100644 --- a/core/src/main/java/google/registry/tools/UnlockDomainCommand.java +++ b/core/src/main/java/google/registry/tools/UnlockDomainCommand.java @@ -14,25 +14,19 @@ package google.registry.tools; -import static com.google.common.collect.ImmutableList.toImmutableList; import static google.registry.model.EppResourceUtils.loadByForeignKey; -import static google.registry.util.PreconditionsUtils.checkArgumentPresent; -import static org.joda.time.DateTimeZone.UTC; import com.beust.jcommander.Parameters; -import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableSet; import com.google.common.collect.Sets; import com.google.common.flogger.FluentLogger; -import com.google.template.soy.data.SoyMapData; import google.registry.model.domain.DomainBase; import google.registry.model.eppcommon.StatusValue; -import google.registry.tools.soy.DomainUpdateSoyInfo; -import java.util.Optional; +import google.registry.schema.domain.RegistryLock; import org.joda.time.DateTime; /** - * A command to registry unlock domain names via EPP. + * A command to registry unlock domain names. * *

A registry lock consists of server-side statuses preventing deletes, updates, and transfers. */ @@ -42,41 +36,35 @@ public class UnlockDomainCommand extends LockOrUnlockDomainCommand { private static final FluentLogger logger = FluentLogger.forEnclosingClass(); @Override - protected void initMutatingEppToolCommand() { + protected ImmutableSet getRelevantDomains() { // Project all domains as of the same time so that argument order doesn't affect behavior. - DateTime now = DateTime.now(UTC); + DateTime now = clock.nowUtc(); + ImmutableSet.Builder relevantDomains = new ImmutableSet.Builder<>(); for (String domain : getDomains()) { - Optional domainBase = loadByForeignKey(DomainBase.class, domain, now); - checkArgumentPresent(domainBase, "Domain '%s' does not exist or is deleted", domain); + DomainBase domainBase = + loadByForeignKey(DomainBase.class, domain, now) + .orElseThrow( + () -> + new IllegalArgumentException( + String.format("Domain '%s' does not exist or is deleted", domain))); ImmutableSet statusesToRemove = - Sets.intersection(domainBase.get().getStatusValues(), REGISTRY_LOCK_STATUSES) - .immutableCopy(); + Sets.intersection(domainBase.getStatusValues(), REGISTRY_LOCK_STATUSES).immutableCopy(); if (statusesToRemove.isEmpty()) { logger.atInfo().log("Domain '%s' is already unlocked and needs no updates.", domain); continue; } - - setSoyTemplate(DomainUpdateSoyInfo.getInstance(), DomainUpdateSoyInfo.DOMAINUPDATE); - addSoyRecord( - clientId, - new SoyMapData( - "domain", domain, - "add", false, - "addNameservers", ImmutableList.of(), - "addAdmins", ImmutableList.of(), - "addTechs", ImmutableList.of(), - "addStatuses", ImmutableList.of(), - "remove", true, - "removeNameservers", ImmutableList.of(), - "removeAdmins", ImmutableList.of(), - "removeTechs", ImmutableList.of(), - "removeStatuses", - statusesToRemove.stream().map(StatusValue::getXmlName).collect(toImmutableList()), - "change", false, - "secdns", false, - "addDsRecords", ImmutableList.of(), - "removeDsRecords", ImmutableList.of(), - "removeAllDsRecords", false)); + relevantDomains.add(domain); } + return relevantDomains.build(); + } + + @Override + protected RegistryLock createLock(String domain) { + return DomainLockUtils.createRegistryUnlockRequest(domain, clientId, true, clock); + } + + @Override + protected void finalizeLockOrUnlockRequest(RegistryLock lock) { + DomainLockUtils.verifyAndApplyUnlock(lock.getVerificationCode(), true, clock); } } diff --git a/core/src/test/java/google/registry/model/registry/RegistryLockDaoTest.java b/core/src/test/java/google/registry/model/registry/RegistryLockDaoTest.java index 5055486ac..c2c6bd4e4 100644 --- a/core/src/test/java/google/registry/model/registry/RegistryLockDaoTest.java +++ b/core/src/test/java/google/registry/model/registry/RegistryLockDaoTest.java @@ -44,23 +44,13 @@ public final class RegistryLockDaoTest { public void testSaveAndLoad_success() { RegistryLock lock = createLock(); RegistryLockDao.save(lock); - RegistryLock fromDatabase = RegistryLockDao.getByVerificationCode(lock.getVerificationCode()); + RegistryLock fromDatabase = + RegistryLockDao.getByVerificationCode(lock.getVerificationCode()).get(); assertThat(fromDatabase.getDomainName()).isEqualTo(lock.getDomainName()); assertThat(fromDatabase.getVerificationCode()).isEqualTo(lock.getVerificationCode()); assertThat(fromDatabase.getLastUpdateTimestamp()).isEqualTo(jpaRule.getTxnClock().nowUtc()); } - @Test - public void testSaveAndLoad_failure_differentCode() { - RegistryLock lock = createLock(); - RegistryLockDao.save(lock); - NullPointerException thrown = - assertThrows( - NullPointerException.class, - () -> RegistryLockDao.getByVerificationCode(UUID.randomUUID().toString())); - assertThat(thrown).hasMessageThat().isEqualTo("No registry lock with this code"); - } - @Test public void testSaveTwiceAndLoad_returnsLatest() { RegistryLock lock = createLock(); @@ -70,7 +60,7 @@ public final class RegistryLockDaoTest { .transact( () -> { RegistryLock updatedLock = - RegistryLockDao.getByVerificationCode(lock.getVerificationCode()); + RegistryLockDao.getByVerificationCode(lock.getVerificationCode()).get(); RegistryLockDao.save( updatedLock .asBuilder() @@ -81,7 +71,7 @@ public final class RegistryLockDaoTest { .transact( () -> { RegistryLock fromDatabase = - RegistryLockDao.getByVerificationCode(lock.getVerificationCode()); + RegistryLockDao.getByVerificationCode(lock.getVerificationCode()).get(); assertThat(fromDatabase.getLockCompletionTimestamp().get()) .isEqualTo(jpaRule.getTxnClock().nowUtc()); assertThat(fromDatabase.getLastUpdateTimestamp()) @@ -100,7 +90,8 @@ public final class RegistryLockDaoTest { .setUnlockCompletionTimestamp(jpaRule.getTxnClock().nowUtc()) .build()); RegistryLockDao.save(lock); - RegistryLock fromDatabase = RegistryLockDao.getByVerificationCode(lock.getVerificationCode()); + RegistryLock fromDatabase = + RegistryLockDao.getByVerificationCode(lock.getVerificationCode()).get(); assertThat(fromDatabase.getUnlockRequestTimestamp()) .isEqualTo(Optional.of(jpaRule.getTxnClock().nowUtc())); assertThat(fromDatabase.getUnlockCompletionTimestamp()) @@ -119,7 +110,7 @@ public final class RegistryLockDaoTest { .transact( () -> { RegistryLock fromDatabase = - RegistryLockDao.getByVerificationCode(lock.getVerificationCode()); + RegistryLockDao.getByVerificationCode(lock.getVerificationCode()).get(); assertThat(fromDatabase.getLockCompletionTimestamp()) .isEqualTo(Optional.of(jpaRule.getTxnClock().nowUtc())); }); @@ -130,6 +121,11 @@ public final class RegistryLockDaoTest { assertThrows(NullPointerException.class, () -> RegistryLockDao.save(null)); } + @Test + public void getLock_unknownCode() { + assertThat(RegistryLockDao.getByVerificationCode("hi").isPresent()).isFalse(); + } + @Test public void testLoad_lockedDomains_byRegistrarId() { RegistryLock lock = @@ -180,6 +176,28 @@ public final class RegistryLockDaoTest { assertThat(RegistryLockDao.getMostRecentByRepoId("nonexistent").isPresent()).isFalse(); } + @Test + public void testLoad_verified_byRepoId() { + RegistryLock completedLock = + createLock().asBuilder().setLockCompletionTimestamp(jpaRule.getTxnClock().nowUtc()).build(); + RegistryLockDao.save(completedLock); + + jpaRule.getTxnClock().advanceOneMilli(); + RegistryLock inProgressLock = createLock(); + RegistryLockDao.save(inProgressLock); + + Optional mostRecent = RegistryLockDao.getMostRecentVerifiedLockByRepoId("repoId"); + assertThat(mostRecent.isPresent()).isTrue(); + assertThat(mostRecent.get().isLocked()).isTrue(); + } + + @Test + public void testLoad_verified_byRepoId_empty() { + RegistryLockDao.save(createLock()); + Optional mostRecent = RegistryLockDao.getMostRecentVerifiedLockByRepoId("repoId"); + assertThat(mostRecent.isPresent()).isFalse(); + } + private RegistryLock createLock() { return new RegistryLock.Builder() .setRepoId("repoId") diff --git a/core/src/test/java/google/registry/schema/integration/SqlIntegrationTestSuite.java b/core/src/test/java/google/registry/schema/integration/SqlIntegrationTestSuite.java index 479439688..60edb5262 100644 --- a/core/src/test/java/google/registry/schema/integration/SqlIntegrationTestSuite.java +++ b/core/src/test/java/google/registry/schema/integration/SqlIntegrationTestSuite.java @@ -22,6 +22,9 @@ import google.registry.schema.tld.PremiumListUtilsTest; import google.registry.schema.tld.ReservedListDaoTest; import google.registry.schema.tmch.ClaimsListDaoTest; import google.registry.tools.CreateReservedListCommandTest; +import google.registry.tools.DomainLockUtilsTest; +import google.registry.tools.LockDomainCommandTest; +import google.registry.tools.UnlockDomainCommandTest; import google.registry.tools.UpdateReservedListCommandTest; import google.registry.tools.server.CreatePremiumListActionTest; import google.registry.tools.server.UpdatePremiumListActionTest; @@ -42,14 +45,17 @@ import org.junit.runners.Suite.SuiteClasses; @RunWith(Suite.class) @SuiteClasses({ ClaimsListDaoTest.class, + CreatePremiumListActionTest.class, CreateReservedListCommandTest.class, CursorDaoTest.class, - CreatePremiumListActionTest.class, + DomainLockUtilsTest.class, + LockDomainCommandTest.class, PremiumListDaoTest.class, PremiumListUtilsTest.class, RegistryLockDaoTest.class, RegistryLockGetActionTest.class, ReservedListDaoTest.class, + UnlockDomainCommandTest.class, UpdatePremiumListActionTest.class, UpdateReservedListCommandTest.class }) diff --git a/core/src/test/java/google/registry/testing/DatastoreHelper.java b/core/src/test/java/google/registry/testing/DatastoreHelper.java index 1fd465ccf..6515074c7 100644 --- a/core/src/test/java/google/registry/testing/DatastoreHelper.java +++ b/core/src/test/java/google/registry/testing/DatastoreHelper.java @@ -948,7 +948,7 @@ public class DatastoreHelper { * Returns all of the history entries that are parented off the given EppResource with the given * type. */ - public static List getHistoryEntriesOfType( + public static ImmutableList getHistoryEntriesOfType( EppResource resource, final HistoryEntry.Type type) { return getHistoryEntries(resource) .stream() diff --git a/core/src/test/java/google/registry/tools/DomainLockUtilsTest.java b/core/src/test/java/google/registry/tools/DomainLockUtilsTest.java new file mode 100644 index 000000000..b8abd51e3 --- /dev/null +++ b/core/src/test/java/google/registry/tools/DomainLockUtilsTest.java @@ -0,0 +1,361 @@ +// 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.tools; + +import static com.google.common.truth.Truth.assertThat; +import static google.registry.model.ofy.ObjectifyService.ofy; +import static google.registry.testing.DatastoreHelper.createTlds; +import static google.registry.testing.DatastoreHelper.getHistoryEntriesOfType; +import static google.registry.testing.DatastoreHelper.getOnlyHistoryEntryOfType; +import static google.registry.testing.DatastoreHelper.newDomainBase; +import static google.registry.testing.DatastoreHelper.persistActiveHost; +import static google.registry.testing.DatastoreHelper.persistResource; +import static google.registry.tools.LockOrUnlockDomainCommand.REGISTRY_LOCK_STATUSES; +import static org.junit.Assert.assertThrows; + +import com.google.common.collect.ImmutableList; +import google.registry.model.billing.BillingEvent; +import google.registry.model.billing.BillingEvent.Reason; +import google.registry.model.domain.DomainBase; +import google.registry.model.host.HostResource; +import google.registry.model.registry.Registry; +import google.registry.model.registry.RegistryLockDao; +import google.registry.model.reporting.HistoryEntry; +import google.registry.persistence.transaction.JpaTestRules; +import google.registry.persistence.transaction.JpaTestRules.JpaIntegrationTestRule; +import google.registry.schema.domain.RegistryLock; +import google.registry.testing.AppEngineRule; +import google.registry.testing.DatastoreHelper; +import google.registry.testing.FakeClock; +import google.registry.testing.UserInfo; +import java.util.Set; +import java.util.stream.Collectors; +import org.joda.time.Duration; +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 google.registry.tools.DomainLockUtils}. */ +@RunWith(JUnit4.class) +public final class DomainLockUtilsTest { + + private static final String DOMAIN_NAME = "example.tld"; + private static final String POC_ID = "marla.singer@example.com"; + + @Rule + public final AppEngineRule appEngineRule = + AppEngineRule.builder() + .withDatastore() + .withUserService(UserInfo.create(POC_ID, "12345")) + .build(); + + @Rule + public final JpaIntegrationTestRule jpaRule = + new JpaTestRules.Builder().buildIntegrationTestRule(); + + private final FakeClock clock = jpaRule.getTxnClock(); + + private DomainBase domain; + + @Before + public void setup() { + createTlds("tld", "net"); + HostResource host = persistActiveHost("ns1.example.net"); + domain = persistResource(newDomainBase(DOMAIN_NAME, host)); + } + + @Test + public void testSuccess_createLock() { + DomainLockUtils.createRegistryLockRequest(DOMAIN_NAME, "TheRegistrar", POC_ID, false, clock); + } + + @Test + public void testSuccess_createUnlock() { + RegistryLock lock = + DomainLockUtils.createRegistryLockRequest( + DOMAIN_NAME, "TheRegistrar", POC_ID, false, clock); + DomainLockUtils.verifyAndApplyLock(lock.getVerificationCode(), false, clock); + DomainLockUtils.createRegistryUnlockRequest(DOMAIN_NAME, "TheRegistrar", false, clock); + } + + @Test + public void testSuccess_createUnlock_adminUnlockingAdmin() { + RegistryLock lock = + DomainLockUtils.createRegistryLockRequest(DOMAIN_NAME, "TheRegistrar", null, true, clock); + DomainLockUtils.verifyAndApplyLock(lock.getVerificationCode(), true, clock); + DomainLockUtils.createRegistryUnlockRequest(DOMAIN_NAME, "TheRegistrar", true, clock); + } + + @Test + public void testSuccess_createLock_previousLockExpired() { + DomainLockUtils.createRegistryLockRequest(DOMAIN_NAME, "TheRegistrar", POC_ID, false, clock); + clock.advanceBy(Duration.standardDays(1)); + DomainLockUtils.createRegistryLockRequest(DOMAIN_NAME, "TheRegistrar", POC_ID, false, clock); + } + + @Test + public void testSuccess_applyLockDomain() { + RegistryLock lock = + DomainLockUtils.createRegistryLockRequest( + DOMAIN_NAME, "TheRegistrar", POC_ID, false, clock); + DomainLockUtils.verifyAndApplyLock(lock.getVerificationCode(), false, clock); + assertThat(reloadDomain().getStatusValues()).containsExactlyElementsIn(REGISTRY_LOCK_STATUSES); + HistoryEntry historyEntry = getOnlyHistoryEntryOfType(domain, HistoryEntry.Type.DOMAIN_UPDATE); + assertThat(historyEntry.getRequestedByRegistrar()).isTrue(); + assertThat(historyEntry.getBySuperuser()).isFalse(); + assertThat(historyEntry.getReason()) + .isEqualTo("Lock or unlock of a domain through a RegistryLock operation"); + assertBillingEvent(historyEntry); + } + + @Test + public void testSuccess_applyUnlockDomain() { + RegistryLock lock = + DomainLockUtils.createRegistryLockRequest( + DOMAIN_NAME, "TheRegistrar", POC_ID, false, clock); + DomainLockUtils.verifyAndApplyLock(lock.getVerificationCode(), false, clock); + RegistryLock unlock = + DomainLockUtils.createRegistryUnlockRequest(DOMAIN_NAME, "TheRegistrar", false, clock); + DomainLockUtils.verifyAndApplyUnlock(unlock.getVerificationCode(), false, clock); + + assertThat(reloadDomain().getStatusValues()).containsNoneIn(REGISTRY_LOCK_STATUSES); + ImmutableList historyEntries = + getHistoryEntriesOfType(domain, HistoryEntry.Type.DOMAIN_UPDATE); + assertThat(historyEntries.size()).isEqualTo(2); + historyEntries.forEach( + entry -> { + assertThat(entry.getRequestedByRegistrar()).isTrue(); + assertThat(entry.getBySuperuser()).isFalse(); + assertThat(entry.getReason()) + .isEqualTo("Lock or unlock of a domain through a RegistryLock operation"); + }); + assertBillingEvents(historyEntries); + } + + @Test + public void testSuccess_applyAdminLock_onlyHistoryEntry() { + RegistryLock lock = + DomainLockUtils.createRegistryLockRequest(DOMAIN_NAME, "TheRegistrar", null, true, clock); + DomainLockUtils.verifyAndApplyLock(lock.getVerificationCode(), true, clock); + + HistoryEntry historyEntry = getOnlyHistoryEntryOfType(domain, HistoryEntry.Type.DOMAIN_UPDATE); + assertThat(historyEntry.getRequestedByRegistrar()).isFalse(); + assertThat(historyEntry.getBySuperuser()).isTrue(); + DatastoreHelper.assertNoBillingEvents(); + } + + @Test + public void testFailure_createUnlock_alreadyPendingUnlock() { + RegistryLock lock = + DomainLockUtils.createRegistryLockRequest( + DOMAIN_NAME, "TheRegistrar", POC_ID, false, clock); + DomainLockUtils.verifyAndApplyLock(lock.getVerificationCode(), false, clock); + DomainLockUtils.createRegistryUnlockRequest(DOMAIN_NAME, "TheRegistrar", false, clock); + + assertThat( + assertThrows( + IllegalArgumentException.class, + () -> + DomainLockUtils.createRegistryUnlockRequest( + DOMAIN_NAME, "TheRegistrar", false, clock))) + .hasMessageThat() + .isEqualTo("A pending unlock action already exists for example.tld"); + } + + @Test + public void testFailure_createUnlock_nonAdminUnlockingAdmin() { + RegistryLock lock = + DomainLockUtils.createRegistryLockRequest(DOMAIN_NAME, "TheRegistrar", null, true, clock); + DomainLockUtils.verifyAndApplyLock(lock.getVerificationCode(), true, clock); + assertThat( + assertThrows( + IllegalArgumentException.class, + () -> + DomainLockUtils.createRegistryUnlockRequest( + DOMAIN_NAME, "TheRegistrar", false, clock))) + .hasMessageThat() + .isEqualTo("Non-admin user cannot unlock admin-locked domain example.tld"); + } + + @Test + public void testFailure_createLock_unknownDomain() { + assertThat( + assertThrows( + IllegalArgumentException.class, + () -> + DomainLockUtils.createRegistryLockRequest( + "asdf.tld", "TheRegistrar", POC_ID, false, clock))) + .hasMessageThat() + .isEqualTo("Unknown domain asdf.tld"); + } + + @Test + public void testFailure_createLock_alreadyPendingLock() { + DomainLockUtils.createRegistryLockRequest(DOMAIN_NAME, "TheRegistrar", POC_ID, false, clock); + assertThat( + assertThrows( + IllegalArgumentException.class, + () -> + DomainLockUtils.createRegistryLockRequest( + DOMAIN_NAME, "TheRegistrar", POC_ID, false, clock))) + .hasMessageThat() + .isEqualTo("A pending or completed lock action already exists for example.tld"); + } + + @Test + public void testFailure_createLock_alreadyLocked() { + persistResource(domain.asBuilder().setStatusValues(REGISTRY_LOCK_STATUSES).build()); + assertThat( + assertThrows( + IllegalArgumentException.class, + () -> + DomainLockUtils.createRegistryLockRequest( + DOMAIN_NAME, "TheRegistrar", POC_ID, false, clock))) + .hasMessageThat() + .isEqualTo("Domain example.tld is already locked"); + } + + @Test + public void testFailure_createUnlock_alreadyUnlocked() { + assertThat( + assertThrows( + IllegalArgumentException.class, + () -> + DomainLockUtils.createRegistryUnlockRequest( + DOMAIN_NAME, "TheRegistrar", false, clock))) + .hasMessageThat() + .isEqualTo("Domain example.tld is already unlocked"); + } + + @Test + public void testFailure_applyLock_alreadyApplied() { + RegistryLock lock = + DomainLockUtils.createRegistryLockRequest( + DOMAIN_NAME, "TheRegistrar", POC_ID, false, clock); + DomainLockUtils.verifyAndApplyLock(lock.getVerificationCode(), false, clock); + domain = reloadDomain(); + assertThat( + assertThrows( + IllegalArgumentException.class, + () -> DomainLockUtils.verifyAndApplyLock(lock.getVerificationCode(), false, clock))) + .hasMessageThat() + .isEqualTo("Domain example.tld is already locked"); + assertNoDomainChanges(); + } + + @Test + public void testFailure_applyLock_expired() { + RegistryLock lock = + DomainLockUtils.createRegistryLockRequest( + DOMAIN_NAME, "TheRegistrar", POC_ID, false, clock); + clock.advanceBy(Duration.standardDays(1)); + assertThat( + assertThrows( + IllegalArgumentException.class, + () -> DomainLockUtils.verifyAndApplyLock(lock.getVerificationCode(), true, clock))) + .hasMessageThat() + .isEqualTo("The pending lock has expired; please try again"); + assertNoDomainChanges(); + } + + @Test + public void testFailure_applyLock_nonAdmin_applyAdminLock() { + RegistryLock lock = + DomainLockUtils.createRegistryLockRequest(DOMAIN_NAME, "TheRegistrar", null, true, clock); + assertThat( + assertThrows( + IllegalArgumentException.class, + () -> DomainLockUtils.verifyAndApplyLock(lock.getVerificationCode(), false, clock))) + .hasMessageThat() + .isEqualTo("Non-admin user cannot complete admin lock"); + assertNoDomainChanges(); + } + + @Test + public void testFailure_applyUnlock_alreadyUnlocked() { + RegistryLock lock = + DomainLockUtils.createRegistryLockRequest( + DOMAIN_NAME, "TheRegistrar", POC_ID, false, clock); + DomainLockUtils.verifyAndApplyLock(lock.getVerificationCode(), false, clock); + RegistryLock unlock = + DomainLockUtils.createRegistryUnlockRequest(DOMAIN_NAME, "TheRegistrar", false, clock); + DomainLockUtils.verifyAndApplyUnlock(unlock.getVerificationCode(), false, clock); + + assertThat( + assertThrows( + IllegalArgumentException.class, + () -> + DomainLockUtils.verifyAndApplyUnlock( + unlock.getVerificationCode(), false, clock))) + .hasMessageThat() + .isEqualTo("Domain example.tld is already unlocked"); + assertNoDomainChanges(); + } + + @Test + public void testFailure_applyLock_alreadyLocked() { + RegistryLock lock = + DomainLockUtils.createRegistryLockRequest( + DOMAIN_NAME, "TheRegistrar", POC_ID, false, clock); + String verificationCode = lock.getVerificationCode(); + // reload to pick up modification times, etc + lock = RegistryLockDao.getByVerificationCode(verificationCode).get(); + domain = persistResource(domain.asBuilder().setStatusValues(REGISTRY_LOCK_STATUSES).build()); + assertThat( + assertThrows( + IllegalArgumentException.class, + () -> DomainLockUtils.verifyAndApplyLock(verificationCode, false, clock))) + .hasMessageThat() + .isEqualTo("Domain example.tld is already locked"); + + // Failure during Datastore portion shouldn't affect the SQL object + RegistryLock afterAction = + RegistryLockDao.getByVerificationCode(lock.getVerificationCode()).get(); + assertThat(afterAction).isEqualTo(lock); + assertNoDomainChanges(); + } + + private DomainBase reloadDomain() { + return ofy().load().entity(domain).now(); + } + + private void assertNoDomainChanges() { + assertThat(reloadDomain()).isEqualTo(domain); + } + + private void assertBillingEvent(HistoryEntry historyEntry) { + assertBillingEvents(ImmutableList.of(historyEntry)); + } + + private void assertBillingEvents(ImmutableList historyEntries) { + Set expectedEvents = + historyEntries.stream() + .map( + entry -> + new BillingEvent.OneTime.Builder() + .setReason(Reason.SERVER_STATUS) + .setTargetId(domain.getForeignKey()) + .setClientId(domain.getCurrentSponsorClientId()) + .setCost(Registry.get(domain.getTld()).getServerStatusChangeCost()) + .setEventTime(clock.nowUtc()) + .setBillingTime(clock.nowUtc()) + .setParent(entry) + .build()) + .collect(Collectors.toSet()); + DatastoreHelper.assertBillingEvents(expectedEvents); + } +} diff --git a/core/src/test/java/google/registry/tools/LockDomainCommandTest.java b/core/src/test/java/google/registry/tools/LockDomainCommandTest.java index ea6e3999f..f915da835 100644 --- a/core/src/test/java/google/registry/tools/LockDomainCommandTest.java +++ b/core/src/test/java/google/registry/tools/LockDomainCommandTest.java @@ -16,6 +16,7 @@ package google.registry.tools; import static com.google.common.truth.Truth.assertThat; import static google.registry.model.eppcommon.StatusValue.SERVER_TRANSFER_PROHIBITED; +import static google.registry.testing.DatastoreHelper.createTld; import static google.registry.testing.DatastoreHelper.newDomainBase; import static google.registry.testing.DatastoreHelper.persistActiveDomain; import static google.registry.testing.DatastoreHelper.persistNewRegistrar; @@ -24,55 +25,75 @@ import static google.registry.tools.LockOrUnlockDomainCommand.REGISTRY_LOCK_STAT import static org.junit.Assert.assertThrows; import com.google.common.collect.ImmutableList; -import com.google.common.collect.ImmutableMap; +import google.registry.model.domain.DomainBase; import google.registry.model.registrar.Registrar.Type; +import google.registry.model.registry.RegistryLockDao; +import google.registry.persistence.transaction.JpaTestRules; +import google.registry.persistence.transaction.JpaTestRules.JpaIntegrationTestRule; +import google.registry.testing.FakeClock; import java.util.ArrayList; import java.util.List; +import java.util.stream.Collectors; import org.junit.Before; +import org.junit.Rule; import org.junit.Test; /** Unit tests for {@link LockDomainCommand}. */ -public class LockDomainCommandTest extends EppToolCommandTestCase { +public class LockDomainCommandTest extends CommandTestCase { + + @Rule + public final JpaIntegrationTestRule jpaRule = + new JpaTestRules.Builder().buildIntegrationTestRule(); @Before public void before() { - eppVerifier.expectSuperuser(); persistNewRegistrar("adminreg", "Admin Registrar", Type.REAL, 693L); + createTld("tld"); command.registryAdminClientId = "adminreg"; + command.clock = new FakeClock(); } @Test - public void testSuccess_sendsCorrectEppXml() throws Exception { - persistActiveDomain("example.tld"); + public void testSuccess_locksDomain() throws Exception { + DomainBase domain = persistActiveDomain("example.tld"); runCommandForced("--client=NewRegistrar", "example.tld"); - eppVerifier.verifySent("domain_lock.xml", ImmutableMap.of("DOMAIN", "example.tld")); + assertThat(reloadResource(domain).getStatusValues()) + .containsAtLeastElementsIn(REGISTRY_LOCK_STATUSES); } @Test public void testSuccess_partiallyUpdatesStatuses() throws Exception { - persistResource( - newDomainBase("example.tld") - .asBuilder() - .addStatusValue(SERVER_TRANSFER_PROHIBITED) - .build()); + DomainBase domain = + persistResource( + newDomainBase("example.tld") + .asBuilder() + .addStatusValue(SERVER_TRANSFER_PROHIBITED) + .build()); runCommandForced("--client=NewRegistrar", "example.tld"); - eppVerifier.verifySent("domain_lock_partial_statuses.xml"); + assertThat(reloadResource(domain).getStatusValues()) + .containsAtLeastElementsIn(REGISTRY_LOCK_STATUSES); } @Test public void testSuccess_manyDomains() throws Exception { // Create 26 domains -- one more than the number of entity groups allowed in a transaction (in // case that was going to be the failure point). - List domains = new ArrayList<>(); + List domains = new ArrayList<>(); for (int n = 0; n < 26; n++) { String domain = String.format("domain%d.tld", n); - persistActiveDomain(domain); - domains.add(domain); + domains.add(persistActiveDomain(domain)); } runCommandForced( - ImmutableList.builder().add("--client=NewRegistrar").addAll(domains).build()); - for (String domain : domains) { - eppVerifier.verifySent("domain_lock.xml", ImmutableMap.of("DOMAIN", domain)); + ImmutableList.builder() + .add("--client=NewRegistrar") + .addAll( + domains.stream() + .map(DomainBase::getFullyQualifiedDomainName) + .collect(Collectors.toList())) + .build()); + for (DomainBase domain : domains) { + assertThat(reloadResource(domain).getStatusValues()) + .containsAtLeastElementsIn(REGISTRY_LOCK_STATUSES); } } @@ -87,21 +108,22 @@ public class LockDomainCommandTest extends EppToolCommandTestCase { +public class UnlockDomainCommandTest extends CommandTestCase { + + @Rule + public final JpaIntegrationTestRule jpaRule = + new JpaTestRules.Builder().buildIntegrationTestRule(); @Before public void before() { - eppVerifier.expectSuperuser(); persistNewRegistrar("adminreg", "Admin Registrar", Type.REAL, 693L); + createTld("tld"); command.registryAdminClientId = "adminreg"; + command.clock = new FakeClock(); } - private static void persistLockedDomain(String domainName) { - persistResource( - newDomainBase(domainName) - .asBuilder() - .addStatusValues( - ImmutableSet.of( - SERVER_DELETE_PROHIBITED, SERVER_TRANSFER_PROHIBITED, SERVER_UPDATE_PROHIBITED)) - .build()); + private DomainBase persistLockedDomain(String domainName, String registrarId) { + DomainBase domain = persistResource(newDomainBase(domainName)); + RegistryLock lock = + DomainLockUtils.createRegistryLockRequest( + domainName, registrarId, null, true, command.clock); + DomainLockUtils.verifyAndApplyLock(lock.getVerificationCode(), true, command.clock); + return reloadResource(domain); } @Test - public void testSuccess_sendsCorrectEppXml() throws Exception { - persistLockedDomain("example.tld"); + public void testSuccess_unlocksDomain() throws Exception { + DomainBase domain = persistLockedDomain("example.tld", "NewRegistrar"); runCommandForced("--client=NewRegistrar", "example.tld"); - eppVerifier.verifySent("domain_unlock.xml", ImmutableMap.of("DOMAIN", "example.tld")); + assertThat(reloadResource(domain).getStatusValues()).containsNoneIn(REGISTRY_LOCK_STATUSES); } @Test public void testSuccess_partiallyUpdatesStatuses() throws Exception { - persistResource( - newDomainBase("example.tld") - .asBuilder() - .addStatusValues(ImmutableSet.of(SERVER_DELETE_PROHIBITED, SERVER_UPDATE_PROHIBITED)) - .build()); + DomainBase domain = persistLockedDomain("example.tld", "NewRegistrar"); + domain = + persistResource( + domain + .asBuilder() + .setStatusValues( + ImmutableSet.of(SERVER_DELETE_PROHIBITED, SERVER_UPDATE_PROHIBITED)) + .build()); runCommandForced("--client=NewRegistrar", "example.tld"); - eppVerifier.verifySent("domain_unlock_partial_statuses.xml"); + assertThat(reloadResource(domain).getStatusValues()).containsNoneIn(REGISTRY_LOCK_STATUSES); } @Test public void testSuccess_manyDomains() throws Exception { // Create 26 domains -- one more than the number of entity groups allowed in a transaction (in // case that was going to be the failure point). - List domains = new ArrayList<>(); + List domains = new ArrayList<>(); for (int n = 0; n < 26; n++) { String domain = String.format("domain%d.tld", n); - persistLockedDomain(domain); - domains.add(domain); + domains.add(persistLockedDomain(domain, "NewRegistrar")); } runCommandForced( - ImmutableList.builder().add("--client=NewRegistrar").addAll(domains).build()); - for (String domain : domains) { - eppVerifier.verifySent("domain_unlock.xml", ImmutableMap.of("DOMAIN", domain)); + ImmutableList.builder() + .add("--client=NewRegistrar") + .addAll( + domains.stream() + .map(DomainBase::getFullyQualifiedDomainName) + .collect(Collectors.toList())) + .build()); + for (DomainBase domain : domains) { + assertThat(reloadResource(domain).getStatusValues()).containsNoneIn(REGISTRY_LOCK_STATUSES); } } @@ -99,17 +119,17 @@ public class UnlockDomainCommandTest extends EppToolCommandTestCase - - - - - %DOMAIN% - - - - - - - - RegistryTool - - diff --git a/core/src/test/resources/google/registry/tools/server/domain_lock_partial_statuses.xml b/core/src/test/resources/google/registry/tools/server/domain_lock_partial_statuses.xml deleted file mode 100644 index 0a4843905..000000000 --- a/core/src/test/resources/google/registry/tools/server/domain_lock_partial_statuses.xml +++ /dev/null @@ -1,15 +0,0 @@ - - - - - - example.tld - - - - - - - RegistryTool - - diff --git a/core/src/test/resources/google/registry/tools/server/domain_unlock.xml b/core/src/test/resources/google/registry/tools/server/domain_unlock.xml deleted file mode 100644 index f8be65aaf..000000000 --- a/core/src/test/resources/google/registry/tools/server/domain_unlock.xml +++ /dev/null @@ -1,16 +0,0 @@ - - - - - - %DOMAIN% - - - - - - - - RegistryTool - - diff --git a/core/src/test/resources/google/registry/tools/server/domain_unlock_partial_statuses.xml b/core/src/test/resources/google/registry/tools/server/domain_unlock_partial_statuses.xml deleted file mode 100644 index d8454b7b3..000000000 --- a/core/src/test/resources/google/registry/tools/server/domain_unlock_partial_statuses.xml +++ /dev/null @@ -1,15 +0,0 @@ - - - - - - example.tld - - - - - - - RegistryTool - -