From 68f975451e20a7206c6819b9bc83fb2bd05be815 Mon Sep 17 00:00:00 2001 From: sarahcaseybot Date: Tue, 26 Jul 2022 10:28:32 -0400 Subject: [PATCH] Implement EPP Allocation Token Extension for domain:renew (#1693) * Implement EPP Allocation Token Extension for domain:renew * Add more tests * Fix AllocationTokenFlowUtilsTest * Combine loadTokenAndValidateDomain with verifyAllocationTokenIfPresent * Change to Optional.empty * Remove unused variable --- .../flows/domain/DomainCreateFlow.java | 20 +- .../flows/domain/DomainRenewFlow.java | 57 ++++- .../token/AllocationTokenCustomLogic.java | 11 +- .../token/AllocationTokenFlowUtils.java | 65 ++++-- .../flows/domain/DomainRenewFlowTest.java | 179 +++++++++++++++ .../token/AllocationTokenFlowUtilsTest.java | 205 +++++++++++++++--- .../domain/domain_renew_allocationtoken.xml | 20 ++ docs/flows.md | 7 + 8 files changed, 481 insertions(+), 83 deletions(-) create mode 100644 core/src/test/resources/google/registry/flows/domain/domain_renew_allocationtoken.xml diff --git a/core/src/main/java/google/registry/flows/domain/DomainCreateFlow.java b/core/src/main/java/google/registry/flows/domain/DomainCreateFlow.java index da230f714..f6e7bbeba 100644 --- a/core/src/main/java/google/registry/flows/domain/DomainCreateFlow.java +++ b/core/src/main/java/google/registry/flows/domain/DomainCreateFlow.java @@ -262,7 +262,12 @@ public final class DomainCreateFlow implements TransactionalFlow { } boolean isSunriseCreate = hasSignedMarks && (tldState == START_DATE_SUNRISE); Optional allocationToken = - verifyAllocationTokenIfPresent(command, registry, registrarId, now); + allocationTokenFlowUtils.verifyAllocationTokenCreateIfPresent( + command, + registry, + registrarId, + now, + eppInput.getSingleExtension(AllocationTokenExtension.class)); boolean isAnchorTenant = isAnchorTenant( domainName, allocationToken, eppInput.getSingleExtension(MetadataExtension.class)); @@ -485,19 +490,6 @@ public final class DomainCreateFlow implements TransactionalFlow { throw new NoGeneralRegistrationsInCurrentPhaseException(); } - /** Verifies and returns the allocation token if one is specified, otherwise does nothing. */ - private Optional verifyAllocationTokenIfPresent( - DomainCommand.Create command, Registry registry, String registrarId, DateTime now) - throws EppException { - Optional extension = - eppInput.getSingleExtension(AllocationTokenExtension.class); - return Optional.ofNullable( - extension.isPresent() - ? allocationTokenFlowUtils.loadTokenAndValidateDomainCreate( - command, extension.get().getAllocationToken(), registry, registrarId, now) - : null); - } - private DomainHistory buildDomainHistory( DomainBase domain, Registry registry, DateTime now, Period period, Duration addGracePeriod) { // We ignore prober transactions diff --git a/core/src/main/java/google/registry/flows/domain/DomainRenewFlow.java b/core/src/main/java/google/registry/flows/domain/DomainRenewFlow.java index 832745d5d..4656c434c 100644 --- a/core/src/main/java/google/registry/flows/domain/DomainRenewFlow.java +++ b/core/src/main/java/google/registry/flows/domain/DomainRenewFlow.java @@ -51,6 +51,8 @@ import google.registry.flows.custom.DomainRenewFlowCustomLogic.BeforeResponsePar import google.registry.flows.custom.DomainRenewFlowCustomLogic.BeforeResponseReturnData; import google.registry.flows.custom.DomainRenewFlowCustomLogic.BeforeSaveParameters; import google.registry.flows.custom.EntityChanges; +import google.registry.flows.domain.token.AllocationTokenFlowUtils; +import google.registry.model.ImmutableObject; import google.registry.model.billing.BillingEvent; import google.registry.model.billing.BillingEvent.OneTime; import google.registry.model.billing.BillingEvent.Reason; @@ -68,6 +70,9 @@ import google.registry.model.domain.fee.FeeRenewCommandExtension; import google.registry.model.domain.fee.FeeTransformResponseExtension; import google.registry.model.domain.metadata.MetadataExtension; import google.registry.model.domain.rgp.GracePeriodStatus; +import google.registry.model.domain.token.AllocationToken; +import google.registry.model.domain.token.AllocationToken.TokenType; +import google.registry.model.domain.token.AllocationTokenExtension; import google.registry.model.eppcommon.AuthInfo; import google.registry.model.eppcommon.StatusValue; import google.registry.model.eppinput.EppInput; @@ -113,6 +118,18 @@ import org.joda.time.Duration; * @error {@link DomainFlowUtils.RegistrarMustBeActiveForThisOperationException} * @error {@link DomainFlowUtils.UnsupportedFeeAttributeException} * @error {@link DomainRenewFlow.IncorrectCurrentExpirationDateException} + * @error {@link + * google.registry.flows.domain.token.AllocationTokenFlowUtils.AllocationTokenNotValidForDomainException} + * @error {@link + * google.registry.flows.domain.token.AllocationTokenFlowUtils.InvalidAllocationTokenException} + * @error {@link + * google.registry.flows.domain.token.AllocationTokenFlowUtils.AllocationTokenNotInPromotionException} + * @error {@link + * google.registry.flows.domain.token.AllocationTokenFlowUtils.AllocationTokenNotValidForRegistrarException} + * @error {@link + * google.registry.flows.domain.token.AllocationTokenFlowUtils.AllocationTokenNotValidForTldException} + * @error {@link + * google.registry.flows.domain.token.AllocationTokenFlowUtils.AlreadyRedeemedAllocationTokenException} */ @ReportingSpec(ActivityReportField.DOMAIN_RENEW) public final class DomainRenewFlow implements TransactionalFlow { @@ -133,13 +150,15 @@ public final class DomainRenewFlow implements TransactionalFlow { @Inject @Superuser boolean isSuperuser; @Inject DomainHistory.Builder historyBuilder; @Inject EppResponse.Builder responseBuilder; + @Inject AllocationTokenFlowUtils allocationTokenFlowUtils; @Inject DomainRenewFlowCustomLogic flowCustomLogic; @Inject DomainPricingLogic pricingLogic; @Inject DomainRenewFlow() {} @Override public EppResponse run() throws EppException { - extensionManager.register(FeeRenewCommandExtension.class, MetadataExtension.class); + extensionManager.register( + FeeRenewCommandExtension.class, MetadataExtension.class, AllocationTokenExtension.class); flowCustomLogic.beforeValidation(); validateRegistrarIsLoggedIn(registrarId); verifyRegistrarIsActive(registrarId); @@ -148,6 +167,13 @@ public final class DomainRenewFlow implements TransactionalFlow { Renew command = (Renew) resourceCommand; // Loads the target resource if it exists DomainBase existingDomain = loadAndVerifyExistence(DomainBase.class, targetId, now); + Optional allocationToken = + allocationTokenFlowUtils.verifyAllocationTokenIfPresent( + existingDomain, + Registry.get(existingDomain.getTld()), + registrarId, + now, + eppInput.getSingleExtension(AllocationTokenExtension.class)); verifyRenewAllowed(authInfo, existingDomain, command); int years = command.getPeriod().getValue(); DateTime newExpirationTime = @@ -176,7 +202,8 @@ public final class DomainRenewFlow implements TransactionalFlow { String tld = existingDomain.getTld(); // Bill for this explicit renew itself. BillingEvent.OneTime explicitRenewEvent = - createRenewBillingEvent(tld, feesAndCredits.getTotalCost(), years, domainHistoryKey, now); + createRenewBillingEvent( + tld, feesAndCredits.getTotalCost(), years, domainHistoryKey, allocationToken, now); // Create a new autorenew billing event and poll message starting at the new expiration time. BillingEvent.Recurring newAutorenewEvent = newAutorenewBillingEvent(existingDomain) @@ -212,6 +239,14 @@ public final class DomainRenewFlow implements TransactionalFlow { DomainHistory domainHistory = buildDomainHistory( newDomain, now, command.getPeriod(), registry.getRenewGracePeriodLength()); + ImmutableSet.Builder entitiesToSave = new ImmutableSet.Builder<>(); + entitiesToSave.add( + newDomain, domainHistory, explicitRenewEvent, newAutorenewEvent, newAutorenewPollMessage); + if (allocationToken.isPresent() + && TokenType.SINGLE_USE.equals(allocationToken.get().getTokenType())) { + entitiesToSave.add( + allocationTokenFlowUtils.redeemToken(allocationToken.get(), domainHistory.createVKey())); + } EntityChanges entityChanges = flowCustomLogic.beforeSave( BeforeSaveParameters.newBuilder() @@ -221,15 +256,7 @@ public final class DomainRenewFlow implements TransactionalFlow { .setYears(years) .setHistoryEntry(domainHistory) .setEntityChanges( - EntityChanges.newBuilder() - .setSaves( - ImmutableSet.of( - newDomain, - domainHistory, - explicitRenewEvent, - newAutorenewEvent, - newAutorenewPollMessage)) - .build()) + EntityChanges.newBuilder().setSaves(entitiesToSave.build()).build()) .build()); BeforeResponseReturnData responseData = flowCustomLogic.beforeResponse( @@ -290,7 +317,12 @@ public final class DomainRenewFlow implements TransactionalFlow { } private OneTime createRenewBillingEvent( - String tld, Money renewCost, int years, Key domainHistoryKey, DateTime now) { + String tld, + Money renewCost, + int years, + Key domainHistoryKey, + Optional allocationToken, + DateTime now) { return new BillingEvent.OneTime.Builder() .setReason(Reason.RENEW) .setTargetId(targetId) @@ -298,6 +330,7 @@ public final class DomainRenewFlow implements TransactionalFlow { .setPeriodYears(years) .setCost(renewCost) .setEventTime(now) + .setAllocationToken(allocationToken.map(AllocationToken::createVKey).orElse(null)) .setBillingTime(now.plus(Registry.get(tld).getRenewGracePeriodLength())) .setParent(domainHistoryKey) .build(); diff --git a/core/src/main/java/google/registry/flows/domain/token/AllocationTokenCustomLogic.java b/core/src/main/java/google/registry/flows/domain/token/AllocationTokenCustomLogic.java index a7b644b58..2ee8a7210 100644 --- a/core/src/main/java/google/registry/flows/domain/token/AllocationTokenCustomLogic.java +++ b/core/src/main/java/google/registry/flows/domain/token/AllocationTokenCustomLogic.java @@ -19,6 +19,7 @@ import com.google.common.collect.ImmutableMap; import com.google.common.collect.Maps; import com.google.common.net.InternetDomainName; import google.registry.flows.EppException; +import google.registry.model.domain.DomainBase; import google.registry.model.domain.DomainCommand; import google.registry.model.domain.token.AllocationToken; import google.registry.model.tld.Registry; @@ -31,7 +32,7 @@ import org.joda.time.DateTime; */ public class AllocationTokenCustomLogic { - /** Performs additional custom logic for validating a token. */ + /** Performs additional custom logic for validating a token on a domain create. */ public AllocationToken validateToken( DomainCommand.Create command, AllocationToken token, @@ -43,6 +44,14 @@ public class AllocationTokenCustomLogic { return token; } + /** Performs additional custom logic for validating a token on an existing domain. */ + public AllocationToken validateToken( + DomainBase domain, AllocationToken token, Registry registry, String registrarId, DateTime now) + throws EppException { + // Do nothing. + return token; + } + /** Performs additional custom logic for performing domain checks using a token. */ public ImmutableMap checkDomainsWithToken( ImmutableList domainNames, 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 0da6c5378..3ada1b309 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 @@ -26,10 +26,12 @@ import google.registry.flows.EppException; import google.registry.flows.EppException.AssociationProhibitsOperationException; import google.registry.flows.EppException.AuthorizationErrorException; import google.registry.flows.EppException.StatusProhibitsOperationException; +import google.registry.model.domain.DomainBase; import google.registry.model.domain.DomainCommand; import google.registry.model.domain.token.AllocationToken; import google.registry.model.domain.token.AllocationToken.TokenStatus; import google.registry.model.domain.token.AllocationToken.TokenType; +import google.registry.model.domain.token.AllocationTokenExtension; import google.registry.model.reporting.HistoryEntry; import google.registry.model.tld.Registry; import google.registry.persistence.VKey; @@ -48,30 +50,6 @@ public class AllocationTokenFlowUtils { this.tokenCustomLogic = tokenCustomLogic; } - /** - * Loads an allocation token given a string and verifies that the token is valid for the domain - * create request. - * - * @return the loaded {@link AllocationToken} for that string. - * @throws EppException if the token doesn't exist, is already redeemed, or is otherwise invalid - * for this request. - */ - public AllocationToken loadTokenAndValidateDomainCreate( - DomainCommand.Create command, - String token, - Registry registry, - String registrarId, - DateTime now) - throws EppException { - AllocationToken tokenEntity = loadToken(token); - validateToken( - InternetDomainName.from(command.getFullyQualifiedDomainName()), - tokenEntity, - registrarId, - now); - return tokenCustomLogic.validateToken(command, tokenEntity, registry, registrarId, now); - } - /** * Checks if the allocation token applies to the given domain names, used for domain checks. * @@ -170,6 +148,45 @@ public class AllocationTokenFlowUtils { return maybeTokenEntity.get(); } + /** Verifies and returns the allocation token if one is specified, otherwise does nothing. */ + public Optional verifyAllocationTokenCreateIfPresent( + DomainCommand.Create command, + Registry registry, + String registrarId, + DateTime now, + Optional extension) + throws EppException { + if (!extension.isPresent()) { + return Optional.empty(); + } + AllocationToken tokenEntity = loadToken(extension.get().getAllocationToken()); + validateToken( + InternetDomainName.from(command.getFullyQualifiedDomainName()), + tokenEntity, + registrarId, + now); + return Optional.of( + tokenCustomLogic.validateToken(command, tokenEntity, registry, registrarId, now)); + } + + /** Verifies and returns the allocation token if one is specified, otherwise does nothing. */ + public Optional verifyAllocationTokenIfPresent( + DomainBase existingDomain, + Registry registry, + String registrarId, + DateTime now, + Optional extension) + throws EppException { + if (!extension.isPresent()) { + return Optional.empty(); + } + AllocationToken tokenEntity = loadToken(extension.get().getAllocationToken()); + validateToken( + InternetDomainName.from(existingDomain.getDomainName()), tokenEntity, registrarId, now); + return Optional.of( + tokenCustomLogic.validateToken(existingDomain, tokenEntity, registry, registrarId, now)); + } + // Note: exception messages should be <= 32 characters long for domain check results /** The allocation token is not currently valid. */ diff --git a/core/src/test/java/google/registry/flows/domain/DomainRenewFlowTest.java b/core/src/test/java/google/registry/flows/domain/DomainRenewFlowTest.java index 2f9003bce..ef88b905d 100644 --- a/core/src/test/java/google/registry/flows/domain/DomainRenewFlowTest.java +++ b/core/src/test/java/google/registry/flows/domain/DomainRenewFlowTest.java @@ -15,10 +15,13 @@ package google.registry.flows.domain; import static com.google.common.truth.Truth.assertThat; +import static com.google.common.truth.Truth8.assertThat; import static google.registry.flows.domain.DomainTransferFlowTestCase.persistWithPendingTransfer; import static google.registry.model.billing.BillingEvent.RenewalPriceBehavior.DEFAULT; import static google.registry.model.billing.BillingEvent.RenewalPriceBehavior.NONPREMIUM; import static google.registry.model.billing.BillingEvent.RenewalPriceBehavior.SPECIFIED; +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.persistence.transaction.TransactionManagerFactory.tm; import static google.registry.testing.DatabaseHelper.assertBillingEvents; import static google.registry.testing.DatabaseHelper.assertPollMessages; @@ -46,6 +49,7 @@ import static org.junit.jupiter.api.Assertions.assertThrows; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; import com.google.common.collect.ImmutableSortedMap; +import com.googlecode.objectify.Key; import google.registry.flows.EppException; import google.registry.flows.EppRequestSource; import google.registry.flows.FlowUtils.NotLoggedInException; @@ -64,6 +68,12 @@ import google.registry.flows.domain.DomainFlowUtils.NotAuthorizedForTldException import google.registry.flows.domain.DomainFlowUtils.RegistrarMustBeActiveForThisOperationException; import google.registry.flows.domain.DomainFlowUtils.UnsupportedFeeAttributeException; import google.registry.flows.domain.DomainRenewFlow.IncorrectCurrentExpirationDateException; +import google.registry.flows.domain.token.AllocationTokenFlowUtils.AllocationTokenNotInPromotionException; +import google.registry.flows.domain.token.AllocationTokenFlowUtils.AllocationTokenNotValidForDomainException; +import google.registry.flows.domain.token.AllocationTokenFlowUtils.AllocationTokenNotValidForRegistrarException; +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.exceptions.ResourceStatusProhibitsOperationException; import google.registry.model.billing.BillingEvent; import google.registry.model.billing.BillingEvent.Flag; @@ -73,6 +83,8 @@ import google.registry.model.domain.DomainBase; import google.registry.model.domain.DomainHistory; import google.registry.model.domain.GracePeriod; import google.registry.model.domain.rgp.GracePeriodStatus; +import google.registry.model.domain.token.AllocationToken; +import google.registry.model.domain.token.AllocationToken.TokenStatus; import google.registry.model.eppcommon.StatusValue; import google.registry.model.poll.PollMessage; import google.registry.model.registrar.Registrar; @@ -81,6 +93,8 @@ import google.registry.model.reporting.DomainTransactionRecord; import google.registry.model.reporting.DomainTransactionRecord.TransactionReportField; import google.registry.model.reporting.HistoryEntry; import google.registry.model.tld.Registry; +import google.registry.persistence.VKey; +import google.registry.testing.DatabaseHelper; import google.registry.testing.SetClockExtension; import java.util.Map; import javax.annotation.Nullable; @@ -321,6 +335,13 @@ class DomainRenewFlowTest extends ResourceFlowTestCase tm().loadByKey(VKey.create(AllocationToken.class, token))); + assertThat(reloadedToken.isRedeemed()).isFalse(); + } + @Test void testNotLoggedIn() { sessionMetadata.setRegistrarId(null); @@ -577,6 +598,164 @@ class DomainRenewFlowTest extends ResourceFlowTestCasenaturalOrder() + .put(START_OF_TIME, TokenStatus.NOT_STARTED) + .put(clock.nowUtc().plusDays(1), TokenStatus.VALID) + .put(clock.nowUtc().plusDays(60), TokenStatus.ENDED) + .build()) + .build()); + assertAboutEppExceptions() + .that(assertThrows(AllocationTokenNotInPromotionException.class, this::runFlow)) + .marshalsToXml(); + } + + @Test + void testFailure_promoTokenNotValidForRegistrar() throws Exception { + setEppInput( + "domain_renew_allocationtoken.xml", ImmutableMap.of("DOMAIN", "example.tld", "YEARS", "2")); + persistDomain(); + persistResource( + new AllocationToken.Builder() + .setToken("abc123") + .setTokenType(UNLIMITED_USE) + .setAllowedRegistrarIds(ImmutableSet.of("someClientId")) + .setDiscountFraction(0.5) + .setTokenStatusTransitions( + ImmutableSortedMap.naturalOrder() + .put(START_OF_TIME, TokenStatus.NOT_STARTED) + .put(clock.nowUtc().minusDays(1), TokenStatus.VALID) + .put(clock.nowUtc().plusDays(1), TokenStatus.ENDED) + .build()) + .build()); + assertAboutEppExceptions() + .that(assertThrows(AllocationTokenNotValidForRegistrarException.class, this::runFlow)) + .marshalsToXml(); + assertAllocationTokenWasNotRedeemed("abc123"); + } + + @Test + void testFailure_promoTokenNotValidForTld() throws Exception { + setEppInput( + "domain_renew_allocationtoken.xml", ImmutableMap.of("DOMAIN", "example.tld", "YEARS", "2")); + persistDomain(); + persistResource( + new AllocationToken.Builder() + .setToken("abc123") + .setTokenType(UNLIMITED_USE) + .setAllowedTlds(ImmutableSet.of("example")) + .setDiscountFraction(0.5) + .setTokenStatusTransitions( + ImmutableSortedMap.naturalOrder() + .put(START_OF_TIME, TokenStatus.NOT_STARTED) + .put(clock.nowUtc().minusDays(1), TokenStatus.VALID) + .put(clock.nowUtc().plusDays(1), TokenStatus.ENDED) + .build()) + .build()); + assertAboutEppExceptions() + .that(assertThrows(AllocationTokenNotValidForTldException.class, this::runFlow)) + .marshalsToXml(); + assertAllocationTokenWasNotRedeemed("abc123"); + } + + @Test + void testFailure_alreadyRedemeedAllocationToken() throws Exception { + setEppInput( + "domain_renew_allocationtoken.xml", ImmutableMap.of("DOMAIN", "example.tld", "YEARS", "2")); + persistDomain(); + DomainBase domain = persistActiveDomain("foo.tld"); + Key historyEntryKey = Key.create(Key.create(domain), HistoryEntry.class, 505L); + persistResource( + new AllocationToken.Builder() + .setToken("abc123") + .setTokenType(SINGLE_USE) + .setRedemptionHistoryEntry(HistoryEntry.createVKey(historyEntryKey)) + .build()); + clock.advanceOneMilli(); + EppException thrown = + assertThrows(AlreadyRedeemedAllocationTokenException.class, this::runFlow); + assertAboutEppExceptions().that(thrown).marshalsToXml(); + } + @Test void testFailure_suspendedRegistrarCantRenewDomain() { doFailingTest_invalidRegistrarState(State.SUSPENDED); diff --git a/core/src/test/java/google/registry/flows/domain/token/AllocationTokenFlowUtilsTest.java b/core/src/test/java/google/registry/flows/domain/token/AllocationTokenFlowUtilsTest.java index 49342b84f..6ec926750 100644 --- a/core/src/test/java/google/registry/flows/domain/token/AllocationTokenFlowUtilsTest.java +++ b/core/src/test/java/google/registry/flows/domain/token/AllocationTokenFlowUtilsTest.java @@ -22,6 +22,7 @@ import static google.registry.model.domain.token.AllocationToken.TokenStatus.VAL 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.testing.DatabaseHelper.createTld; +import static google.registry.testing.DatabaseHelper.newDomainBase; import static google.registry.testing.DatabaseHelper.persistActiveDomain; import static google.registry.testing.DatabaseHelper.persistResource; import static google.registry.testing.EppExceptionSubject.assertAboutEppExceptions; @@ -47,9 +48,11 @@ import google.registry.model.domain.DomainBase; import google.registry.model.domain.DomainCommand; import google.registry.model.domain.token.AllocationToken; import google.registry.model.domain.token.AllocationToken.TokenStatus; +import google.registry.model.domain.token.AllocationTokenExtension; import google.registry.model.reporting.HistoryEntry; import google.registry.model.tld.Registry; import google.registry.testing.AppEngineExtension; +import java.util.Optional; import org.joda.time.DateTime; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -64,98 +67,195 @@ class AllocationTokenFlowUtilsTest { @RegisterExtension final AppEngineExtension appEngine = AppEngineExtension.builder().withCloudSql().build(); + private final AllocationTokenExtension allocationTokenExtension = + mock(AllocationTokenExtension.class); + @BeforeEach void beforeEach() { createTld("tld"); } @Test - void test_validateToken_successfullyVerifiesValidToken() throws Exception { + void test_validateToken_successfullyVerifiesValidTokenOnCreate() throws Exception { AllocationToken token = persistResource( new AllocationToken.Builder().setToken("tokeN").setTokenType(SINGLE_USE).build()); + when(allocationTokenExtension.getAllocationToken()).thenReturn("tokeN"); assertThat( - flowUtils.loadTokenAndValidateDomainCreate( - createCommand("blah.tld"), - "tokeN", - Registry.get("tld"), - "TheRegistrar", - DateTime.now(UTC))) + flowUtils + .verifyAllocationTokenCreateIfPresent( + createCommand("blah.tld"), + Registry.get("tld"), + "TheRegistrar", + DateTime.now(UTC), + Optional.of(allocationTokenExtension)) + .get()) .isEqualTo(token); } @Test - void test_validateToken_failsOnNonexistentToken() { - assertValidateThrowsEppException(InvalidAllocationTokenException.class); + void test_validateToken_successfullyVerifiesValidTokenExistingDomain() throws Exception { + AllocationToken token = + persistResource( + new AllocationToken.Builder().setToken("tokeN").setTokenType(SINGLE_USE).build()); + when(allocationTokenExtension.getAllocationToken()).thenReturn("tokeN"); + assertThat( + flowUtils + .verifyAllocationTokenIfPresent( + newDomainBase("blah.tld"), + Registry.get("tld"), + "TheRegistrar", + DateTime.now(UTC), + Optional.of(allocationTokenExtension)) + .get()) + .isEqualTo(token); } @Test - void test_validateToken_failsOnNullToken() { + void test_validateTokenCreate_failsOnNonexistentToken() { + assertValidateCreateThrowsEppException(InvalidAllocationTokenException.class); + } + + @Test + void test_validateTokenExistingDomain_failsOnNonexistentToken() { + assertValidateExistingDomainThrowsEppException(InvalidAllocationTokenException.class); + } + + @Test + void test_validateTokenCreate_failsOnNullToken() { assertAboutEppExceptions() .that( assertThrows( InvalidAllocationTokenException.class, () -> - flowUtils.loadTokenAndValidateDomainCreate( + flowUtils.verifyAllocationTokenCreateIfPresent( createCommand("blah.tld"), - null, Registry.get("tld"), "TheRegistrar", - DateTime.now(UTC)))) + DateTime.now(UTC), + Optional.of(allocationTokenExtension)))) .marshalsToXml(); } @Test - void test_validateToken_callsCustomLogic() { + void test_validateTokenExistingDomain_failsOnNullToken() { + assertAboutEppExceptions() + .that( + assertThrows( + InvalidAllocationTokenException.class, + () -> + flowUtils.verifyAllocationTokenIfPresent( + newDomainBase("blah.tld"), + Registry.get("tld"), + "TheRegistrar", + DateTime.now(UTC), + Optional.of(allocationTokenExtension)))) + .marshalsToXml(); + } + + @Test + void test_validateTokenCreate_callsCustomLogic() { AllocationTokenFlowUtils failingFlowUtils = new AllocationTokenFlowUtils(new FailingAllocationTokenCustomLogic()); persistResource( new AllocationToken.Builder().setToken("tokeN").setTokenType(SINGLE_USE).build()); + when(allocationTokenExtension.getAllocationToken()).thenReturn("tokeN"); Exception thrown = assertThrows( IllegalStateException.class, () -> - failingFlowUtils.loadTokenAndValidateDomainCreate( + failingFlowUtils.verifyAllocationTokenCreateIfPresent( createCommand("blah.tld"), - "tokeN", Registry.get("tld"), "TheRegistrar", - DateTime.now(UTC))); + DateTime.now(UTC), + Optional.of(allocationTokenExtension))); assertThat(thrown).hasMessageThat().isEqualTo("failed for tests"); } @Test - void test_validateToken_invalidForClientId() { + void test_validateTokenExistingDomain_callsCustomLogic() { + AllocationTokenFlowUtils failingFlowUtils = + new AllocationTokenFlowUtils(new FailingAllocationTokenCustomLogic()); + persistResource( + new AllocationToken.Builder().setToken("tokeN").setTokenType(SINGLE_USE).build()); + when(allocationTokenExtension.getAllocationToken()).thenReturn("tokeN"); + Exception thrown = + assertThrows( + IllegalStateException.class, + () -> + failingFlowUtils.verifyAllocationTokenIfPresent( + newDomainBase("blah.tld"), + Registry.get("tld"), + "TheRegistrar", + DateTime.now(UTC), + Optional.of(allocationTokenExtension))); + assertThat(thrown).hasMessageThat().isEqualTo("failed for tests"); + } + + @Test + void test_validateTokenCreate_invalidForClientId() { persistResource( createOneMonthPromoTokenBuilder(DateTime.now(UTC).minusDays(1)) .setAllowedRegistrarIds(ImmutableSet.of("NewRegistrar")) .build()); - assertValidateThrowsEppException(AllocationTokenNotValidForRegistrarException.class); + assertValidateCreateThrowsEppException(AllocationTokenNotValidForRegistrarException.class); } @Test - void test_validateToken_invalidForTld() { + void test_validateTokenExistingDomain_invalidForClientId() { + persistResource( + createOneMonthPromoTokenBuilder(DateTime.now(UTC).minusDays(1)) + .setAllowedRegistrarIds(ImmutableSet.of("NewRegistrar")) + .build()); + assertValidateExistingDomainThrowsEppException( + AllocationTokenNotValidForRegistrarException.class); + } + + @Test + void test_validateTokenCreate_invalidForTld() { persistResource( createOneMonthPromoTokenBuilder(DateTime.now(UTC).minusDays(1)) .setAllowedTlds(ImmutableSet.of("nottld")) .build()); - assertValidateThrowsEppException(AllocationTokenNotValidForTldException.class); + assertValidateCreateThrowsEppException(AllocationTokenNotValidForTldException.class); } @Test - void test_validateToken_beforePromoStart() { + void test_validateTokenExistingDomain_invalidForTld() { + persistResource( + createOneMonthPromoTokenBuilder(DateTime.now(UTC).minusDays(1)) + .setAllowedTlds(ImmutableSet.of("nottld")) + .build()); + assertValidateExistingDomainThrowsEppException(AllocationTokenNotValidForTldException.class); + } + + @Test + void test_validateTokenCreate_beforePromoStart() { persistResource(createOneMonthPromoTokenBuilder(DateTime.now(UTC).plusDays(1)).build()); - assertValidateThrowsEppException(AllocationTokenNotInPromotionException.class); + assertValidateCreateThrowsEppException(AllocationTokenNotInPromotionException.class); } @Test - void test_validateToken_afterPromoEnd() { + void test_validateTokenExistingDomain_beforePromoStart() { + persistResource(createOneMonthPromoTokenBuilder(DateTime.now(UTC).plusDays(1)).build()); + assertValidateExistingDomainThrowsEppException(AllocationTokenNotInPromotionException.class); + } + + @Test + void test_validateTokenCreate_afterPromoEnd() { persistResource(createOneMonthPromoTokenBuilder(DateTime.now(UTC).minusMonths(2)).build()); - assertValidateThrowsEppException(AllocationTokenNotInPromotionException.class); + assertValidateCreateThrowsEppException(AllocationTokenNotInPromotionException.class); } @Test - void test_validateToken_promoCancelled() { + void test_validateTokenExistingDomain_afterPromoEnd() { + persistResource(createOneMonthPromoTokenBuilder(DateTime.now(UTC).minusMonths(2)).build()); + assertValidateExistingDomainThrowsEppException(AllocationTokenNotInPromotionException.class); + } + + @Test + void test_validateTokenCreate_promoCancelled() { // the promo would be valid but it was cancelled 12 hours ago persistResource( createOneMonthPromoTokenBuilder(DateTime.now(UTC).minusDays(1)) @@ -166,7 +266,22 @@ class AllocationTokenFlowUtilsTest { .put(DateTime.now(UTC).minusHours(12), CANCELLED) .build()) .build()); - assertValidateThrowsEppException(AllocationTokenNotInPromotionException.class); + assertValidateCreateThrowsEppException(AllocationTokenNotInPromotionException.class); + } + + @Test + void test_validateTokenExistingDomain_promoCancelled() { + // the promo would be valid but it was cancelled 12 hours ago + persistResource( + createOneMonthPromoTokenBuilder(DateTime.now(UTC).minusDays(1)) + .setTokenStatusTransitions( + ImmutableSortedMap.naturalOrder() + .put(START_OF_TIME, NOT_STARTED) + .put(DateTime.now(UTC).minusMonths(1), VALID) + .put(DateTime.now(UTC).minusHours(12), CANCELLED) + .build()) + .build()); + assertValidateExistingDomainThrowsEppException(AllocationTokenNotInPromotionException.class); } @Test @@ -259,18 +374,33 @@ class AllocationTokenFlowUtilsTest { .inOrder(); } - private void assertValidateThrowsEppException(Class clazz) { + private void assertValidateCreateThrowsEppException(Class clazz) { assertAboutEppExceptions() .that( assertThrows( clazz, () -> - flowUtils.loadTokenAndValidateDomainCreate( + flowUtils.verifyAllocationTokenCreateIfPresent( createCommand("blah.tld"), - "tokeN", Registry.get("tld"), "TheRegistrar", - DateTime.now(UTC)))) + DateTime.now(UTC), + Optional.of(allocationTokenExtension)))) + .marshalsToXml(); + } + + private void assertValidateExistingDomainThrowsEppException(Class clazz) { + assertAboutEppExceptions() + .that( + assertThrows( + clazz, + () -> + flowUtils.verifyAllocationTokenIfPresent( + newDomainBase("blah.tld"), + Registry.get("tld"), + "TheRegistrar", + DateTime.now(UTC), + Optional.of(allocationTokenExtension)))) .marshalsToXml(); } @@ -280,7 +410,8 @@ class AllocationTokenFlowUtilsTest { return command; } - private static AllocationToken.Builder createOneMonthPromoTokenBuilder(DateTime promoStart) { + private AllocationToken.Builder createOneMonthPromoTokenBuilder(DateTime promoStart) { + when(allocationTokenExtension.getAllocationToken()).thenReturn("tokeN"); return new AllocationToken.Builder() .setToken("tokeN") .setTokenType(UNLIMITED_USE) @@ -305,6 +436,16 @@ class AllocationTokenFlowUtilsTest { throw new IllegalStateException("failed for tests"); } + @Override + public AllocationToken validateToken( + DomainBase domain, + AllocationToken token, + Registry registry, + String registrarId, + DateTime now) { + throw new IllegalStateException("failed for tests"); + } + @Override public ImmutableMap checkDomainsWithToken( ImmutableList domainNames, diff --git a/core/src/test/resources/google/registry/flows/domain/domain_renew_allocationtoken.xml b/core/src/test/resources/google/registry/flows/domain/domain_renew_allocationtoken.xml new file mode 100644 index 000000000..e79750b89 --- /dev/null +++ b/core/src/test/resources/google/registry/flows/domain/domain_renew_allocationtoken.xml @@ -0,0 +1,20 @@ + + + + + %DOMAIN% + 2000-04-03 + %YEARS% + + + + + abc123 + + + ABC-12345 + + diff --git a/docs/flows.md b/docs/flows.md index 303ce7806..bb9c3b59a 100644 --- a/docs/flows.md +++ b/docs/flows.md @@ -489,10 +489,17 @@ comes in at the exact millisecond that the domain would have expired. * Registrar is missing the billing account map for this currency type. * Registrar is not authorized to access this TLD. * Registrar must be active in order to perform this operation. + * The allocation token is invalid. * 2303 * Resource with this id does not exist. * 2304 * Resource status prohibits this operation. + * The allocation token is not currently valid. +* 2305 + * The allocation token is not valid for this domain. + * The allocation token is not valid for this registrar. + * The allocation token is not valid for this TLD. + * The allocation token was already redeemed. * 2306 * Periods for domain registrations must be specified in years. * The requested fees cannot be provided in the requested currency.