diff --git a/docs/flows.md b/docs/flows.md index c1323aa54..547cc597f 100644 --- a/docs/flows.md +++ b/docs/flows.md @@ -344,6 +344,7 @@ An EPP flow that creates a new domain resource. * 2303 * Resource linked to this domain does not exist. * 2304 + * The allocation token is not currently valid. * The claims period for this TLD has ended. * Requested domain is reserved. * Linked resource in pending delete prohibits operation. @@ -355,6 +356,8 @@ An EPP flow that creates a new domain resource. * Registrant is not whitelisted for this TLD. * Requested domain does not require a claims notice. * 2305 + * 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 * Anchor tenant domain create is for the wrong number of years. diff --git a/java/google/registry/flows/domain/DomainCreateFlow.java b/java/google/registry/flows/domain/DomainCreateFlow.java index 8ccb92527..089450f17 100644 --- a/java/google/registry/flows/domain/DomainCreateFlow.java +++ b/java/google/registry/flows/domain/DomainCreateFlow.java @@ -114,6 +114,9 @@ import org.joda.time.Duration; /** * An EPP flow that creates a new domain resource. * + * @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} * @error {@link google.registry.flows.domain.token.AllocationTokenFlowUtils.InvalidAllocationTokenException} * @error {@link google.registry.flows.exceptions.OnlyToolCanPassMetadataException} @@ -445,7 +448,7 @@ public class DomainCreateFlow implements TransactionalFlow { eppInput.getSingleExtension(AllocationTokenExtension.class); return Optional.ofNullable( extension.isPresent() - ? allocationTokenFlowUtils.loadAndVerifyToken( + ? allocationTokenFlowUtils.loadTokenAndValidateDomainCreate( command, extension.get().getAllocationToken(), registry, clientId, now) : null); } diff --git a/java/google/registry/flows/domain/token/AllocationTokenCustomLogic.java b/java/google/registry/flows/domain/token/AllocationTokenCustomLogic.java index 10f36f792..5598fb47e 100644 --- a/java/google/registry/flows/domain/token/AllocationTokenCustomLogic.java +++ b/java/google/registry/flows/domain/token/AllocationTokenCustomLogic.java @@ -31,8 +31,8 @@ import org.joda.time.DateTime; */ public class AllocationTokenCustomLogic { - /** Performs additional custom logic for verifying a token. */ - public AllocationToken verifyToken( + /** Performs additional custom logic for validating a token. */ + public AllocationToken validateToken( DomainCommand.Create command, AllocationToken token, Registry registry, diff --git a/java/google/registry/flows/domain/token/AllocationTokenFlowUtils.java b/java/google/registry/flows/domain/token/AllocationTokenFlowUtils.java index c85c8906c..7f7831f7f 100644 --- a/java/google/registry/flows/domain/token/AllocationTokenFlowUtils.java +++ b/java/google/registry/flows/domain/token/AllocationTokenFlowUtils.java @@ -25,8 +25,10 @@ import com.googlecode.objectify.Key; import google.registry.flows.EppException; import google.registry.flows.EppException.AssociationProhibitsOperationException; import google.registry.flows.EppException.ParameterValueSyntaxErrorException; +import google.registry.flows.EppException.StatusProhibitsOperationException; 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.registry.Registry; import google.registry.model.reporting.HistoryEntry; @@ -46,16 +48,19 @@ public class AllocationTokenFlowUtils { } /** - * Verifies that a given allocation token string is valid. + * 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 InvalidAllocationTokenException if the token doesn't exist. + * @throws EppException if the token doesn't exist, is already redeemed, or is otherwise invalid + * for this request. */ - public AllocationToken loadAndVerifyToken( + public AllocationToken loadTokenAndValidateDomainCreate( DomainCommand.Create command, String token, Registry registry, String clientId, DateTime now) throws EppException { AllocationToken tokenEntity = loadToken(token); - return tokenCustomLogic.verifyToken(command, tokenEntity, registry, clientId, now); + validateToken(tokenEntity, clientId, registry.getTldStr(), now); + return tokenCustomLogic.validateToken(command, tokenEntity, registry, clientId, now); } /** @@ -67,24 +72,38 @@ public class AllocationTokenFlowUtils { */ public AllocationTokenDomainCheckResults checkDomainsWithToken( List domainNames, String token, String clientId, DateTime now) { + // If the token is completely invalid, return the error message for all domain names + AllocationToken tokenEntity; try { - AllocationToken tokenEntity = loadToken(token); - // Only call custom logic if there wasn't a global allocation token error that applies to all - // check results. The custom logic can only add errors, not override existing errors. - return AllocationTokenDomainCheckResults.create( - Optional.of(tokenEntity), - tokenCustomLogic.checkDomainsWithToken( - ImmutableList.copyOf(domainNames), tokenEntity, clientId, now)); + tokenEntity = loadToken(token); } catch (EppException e) { return AllocationTokenDomainCheckResults.create( Optional.empty(), ImmutableMap.copyOf(Maps.toMap(domainNames, ignored -> e.getMessage()))); } + + // If the token is only invalid for some domain names (e.g. an invalid TLD), include those error + // results for only those domain names + ImmutableList.Builder validDomainNames = new ImmutableList.Builder<>(); + ImmutableMap.Builder resultsBuilder = new ImmutableMap.Builder<>(); + for (InternetDomainName domainName : domainNames) { + try { + validateToken(tokenEntity, clientId, domainName.parent().toString(), now); + validDomainNames.add(domainName); + } catch (EppException e) { + resultsBuilder.put(domainName, e.getMessage()); + } + } + + // For all valid domain names, run the custom logic and include the results + resultsBuilder.putAll( + tokenCustomLogic.checkDomainsWithToken( + validDomainNames.build(), tokenEntity, clientId, now)); + return AllocationTokenDomainCheckResults.create( + Optional.of(tokenEntity), resultsBuilder.build()); } - /** - * Redeems a SINGLE_USE {@link AllocationToken}, returning the redeemed copy. - */ + /** Redeems a SINGLE_USE {@link AllocationToken}, returning the redeemed copy. */ public AllocationToken redeemToken( AllocationToken token, Key redemptionHistoryEntry) { checkArgument( @@ -93,6 +112,30 @@ public class AllocationTokenFlowUtils { return token.asBuilder().setRedemptionHistoryEntry(redemptionHistoryEntry).build(); } + /** + * Validates a given token. The token could be invalid if it has allowed client IDs or TLDs that + * do not include this client ID / TLD, or if the token has a promotion that is not currently + * running. + * + * @throws EppException if the token is invalid in any way + */ + private void validateToken(AllocationToken token, String clientId, String tld, DateTime now) + throws EppException { + if (!token.getAllowedClientIds().isEmpty() && !token.getAllowedClientIds().contains(clientId)) { + throw new AllocationTokenNotValidForRegistrarException(); + } + if (!token.getAllowedTlds().isEmpty() && !token.getAllowedTlds().contains(tld)) { + throw new AllocationTokenNotValidForTldException(); + } + // Tokens without status transitions will just have a single-entry NOT_STARTED map, so only + // check the status transitions map if it's non-trivial. + if (token.getTokenStatusTransitions().size() > 1 + && !TokenStatus.VALID.equals(token.getTokenStatusTransitions().getValueAtTime(now))) { + throw new AllocationTokenNotInPromotionException(); + } + } + + /** Loads a given token and validates that it is not redeemed */ private AllocationToken loadToken(String token) throws EppException { AllocationToken tokenEntity = ofy().load().key(Key.create(AllocationToken.class, token)).now(); if (tokenEntity == null) { @@ -104,6 +147,31 @@ public class AllocationTokenFlowUtils { return tokenEntity; } + // Note: exception messages should be <= 32 characters long for domain check results + + /** The allocation token is not currently valid. */ + public static class AllocationTokenNotInPromotionException + extends StatusProhibitsOperationException { + public AllocationTokenNotInPromotionException() { + super("Alloc token not in promo period"); + } + } + /** The allocation token is not valid for this TLD. */ + public static class AllocationTokenNotValidForTldException + extends AssociationProhibitsOperationException { + public AllocationTokenNotValidForTldException() { + super("Alloc token invalid for TLD"); + } + } + + /** The allocation token is not valid for this registrar. */ + public static class AllocationTokenNotValidForRegistrarException + extends AssociationProhibitsOperationException { + public AllocationTokenNotValidForRegistrarException() { + super("Alloc token invalid for client"); + } + } + /** The allocation token was already redeemed. */ public static class AlreadyRedeemedAllocationTokenException extends AssociationProhibitsOperationException { diff --git a/javatests/google/registry/flows/domain/DomainCheckFlowTest.java b/javatests/google/registry/flows/domain/DomainCheckFlowTest.java index 28e46ffcc..d9701947e 100644 --- a/javatests/google/registry/flows/domain/DomainCheckFlowTest.java +++ b/javatests/google/registry/flows/domain/DomainCheckFlowTest.java @@ -186,6 +186,7 @@ public class DomainCheckFlowTest @Test public void testSuccess_allocationTokenPromotion() throws Exception { + createTld("example"); persistResource( new AllocationToken.Builder() .setToken("abc123") @@ -194,14 +195,82 @@ public class DomainCheckFlowTest .setTokenStatusTransitions( ImmutableSortedMap.naturalOrder() .put(START_OF_TIME, TokenStatus.NOT_STARTED) - .put(clock.nowUtc().plusMillis(1), TokenStatus.VALID) - .put(clock.nowUtc().plusSeconds(1), TokenStatus.ENDED) + .put(clock.nowUtc().minusDays(1), TokenStatus.VALID) + .put(clock.nowUtc().plusDays(1), TokenStatus.ENDED) .build()) .build()); setEppInput("domain_check_allocationtoken_fee.xml"); runFlowAssertResponse(loadFile("domain_check_allocationtoken_fee_response.xml")); } + @Test + public void testSuccess_promotionNotActive() throws Exception { + createTld("example"); + persistResource( + new AllocationToken.Builder() + .setToken("abc123") + .setTokenType(UNLIMITED_USE) + .setDiscountFraction(0.5) + .setTokenStatusTransitions( + ImmutableSortedMap.naturalOrder() + .put(START_OF_TIME, TokenStatus.NOT_STARTED) + .put(clock.nowUtc().plusDays(1), TokenStatus.VALID) + .put(clock.nowUtc().plusDays(60), TokenStatus.ENDED) + .build()) + .build()); + setEppInput("domain_check_allocationtoken_fee.xml"); + doCheckTest( + create(false, "example1.tld", "Alloc token not in promo period"), + create(false, "example2.example", "Alloc token not in promo period"), + create(false, "reserved.tld", "Reserved")); + } + + @Test + public void testSuccess_promoTokenNotValidForTld() throws Exception { + createTld("example"); + persistResource( + new AllocationToken.Builder() + .setToken("abc123") + .setTokenType(UNLIMITED_USE) + .setDiscountFraction(0.5) + .setAllowedTlds(ImmutableSet.of("example")) + .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()); + setEppInput("domain_check_allocationtoken_fee.xml"); + doCheckTest( + create(false, "example1.tld", "Alloc token invalid for TLD"), + create(true, "example2.example", null), + create(false, "reserved.tld", "Reserved")); + } + + @Test + public void testSuccess_promoTokenNotValidForRegistrar() throws Exception { + createTld("example"); + persistResource( + new AllocationToken.Builder() + .setToken("abc123") + .setTokenType(UNLIMITED_USE) + .setDiscountFraction(0.5) + .setAllowedClientIds(ImmutableSet.of("someOtherClient")) + .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()); + setEppInput("domain_check_allocationtoken_fee.xml"); + doCheckTest( + create(false, "example1.tld", "Alloc token invalid for client"), + create(false, "example2.example", "Alloc token invalid for client"), + create(false, "reserved.tld", "Reserved")); + } + @Test public void testSuccess_oneReservedInSunrise() throws Exception { createTld("tld", START_DATE_SUNRISE); diff --git a/javatests/google/registry/flows/domain/DomainCreateFlowTest.java b/javatests/google/registry/flows/domain/DomainCreateFlowTest.java index 1c529d909..bdc07a69e 100644 --- a/javatests/google/registry/flows/domain/DomainCreateFlowTest.java +++ b/javatests/google/registry/flows/domain/DomainCreateFlowTest.java @@ -128,6 +128,9 @@ import google.registry.flows.domain.DomainFlowUtils.TrailingDashException; import google.registry.flows.domain.DomainFlowUtils.UnexpectedClaimsNoticeException; import google.registry.flows.domain.DomainFlowUtils.UnsupportedFeeAttributeException; import google.registry.flows.domain.DomainFlowUtils.UnsupportedMarkTypeException; +import google.registry.flows.domain.token.AllocationTokenFlowUtils.AllocationTokenNotInPromotionException; +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.OnlyToolCanPassMetadataException; @@ -472,7 +475,16 @@ public class DomainCreateFlowTest extends ResourceFlowTestCasenaturalOrder() + .put(START_OF_TIME, TokenStatus.NOT_STARTED) + .put(clock.nowUtc().minusDays(1), TokenStatus.VALID) + .put(clock.nowUtc().plusDays(1), TokenStatus.ENDED) + .build()) + .build()); clock.advanceOneMilli(); runFlow(); assertSuccessfulCreate("tld", ImmutableSet.of(), allocationToken); @@ -1156,6 +1168,71 @@ public class DomainCreateFlowTest 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()); + setEppInput("domain_create_allocationtoken.xml", ImmutableMap.of("DOMAIN", "example.tld")); + assertAboutEppExceptions() + .that(assertThrows(AllocationTokenNotInPromotionException.class, this::runFlow)) + .marshalsToXml(); + } + + @Test + public void testSuccess_promoTokenNotValidForTld() { + persistContactsAndHosts(); + persistResource( + new AllocationToken.Builder() + .setToken("abc123") + .setTokenType(TokenType.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()); + setEppInput("domain_create_allocationtoken.xml", ImmutableMap.of("DOMAIN", "example.tld")); + assertAboutEppExceptions() + .that(assertThrows(AllocationTokenNotValidForTldException.class, this::runFlow)) + .marshalsToXml(); + } + + @Test + public void testSuccess_promoTokenNotValidForRegistrar() { + persistContactsAndHosts(); + persistResource( + new AllocationToken.Builder() + .setToken("abc123") + .setTokenType(TokenType.UNLIMITED_USE) + .setAllowedClientIds(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()); + setEppInput("domain_create_allocationtoken.xml", ImmutableMap.of("DOMAIN", "example.tld")); + assertAboutEppExceptions() + .that(assertThrows(AllocationTokenNotValidForRegistrarException.class, this::runFlow)) + .marshalsToXml(); + } + @Test public void testSuccess_superuserReserved() throws Exception { setEppInput("domain_create_reserved.xml"); diff --git a/javatests/google/registry/flows/domain/testdata/domain_check_allocationtoken_fee.xml b/javatests/google/registry/flows/domain/testdata/domain_check_allocationtoken_fee.xml index f1c6b5e49..1bf30eae0 100644 --- a/javatests/google/registry/flows/domain/testdata/domain_check_allocationtoken_fee.xml +++ b/javatests/google/registry/flows/domain/testdata/domain_check_allocationtoken_fee.xml @@ -4,7 +4,7 @@ example1.tld - example2.tld + example2.example reserved.tld @@ -23,7 +23,7 @@ 1 - example2.tld + example2.example USD create 1 diff --git a/javatests/google/registry/flows/domain/testdata/domain_check_allocationtoken_fee_response.xml b/javatests/google/registry/flows/domain/testdata/domain_check_allocationtoken_fee_response.xml index c1ac63e0f..5dcd84538 100644 --- a/javatests/google/registry/flows/domain/testdata/domain_check_allocationtoken_fee_response.xml +++ b/javatests/google/registry/flows/domain/testdata/domain_check_allocationtoken_fee_response.xml @@ -10,7 +10,7 @@ example1.tld - example2.tld + example2.example reserved.tld @@ -21,7 +21,7 @@ - example2.tld + example2.example USD create 1 diff --git a/javatests/google/registry/flows/domain/token/AllocationTokenFlowUtilsTest.java b/javatests/google/registry/flows/domain/token/AllocationTokenFlowUtilsTest.java index 3ea6eb107..64e0a336a 100644 --- a/javatests/google/registry/flows/domain/token/AllocationTokenFlowUtilsTest.java +++ b/javatests/google/registry/flows/domain/token/AllocationTokenFlowUtilsTest.java @@ -15,23 +15,35 @@ package google.registry.flows.domain.token; import static com.google.common.truth.Truth.assertThat; +import static google.registry.model.domain.token.AllocationToken.TokenStatus.CANCELLED; +import static google.registry.model.domain.token.AllocationToken.TokenStatus.ENDED; +import static google.registry.model.domain.token.AllocationToken.TokenStatus.NOT_STARTED; +import static google.registry.model.domain.token.AllocationToken.TokenStatus.VALID; 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.DatastoreHelper.createTld; import static google.registry.testing.DatastoreHelper.persistResource; import static google.registry.testing.EppExceptionSubject.assertAboutEppExceptions; import static google.registry.testing.JUnitBackports.assertThrows; +import static google.registry.util.DateTimeUtils.START_OF_TIME; import static org.joda.time.DateTimeZone.UTC; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSet; +import com.google.common.collect.ImmutableSortedMap; import com.google.common.net.InternetDomainName; import com.googlecode.objectify.Key; import google.registry.flows.EppException; +import google.registry.flows.domain.token.AllocationTokenFlowUtils.AllocationTokenNotInPromotionException; +import google.registry.flows.domain.token.AllocationTokenFlowUtils.AllocationTokenNotValidForRegistrarException; +import google.registry.flows.domain.token.AllocationTokenFlowUtils.AllocationTokenNotValidForTldException; import google.registry.flows.domain.token.AllocationTokenFlowUtils.InvalidAllocationTokenException; 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.registry.Registry; import google.registry.model.reporting.HistoryEntry; import google.registry.testing.AppEngineRule; @@ -48,6 +60,9 @@ import org.junit.runners.JUnit4; @RunWith(JUnit4.class) public class AllocationTokenFlowUtilsTest extends ShardableTestCase { + private final AllocationTokenFlowUtils flowUtils = + new AllocationTokenFlowUtils(new AllocationTokenCustomLogic()); + @Rule public final AppEngineRule appEngine = AppEngineRule.builder().withDatastore().build(); @Before @@ -56,14 +71,12 @@ public class AllocationTokenFlowUtilsTest extends ShardableTestCase { } @Test - public void test_verifyToken_successfullyVerifiesValidToken() throws Exception { + public void test_validateToken_successfullyVerifiesValidToken() throws Exception { AllocationToken token = persistResource( new AllocationToken.Builder().setToken("tokeN").setTokenType(SINGLE_USE).build()); - AllocationTokenFlowUtils flowUtils = - new AllocationTokenFlowUtils(new AllocationTokenCustomLogic()); assertThat( - flowUtils.loadAndVerifyToken( + flowUtils.loadTokenAndValidateDomainCreate( createCommand("blah.tld"), "tokeN", Registry.get("tld"), @@ -73,33 +86,21 @@ public class AllocationTokenFlowUtilsTest extends ShardableTestCase { } @Test - public void test_verifyToken_failsOnNonexistentToken() { - AllocationTokenFlowUtils flowUtils = - new AllocationTokenFlowUtils(new AllocationTokenCustomLogic()); - EppException thrown = - assertThrows( - InvalidAllocationTokenException.class, - () -> - flowUtils.loadAndVerifyToken( - createCommand("blah.tld"), - "tokeN", - Registry.get("tld"), - "TheRegistrar", - DateTime.now(UTC))); - assertAboutEppExceptions().that(thrown).marshalsToXml(); + public void test_validateToken_failsOnNonexistentToken() { + assertValidateThrowsEppException(InvalidAllocationTokenException.class); } @Test - public void test_verifyToken_callsCustomLogic() { + public void test_validateToken_callsCustomLogic() { + AllocationTokenFlowUtils failingFlowUtils = + new AllocationTokenFlowUtils(new FailingAllocationTokenCustomLogic()); persistResource( new AllocationToken.Builder().setToken("tokeN").setTokenType(SINGLE_USE).build()); - AllocationTokenFlowUtils flowUtils = - new AllocationTokenFlowUtils(new FailingAllocationTokenCustomLogic()); Exception thrown = assertThrows( IllegalStateException.class, () -> - flowUtils.loadAndVerifyToken( + failingFlowUtils.loadTokenAndValidateDomainCreate( createCommand("blah.tld"), "tokeN", Registry.get("tld"), @@ -108,12 +109,55 @@ public class AllocationTokenFlowUtilsTest extends ShardableTestCase { assertThat(thrown).hasMessageThat().isEqualTo("failed for tests"); } + @Test + public void test_validateToken_invalidForClientId() { + persistResource( + createOneMonthPromoTokenBuilder(DateTime.now(UTC).minusDays(1)) + .setAllowedClientIds(ImmutableSet.of("NewRegistrar")) + .build()); + assertValidateThrowsEppException(AllocationTokenNotValidForRegistrarException.class); + } + + @Test + public void test_validateToken_invalidForTld() { + persistResource( + createOneMonthPromoTokenBuilder(DateTime.now(UTC).minusDays(1)) + .setAllowedTlds(ImmutableSet.of("nottld")) + .build()); + assertValidateThrowsEppException(AllocationTokenNotValidForTldException.class); + } + + @Test + public void test_validateToken_beforePromoStart() { + persistResource(createOneMonthPromoTokenBuilder(DateTime.now(UTC).plusDays(1)).build()); + assertValidateThrowsEppException(AllocationTokenNotInPromotionException.class); + } + + @Test + public void test_validateToken_afterPromoEnd() { + persistResource(createOneMonthPromoTokenBuilder(DateTime.now(UTC).minusMonths(2)).build()); + assertValidateThrowsEppException(AllocationTokenNotInPromotionException.class); + } + + @Test + public void test_validateToken_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()); + assertValidateThrowsEppException(AllocationTokenNotInPromotionException.class); + } + @Test public void test_checkDomainsWithToken_successfullyVerifiesValidToken() { persistResource( new AllocationToken.Builder().setToken("tokeN").setTokenType(SINGLE_USE).build()); - AllocationTokenFlowUtils flowUtils = - new AllocationTokenFlowUtils(new AllocationTokenCustomLogic()); assertThat( flowUtils .checkDomainsWithToken( @@ -137,8 +181,6 @@ public class AllocationTokenFlowUtilsTest extends ShardableTestCase { .setTokenType(SINGLE_USE) .setRedemptionHistoryEntry(Key.create(HistoryEntry.class, 101L)) .build()); - AllocationTokenFlowUtils flowUtils = - new AllocationTokenFlowUtils(new AllocationTokenCustomLogic()); assertThat( flowUtils .checkDomainsWithToken( @@ -161,13 +203,13 @@ public class AllocationTokenFlowUtilsTest extends ShardableTestCase { public void test_checkDomainsWithToken_callsCustomLogic() { persistResource( new AllocationToken.Builder().setToken("tokeN").setTokenType(SINGLE_USE).build()); - AllocationTokenFlowUtils flowUtils = + AllocationTokenFlowUtils failingFlowUtils = new AllocationTokenFlowUtils(new FailingAllocationTokenCustomLogic()); Exception thrown = assertThrows( IllegalStateException.class, () -> - flowUtils.checkDomainsWithToken( + failingFlowUtils.checkDomainsWithToken( ImmutableList.of( InternetDomainName.from("blah.tld"), InternetDomainName.from("blah2.tld")), "tokeN", @@ -180,10 +222,10 @@ public class AllocationTokenFlowUtilsTest extends ShardableTestCase { public void test_checkDomainsWithToken_resultsFromCustomLogicAreIntegrated() { persistResource( new AllocationToken.Builder().setToken("tokeN").setTokenType(SINGLE_USE).build()); - AllocationTokenFlowUtils flowUtils = + AllocationTokenFlowUtils customResultFlowUtils = new AllocationTokenFlowUtils(new CustomResultAllocationTokenCustomLogic()); assertThat( - flowUtils + customResultFlowUtils .checkDomainsWithToken( ImmutableList.of( InternetDomainName.from("blah.tld"), InternetDomainName.from("bunny.tld")), @@ -200,17 +242,44 @@ public class AllocationTokenFlowUtilsTest extends ShardableTestCase { .inOrder(); } + private void assertValidateThrowsEppException(Class clazz) { + assertAboutEppExceptions() + .that( + assertThrows( + clazz, + () -> + flowUtils.loadTokenAndValidateDomainCreate( + createCommand("blah.tld"), + "tokeN", + Registry.get("tld"), + "TheRegistrar", + DateTime.now(UTC)))) + .marshalsToXml(); + } + private static DomainCommand.Create createCommand(String domainName) { DomainCommand.Create command = mock(DomainCommand.Create.class); when(command.getFullyQualifiedDomainName()).thenReturn(domainName); return command; } + private static AllocationToken.Builder createOneMonthPromoTokenBuilder(DateTime promoStart) { + return new AllocationToken.Builder() + .setToken("tokeN") + .setTokenType(UNLIMITED_USE) + .setTokenStatusTransitions( + ImmutableSortedMap.naturalOrder() + .put(START_OF_TIME, NOT_STARTED) + .put(promoStart, VALID) + .put(promoStart.plusMonths(1), ENDED) + .build()); + } + /** An {@link AllocationTokenCustomLogic} class that throws exceptions on every method. */ private static class FailingAllocationTokenCustomLogic extends AllocationTokenCustomLogic { @Override - public AllocationToken verifyToken( + public AllocationToken validateToken( DomainCommand.Create command, AllocationToken token, Registry registry,