From 5ec4ec3af54d94e3070bed3544b7fa793bc3e529 Mon Sep 17 00:00:00 2001 From: Pavlo Tkach <3469726+ptkach@users.noreply.github.com> Date: Mon, 19 Sep 2022 15:11:36 -0400 Subject: [PATCH] Add REMOVEPACKAGE token functionality to domain transfer flow (#1792) --- .../domain/DomainTransferRequestFlow.java | 28 +++-- .../token/AllocationTokenFlowUtils.java | 4 +- .../domain/DomainTransferRequestFlowTest.java | 101 ++++++++++++++++-- ...main_transfer_request_allocation_token.xml | 2 +- docs/flows.md | 3 +- 5 files changed, 119 insertions(+), 19 deletions(-) diff --git a/core/src/main/java/google/registry/flows/domain/DomainTransferRequestFlow.java b/core/src/main/java/google/registry/flows/domain/DomainTransferRequestFlow.java index 6ea4a2b82..b330ac9ef 100644 --- a/core/src/main/java/google/registry/flows/domain/DomainTransferRequestFlow.java +++ b/core/src/main/java/google/registry/flows/domain/DomainTransferRequestFlow.java @@ -32,6 +32,8 @@ import static google.registry.flows.domain.DomainTransferUtils.createLosingTrans import static google.registry.flows.domain.DomainTransferUtils.createPendingTransferData; import static google.registry.flows.domain.DomainTransferUtils.createTransferResponse; import static google.registry.flows.domain.DomainTransferUtils.createTransferServerApproveEntities; +import static google.registry.flows.domain.token.AllocationTokenFlowUtils.maybeApplyPackageRemovalToken; +import static google.registry.flows.domain.token.AllocationTokenFlowUtils.verifyTokenAllowedOnDomain; import static google.registry.model.eppoutput.Result.Code.SUCCESS_WITH_ACTION_PENDING; import static google.registry.model.reporting.HistoryEntry.Type.DOMAIN_TRANSFER_REQUEST; import static google.registry.persistence.transaction.TransactionManagerFactory.tm; @@ -63,6 +65,7 @@ import google.registry.model.domain.fee.FeeTransferCommandExtension; import google.registry.model.domain.fee.FeeTransformResponseExtension; import google.registry.model.domain.metadata.MetadataExtension; import google.registry.model.domain.superuser.DomainTransferRequestSuperuserExtension; +import google.registry.model.domain.token.AllocationToken; import google.registry.model.domain.token.AllocationTokenExtension; import google.registry.model.eppcommon.AuthInfo; import google.registry.model.eppcommon.StatusValue; @@ -132,6 +135,8 @@ import org.joda.time.DateTime; * google.registry.flows.domain.token.AllocationTokenFlowUtils.AllocationTokenNotValidForTldException} * @error {@link * google.registry.flows.domain.token.AllocationTokenFlowUtils.AlreadyRedeemedAllocationTokenException} + * @error {@link + * google.registry.flows.domain.token.AllocationTokenFlowUtils.MissingRemovePackageTokenOnPackageDomainException} */ @ReportingSpec(ActivityReportField.DOMAIN_TRANSFER_REQUEST) public final class DomainTransferRequestFlow implements TransactionalFlow { @@ -169,19 +174,24 @@ public final class DomainTransferRequestFlow implements TransactionalFlow { extensionManager.validate(); DateTime now = tm().getTransactionTime(); Domain existingDomain = loadAndVerifyExistence(Domain.class, targetId, now); - allocationTokenFlowUtils.verifyAllocationTokenIfPresent( - existingDomain, - Registry.get(existingDomain.getTld()), - gainingClientId, - now, - eppInput.getSingleExtension(AllocationTokenExtension.class)); + Optional allocationToken = + allocationTokenFlowUtils.verifyAllocationTokenIfPresent( + existingDomain, + Registry.get(existingDomain.getTld()), + gainingClientId, + now, + eppInput.getSingleExtension(AllocationTokenExtension.class)); Optional superuserExtension = eppInput.getSingleExtension(DomainTransferRequestSuperuserExtension.class); Period period = superuserExtension.isPresent() ? superuserExtension.get().getRenewalPeriod() : ((Transfer) resourceCommand).getPeriod(); - verifyTransferAllowed(existingDomain, period, now, superuserExtension); + verifyTransferAllowed(existingDomain, period, now, superuserExtension, allocationToken); + + // If client passed an applicable static token this updates the domain + existingDomain = maybeApplyPackageRemovalToken(existingDomain, allocationToken); + String tld = existingDomain.getTld(); Registry registry = Registry.get(tld); // An optional extension from the client specifying what they think the transfer should cost. @@ -293,9 +303,11 @@ public final class DomainTransferRequestFlow implements TransactionalFlow { Domain existingDomain, Period period, DateTime now, - Optional superuserExtension) + Optional superuserExtension, + Optional allocationToken) throws EppException { verifyNoDisallowedStatuses(existingDomain, DISALLOWED_STATUSES); + verifyTokenAllowedOnDomain(existingDomain, allocationToken); if (!isSuperuser) { verifyAuthInfoPresentForResourceTransfer(authInfo); verifyAuthInfo(authInfo.get(), existingDomain); diff --git a/core/src/main/java/google/registry/flows/domain/token/AllocationTokenFlowUtils.java b/core/src/main/java/google/registry/flows/domain/token/AllocationTokenFlowUtils.java index b04a1331e..c1c65e39d 100644 --- a/core/src/main/java/google/registry/flows/domain/token/AllocationTokenFlowUtils.java +++ b/core/src/main/java/google/registry/flows/domain/token/AllocationTokenFlowUtils.java @@ -293,11 +293,11 @@ public class AllocationTokenFlowUtils { } } - /** The __REMOVEPACKAGE__ token is missing on a renew package domain command */ + /** The __REMOVEPACKAGE__ token is missing on a package domain command */ public static class MissingRemovePackageTokenOnPackageDomainException extends AssociationProhibitsOperationException { MissingRemovePackageTokenOnPackageDomainException() { - super("Domains that are inside packages cannot be explicitly renewed"); + super("Domains that are inside packages cannot be explicitly renewed or transferred"); } } diff --git a/core/src/test/java/google/registry/flows/domain/DomainTransferRequestFlowTest.java b/core/src/test/java/google/registry/flows/domain/DomainTransferRequestFlowTest.java index cb413a5c4..4c631e804 100644 --- a/core/src/test/java/google/registry/flows/domain/DomainTransferRequestFlowTest.java +++ b/core/src/test/java/google/registry/flows/domain/DomainTransferRequestFlowTest.java @@ -21,6 +21,9 @@ import static com.google.common.truth.Truth8.assertThat; import static google.registry.batch.AsyncTaskEnqueuer.PARAM_REQUESTED_TIME; import static google.registry.batch.AsyncTaskEnqueuer.PARAM_RESOURCE_KEY; import static google.registry.batch.AsyncTaskEnqueuer.QUEUE_ASYNC_ACTIONS; +import static google.registry.model.billing.BillingEvent.RenewalPriceBehavior.DEFAULT; +import static google.registry.model.billing.BillingEvent.RenewalPriceBehavior.SPECIFIED; +import static google.registry.model.domain.token.AllocationToken.TokenType.PACKAGE; import static google.registry.model.domain.token.AllocationToken.TokenType.SINGLE_USE; import static google.registry.model.domain.token.AllocationToken.TokenType.UNLIMITED_USE; import static google.registry.model.reporting.DomainTransactionRecord.TransactionReportField.TRANSFER_SUCCESSFUL; @@ -61,6 +64,7 @@ import com.google.common.collect.Iterables; import com.google.common.collect.Maps; import com.google.common.collect.Sets; import com.google.common.collect.Streams; +import com.google.common.truth.Truth8; import com.googlecode.objectify.Key; import google.registry.batch.ResaveEntityAction; import google.registry.flows.EppException; @@ -85,6 +89,7 @@ import google.registry.flows.domain.token.AllocationTokenFlowUtils.AllocationTok import google.registry.flows.domain.token.AllocationTokenFlowUtils.AllocationTokenNotValidForTldException; import google.registry.flows.domain.token.AllocationTokenFlowUtils.AlreadyRedeemedAllocationTokenException; import google.registry.flows.domain.token.AllocationTokenFlowUtils.InvalidAllocationTokenException; +import google.registry.flows.domain.token.AllocationTokenFlowUtils.MissingRemovePackageTokenOnPackageDomainException; import google.registry.flows.exceptions.AlreadyPendingTransferException; import google.registry.flows.exceptions.InvalidTransferPeriodValueException; import google.registry.flows.exceptions.MissingTransferRequestAuthInfoException; @@ -1700,13 +1705,15 @@ class DomainTransferRequestFlowTest .setDomainName("example.tld") .build()); doSuccessfulTest( - "domain_transfer_request_allocation_token.xml", "domain_transfer_request_response.xml"); + "domain_transfer_request_allocation_token.xml", + "domain_transfer_request_response.xml", + ImmutableMap.of("TOKEN", "abc123")); } @Test void testFailure_invalidAllocationToken() throws Exception { setupDomain("example", "tld"); - setEppInput("domain_transfer_request_allocation_token.xml"); + setEppInput("domain_transfer_request_allocation_token.xml", ImmutableMap.of("TOKEN", "abc123")); EppException thrown = assertThrows(InvalidAllocationTokenException.class, this::runFlow); assertAboutEppExceptions().that(thrown).marshalsToXml(); } @@ -1720,7 +1727,7 @@ class DomainTransferRequestFlowTest .setTokenType(SINGLE_USE) .setDomainName("otherdomain.tld") .build()); - setEppInput("domain_transfer_request_allocation_token.xml"); + setEppInput("domain_transfer_request_allocation_token.xml", ImmutableMap.of("TOKEN", "abc123")); EppException thrown = assertThrows(AllocationTokenNotValidForDomainException.class, this::runFlow); assertAboutEppExceptions().that(thrown).marshalsToXml(); @@ -1741,7 +1748,7 @@ class DomainTransferRequestFlowTest .put(clock.nowUtc().plusDays(60), TokenStatus.ENDED) .build()) .build()); - setEppInput("domain_transfer_request_allocation_token.xml"); + setEppInput("domain_transfer_request_allocation_token.xml", ImmutableMap.of("TOKEN", "abc123")); EppException thrown = assertThrows(AllocationTokenNotInPromotionException.class, this::runFlow); assertAboutEppExceptions().that(thrown).marshalsToXml(); } @@ -1762,7 +1769,7 @@ class DomainTransferRequestFlowTest .put(clock.nowUtc().plusDays(1), TokenStatus.ENDED) .build()) .build()); - setEppInput("domain_transfer_request_allocation_token.xml"); + setEppInput("domain_transfer_request_allocation_token.xml", ImmutableMap.of("TOKEN", "abc123")); EppException thrown = assertThrows(AllocationTokenNotValidForRegistrarException.class, this::runFlow); assertAboutEppExceptions().that(thrown).marshalsToXml(); @@ -1784,7 +1791,7 @@ class DomainTransferRequestFlowTest .put(clock.nowUtc().plusDays(1), TokenStatus.ENDED) .build()) .build()); - setEppInput("domain_transfer_request_allocation_token.xml"); + setEppInput("domain_transfer_request_allocation_token.xml", ImmutableMap.of("TOKEN", "abc123")); EppException thrown = assertThrows(AllocationTokenNotValidForTldException.class, this::runFlow); assertAboutEppExceptions().that(thrown).marshalsToXml(); } @@ -1800,9 +1807,89 @@ class DomainTransferRequestFlowTest .setTokenType(SINGLE_USE) .setRedemptionHistoryEntry(HistoryEntry.createVKey(historyEntryKey)) .build()); - setEppInput("domain_transfer_request_allocation_token.xml"); + setEppInput("domain_transfer_request_allocation_token.xml", ImmutableMap.of("TOKEN", "abc123")); EppException thrown = assertThrows(AlreadyRedeemedAllocationTokenException.class, this::runFlow); assertAboutEppExceptions().that(thrown).marshalsToXml(); } + + @Test + void testFailsPackageDomainInvalidAllocationToken() throws Exception { + AllocationToken token = + persistResource( + new AllocationToken.Builder() + .setToken("abc123") + .setTokenType(PACKAGE) + .setAllowedRegistrarIds(ImmutableSet.of("NewRegistrar")) + .setAllowedTlds(ImmutableSet.of("example", "tld")) + .setRenewalPriceBehavior(SPECIFIED) + .build()); + setupDomain("example", "tld"); + persistResource( + reloadResourceByForeignKey() + .asBuilder() + .setCurrentPackageToken(token.createVKey()) + .build()); + + setEppInput("domain_transfer_request_allocation_token.xml", ImmutableMap.of("TOKEN", "abc123")); + assertThrows(MissingRemovePackageTokenOnPackageDomainException.class, this::runFlow); + } + + @Test + void testFailsToTransferPackageDomainNoRemovePackageToken() throws Exception { + AllocationToken token = + persistResource( + new AllocationToken.Builder() + .setToken("abc123") + .setTokenType(PACKAGE) + .setAllowedRegistrarIds(ImmutableSet.of("NewRegistrar")) + .setAllowedTlds(ImmutableSet.of("example", "tld")) + .setRenewalPriceBehavior(SPECIFIED) + .build()); + setupDomain("example", "tld"); + persistResource( + reloadResourceByForeignKey() + .asBuilder() + .setCurrentPackageToken(token.createVKey()) + .build()); + + setEppInput("domain_transfer_request.xml"); + assertThrows(MissingRemovePackageTokenOnPackageDomainException.class, this::runFlow); + } + + @Test + void testSuccesfullyAppliesRemovePackageToken() throws Exception { + setupDomain("example", "tld"); + AllocationToken token = + persistResource( + new AllocationToken.Builder() + .setToken("abc123") + .setTokenType(PACKAGE) + .setAllowedRegistrarIds(ImmutableSet.of("TheRegistrar")) + .setAllowedTlds(ImmutableSet.of("tld")) + .setRenewalPriceBehavior(SPECIFIED) + .build()); + domain = loadByEntity(domain); + persistResource( + loadByKey(domain.getAutorenewBillingEvent()) + .asBuilder() + .setRenewalPriceBehavior(RenewalPriceBehavior.SPECIFIED) + .setRenewalPrice(Money.of(USD, new BigDecimal("10.00"))) + .build()); + persistResource( + reloadResourceByForeignKey() + .asBuilder() + .setCurrentPackageToken(token.createVKey()) + .build()); + + doSuccessfulTest( + "domain_transfer_request_allocation_token.xml", + "domain_transfer_request_response.xml", + ImmutableMap.of("TOKEN", "__REMOVEPACKAGE__")); + Domain domain = reloadResourceByForeignKey(); + Truth8.assertThat(domain.getCurrentPackageToken()).isEmpty(); + RenewalPriceBehavior priceBehavior = + loadByKey(domain.getAutorenewBillingEvent()).getRenewalPriceBehavior(); + assertThat(priceBehavior).isEqualTo(DEFAULT); + } } diff --git a/core/src/test/resources/google/registry/flows/domain/domain_transfer_request_allocation_token.xml b/core/src/test/resources/google/registry/flows/domain/domain_transfer_request_allocation_token.xml index 12c6800d1..6064c60e1 100644 --- a/core/src/test/resources/google/registry/flows/domain/domain_transfer_request_allocation_token.xml +++ b/core/src/test/resources/google/registry/flows/domain/domain_transfer_request_allocation_token.xml @@ -14,7 +14,7 @@ - abc123 + %TOKEN% ABC-12345 diff --git a/docs/flows.md b/docs/flows.md index b2a9378a5..d0c02fc91 100644 --- a/docs/flows.md +++ b/docs/flows.md @@ -498,7 +498,7 @@ comes in at the exact millisecond that the domain would have expired. * Resource status prohibits this operation. * The allocation token is not currently valid. * 2305 - * The __REMOVEPACKAGE__ token is missing on a renew package domain command + * The __REMOVEPACKAGE__ token is missing on a package domain command * The __REMOVEPACKAGE__ token is not allowed on non package domains * The allocation token is not valid for this domain. * The allocation token is not valid for this registrar. @@ -754,6 +754,7 @@ new ones with the correct approval time). * The allocation token is not valid for this registrar. * The allocation token is not valid for this TLD. * The allocation token was already redeemed. + * The __REMOVEPACKAGE__ token is missing on a package domain command * 2306 * Domain transfer period must be one year. * Domain transfer period must be zero or one year when using the superuser