Change AllocationToken behavior in non-catastrophic situations (#2730)

We're changing the way that allocation tokens work in suboptimal (i.e. incorrect) situations in the domain check, creation, and renewal process. Currently, if a token is not applicable, in any way, to any of the operations (including when a check has multiple operations requested) we return some variation of "Allocation token not valid" for all of those options. We wish to allow for a more lenient process, where if a token is "not applicable" instead of "invalid", we just pass through that part of the request as if the token were not there.

Types of errors that will remain catastrophic, where we'll basically return a token error immediately in all cases:
- nonexistent or null token
- token is assigned to a particular domain and the request isn't for that domain
- token is not valid for this registrar
- token is a single-use token that has already been redeemed
- token has a promotional schedule and it's no longer valid

Types of errors that will now be a silent pass-through, as if the user did not issue a token:
- token is not allowed for this TLD
- token has a discount, is not valid for premium names, and the domain name is premium
- token does not allow the provided EPP action

Currently, the last three types of errors cause that generic "token invalid" message but in the future, we'll pass the requests through as if the user did not pass in a token. This does allow for a default token to apply to these requests if available, meaning that it's possible that a single DomainCheckFlow with multiple check requests could use the provided token for some check(s), and a default token for others.

The flip side of this is that if the user passes in a catastrophically invalid token (the first five error messages above), we will return that result to any/all checks that they request, even if there are other issues with that request (e.g. the domain is reserved or already registered).

See b/315504612 for more details and background
This commit is contained in:
gbrodman
2025-04-23 15:09:37 +00:00
committed by GitHub
parent 0472dda860
commit 56cd2ad282
22 changed files with 815 additions and 1187 deletions
@@ -211,8 +211,8 @@ class DomainCheckFlowTest extends ResourceCheckFlowTestCase<DomainCheckFlow, Dom
doCheckTest(
create(true, "example1.tld", null),
create(false, "example2.tld", "Alloc token invalid for domain"),
create(false, "reserved.tld", "Reserved"),
create(false, "specificuse.tld", "Reserved; alloc. token required"));
create(false, "reserved.tld", "Alloc token invalid for domain"),
create(false, "specificuse.tld", "Alloc token invalid for domain"));
}
@Test
@@ -230,8 +230,8 @@ class DomainCheckFlowTest extends ResourceCheckFlowTestCase<DomainCheckFlow, Dom
doCheckTest(
create(false, "example1.tld", "Blocked by a GlobalBlock service"),
create(false, "example2.tld", "Alloc token invalid for domain"),
create(false, "reserved.tld", "Reserved"),
create(false, "specificuse.tld", "Reserved; alloc. token required"));
create(false, "reserved.tld", "Alloc token invalid for domain"),
create(false, "specificuse.tld", "Alloc token invalid for domain"));
}
@Test
@@ -257,17 +257,6 @@ class DomainCheckFlowTest extends ResourceCheckFlowTestCase<DomainCheckFlow, Dom
create(true, "example3.tld", null));
}
@Test
void testSuccess_oneExists_allocationTokenIsInvalid() throws Exception {
setEppInput("domain_check_allocationtoken.xml");
persistActiveDomain("example1.tld");
doCheckTest(
create(false, "example1.tld", "In use"),
create(false, "example2.tld", "The allocation token is invalid"),
create(false, "reserved.tld", "Reserved"),
create(false, "specificuse.tld", "Reserved; alloc. token required"));
}
@Test
void testSuccess_oneExists_allocationTokenIsValid() throws Exception {
setEppInput("domain_check_allocationtoken.xml");
@@ -300,24 +289,6 @@ class DomainCheckFlowTest extends ResourceCheckFlowTestCase<DomainCheckFlow, Dom
runFlowAssertResponse(loadFile("domain_check_allocationtoken_fee_anchor_response.xml"));
}
@Test
void testSuccess_oneExists_allocationTokenIsRedeemed() throws Exception {
setEppInput("domain_check_allocationtoken.xml");
Domain domain = persistActiveDomain("example1.tld");
HistoryEntryId historyEntryId = new HistoryEntryId(domain.getRepoId(), 1L);
persistResource(
new AllocationToken.Builder()
.setToken("abc123")
.setTokenType(SINGLE_USE)
.setRedemptionHistoryId(historyEntryId)
.build());
doCheckTest(
create(false, "example1.tld", "In use"),
create(false, "example2.tld", "Alloc token was already redeemed"),
create(false, "reserved.tld", "Reserved"),
create(false, "specificuse.tld", "Reserved; alloc. token required"));
}
@Test
void testSuccess_oneExists_allocationTokenForReservedDomain() throws Exception {
setEppInput("domain_check_allocationtoken.xml");
@@ -329,9 +300,9 @@ class DomainCheckFlowTest extends ResourceCheckFlowTestCase<DomainCheckFlow, Dom
.setTokenType(SINGLE_USE)
.build());
doCheckTest(
create(false, "example1.tld", "In use"),
create(false, "example1.tld", "Alloc token invalid for domain"),
create(false, "example2.tld", "Alloc token invalid for domain"),
create(false, "reserved.tld", "Reserved"),
create(false, "reserved.tld", "Alloc token invalid for domain"),
create(true, "specificuse.tld", null));
}
@@ -350,23 +321,6 @@ class DomainCheckFlowTest extends ResourceCheckFlowTestCase<DomainCheckFlow, Dom
runFlowAssertResponse(loadFile("domain_check_allocationtoken_fee_specificuse_response.xml"));
}
@Test
void testSuccess_oneExists_allocationTokenForWrongDomain() throws Exception {
setEppInput("domain_check_allocationtoken.xml");
persistActiveDomain("example1.tld");
persistResource(
new AllocationToken.Builder()
.setDomainName("someotherdomain.tld")
.setToken("abc123")
.setTokenType(SINGLE_USE)
.build());
doCheckTest(
create(false, "example1.tld", "In use"),
create(false, "example2.tld", "Alloc token invalid for domain"),
create(false, "reserved.tld", "Reserved"),
create(false, "specificuse.tld", "Reserved; alloc. token required"));
}
@Test
void testSuccess_notOutOfDateToken_forSpecificDomain() throws Exception {
setEppInput("domain_check_allocationtoken.xml");
@@ -385,44 +339,10 @@ class DomainCheckFlowTest extends ResourceCheckFlowTestCase<DomainCheckFlow, Dom
doCheckTest(
create(false, "example1.tld", "Alloc token invalid for domain"),
create(false, "example2.tld", "Alloc token invalid for domain"),
create(false, "reserved.tld", "Reserved"),
create(false, "reserved.tld", "Alloc token invalid for domain"),
create(true, "specificuse.tld", null));
}
@Test
void testSuccess_outOfDateToken_forSpecificDomain() throws Exception {
setEppInput("domain_check_allocationtoken.xml");
persistResource(
new AllocationToken.Builder()
.setToken("abc123")
.setTokenType(SINGLE_USE)
.setDomainName("specificuse.tld")
.setTokenStatusTransitions(
ImmutableSortedMap.<DateTime, TokenStatus>naturalOrder()
.put(START_OF_TIME, TokenStatus.NOT_STARTED)
.put(clock.nowUtc().minusDays(2), TokenStatus.VALID)
.put(clock.nowUtc().minusDays(1), TokenStatus.ENDED)
.build())
.build());
doCheckTest(
create(false, "example1.tld", "Alloc token invalid for domain"),
create(false, "example2.tld", "Alloc token invalid for domain"),
create(false, "reserved.tld", "Reserved"),
create(false, "specificuse.tld", "Alloc token not in promo period"));
}
@Test
void testSuccess_nothingExists_reservationsOverrideInvalidAllocationTokens() throws Exception {
setEppInput("domain_check_reserved_allocationtoken.xml");
// Fill out these reasons
doCheckTest(
create(false, "collision.tld", "Cannot be delegated"),
create(false, "reserved.tld", "Reserved"),
create(false, "anchor.tld", "Reserved; alloc. token required"),
create(false, "allowedinsunrise.tld", "Reserved"),
create(false, "premiumcollision.tld", "Cannot be delegated"));
}
@Test
void testSuccess_allocationTokenPromotion_singleYear() throws Exception {
createTld("example");
@@ -500,7 +420,75 @@ class DomainCheckFlowTest extends ResourceCheckFlowTestCase<DomainCheckFlow, Dom
}
@Test
void testFailure_allocationTokenPromotion_PremiumsNotSet() throws Exception {
void testSuccess_allocationTokenInvalid_overridesOtherErrors() throws Exception {
setEppInput("domain_check_allocationtoken.xml");
persistActiveDomain("example1.tld");
doCheckTest(
create(false, "example1.tld", "The allocation token is invalid"),
create(false, "example2.tld", "The allocation token is invalid"),
create(false, "reserved.tld", "The allocation token is invalid"),
create(false, "specificuse.tld", "The allocation token is invalid"));
}
@Test
void testSuccess_allocationTokenForWrongDomain_overridesOtherConcerns() throws Exception {
setEppInput("domain_check_allocationtoken.xml");
persistActiveDomain("example1.tld");
persistResource(
new AllocationToken.Builder()
.setDomainName("someotherdomain.tld")
.setToken("abc123")
.setTokenType(SINGLE_USE)
.build());
doCheckTest(
create(false, "example1.tld", "Alloc token invalid for domain"),
create(false, "example2.tld", "Alloc token invalid for domain"),
create(false, "reserved.tld", "Alloc token invalid for domain"),
create(false, "specificuse.tld", "Alloc token invalid for domain"));
}
@Test
void testSuccess_outOfDateToken_overridesOtherIssues() throws Exception {
setEppInput("domain_check_allocationtoken.xml");
persistResource(
new AllocationToken.Builder()
.setToken("abc123")
.setTokenType(SINGLE_USE)
.setDomainName("specificuse.tld")
.setTokenStatusTransitions(
ImmutableSortedMap.<DateTime, TokenStatus>naturalOrder()
.put(START_OF_TIME, TokenStatus.NOT_STARTED)
.put(clock.nowUtc().minusDays(2), TokenStatus.VALID)
.put(clock.nowUtc().minusDays(1), TokenStatus.ENDED)
.build())
.build());
doCheckTest(
create(false, "example1.tld", "Alloc token not in promo period"),
create(false, "example2.tld", "Alloc token not in promo period"),
create(false, "reserved.tld", "Alloc token not in promo period"),
create(false, "specificuse.tld", "Alloc token not in promo period"));
}
@Test
void testSuccess_redeemedTokenOverridesOtherConcerns() throws Exception {
setEppInput("domain_check_allocationtoken.xml");
Domain domain = persistActiveDomain("example1.tld");
HistoryEntryId historyEntryId = new HistoryEntryId(domain.getRepoId(), 1L);
persistResource(
new AllocationToken.Builder()
.setToken("abc123")
.setTokenType(SINGLE_USE)
.setRedemptionHistoryId(historyEntryId)
.build());
doCheckTest(
create(false, "example1.tld", "Alloc token was already redeemed"),
create(false, "example2.tld", "Alloc token was already redeemed"),
create(false, "reserved.tld", "Alloc token was already redeemed"),
create(false, "specificuse.tld", "Alloc token was already redeemed"));
}
@Test
void testSuccess_allocationTokenPromotion_noPremium_stillPasses() throws Exception {
createTld("example");
persistResource(
new AllocationToken.Builder()
@@ -515,7 +503,7 @@ class DomainCheckFlowTest extends ResourceCheckFlowTestCase<DomainCheckFlow, Dom
ImmutableMap.of("DOMAIN", "rich.example"));
doCheckTest(
create(true, "example1.example", null),
create(false, "rich.example", "Token not valid for premium name"),
create(true, "rich.example", null),
create(true, "example3.example", null));
}
@@ -572,7 +560,8 @@ class DomainCheckFlowTest extends ResourceCheckFlowTestCase<DomainCheckFlow, Dom
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"));
create(false, "reserved.tld", "Alloc token not in promo period"),
create(false, "rich.example", "Alloc token not in promo period"));
}
@Test
@@ -593,9 +582,10 @@ class DomainCheckFlowTest extends ResourceCheckFlowTestCase<DomainCheckFlow, Dom
.build());
setEppInput("domain_check_allocationtoken_fee.xml");
doCheckTest(
create(false, "example1.tld", "Alloc token invalid for TLD"),
create(true, "example1.tld", null),
create(true, "example2.example", null),
create(false, "reserved.tld", "Reserved"));
create(false, "reserved.tld", "Reserved"),
create(true, "rich.example", null));
}
@Test
@@ -618,7 +608,8 @@ class DomainCheckFlowTest extends ResourceCheckFlowTestCase<DomainCheckFlow, Dom
doCheckTest(
create(false, "example1.tld", "Alloc token invalid for client"),
create(false, "example2.example", "Alloc token invalid for client"),
create(false, "reserved.tld", "Reserved"));
create(false, "reserved.tld", "Alloc token invalid for client"),
create(false, "rich.example", "Alloc token invalid for client"));
}
@Test
@@ -999,6 +990,7 @@ class DomainCheckFlowTest extends ResourceCheckFlowTestCase<DomainCheckFlow, Dom
.setToken("abc123")
.setTokenType(UNLIMITED_USE)
.setAllowedEppActions(ImmutableSet.of(CommandName.CREATE, CommandName.TRANSFER))
.setDiscountFraction(0.1)
.build());
setEppInput("domain_check_fee_multiple_commands_allocationtoken_v06.xml");
runFlowAssertResponse(
@@ -1028,6 +1020,7 @@ class DomainCheckFlowTest extends ResourceCheckFlowTestCase<DomainCheckFlow, Dom
.setToken("abc123")
.setTokenType(UNLIMITED_USE)
.setAllowedEppActions(ImmutableSet.of(CommandName.CREATE, CommandName.TRANSFER))
.setDiscountFraction(0.1)
.build());
setEppInput("domain_check_fee_multiple_commands_allocationtoken_v12.xml");
runFlowAssertResponse(
@@ -141,13 +141,10 @@ 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.DomainPricingLogic.AllocationTokenInvalidForPremiumNameException;
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.domain.token.AllocationTokenFlowUtils.NonexistentAllocationTokenException;
import google.registry.flows.exceptions.OnlyToolCanPassMetadataException;
import google.registry.flows.exceptions.ResourceAlreadyExistsForThisClientException;
import google.registry.flows.exceptions.ResourceCreateContentionException;
@@ -529,51 +526,10 @@ class DomainCreateFlowTest extends ResourceFlowTestCase<DomainCreateFlow, Domain
"domain_create_allocationtoken.xml",
ImmutableMap.of("DOMAIN", "example.tld", "YEARS", "2"));
persistContactsAndHosts();
EppException thrown = assertThrows(InvalidAllocationTokenException.class, this::runFlow);
EppException thrown = assertThrows(NonexistentAllocationTokenException.class, this::runFlow);
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
void testFailure_reservedDomainCreate_allocationTokenIsForADifferentDomain() {
// Try to register a reserved domain name with an allocation token valid for a different domain
// name.
setEppInput(
"domain_create_allocationtoken.xml", ImmutableMap.of("DOMAIN", "resdom.tld", "YEARS", "2"));
persistContactsAndHosts();
persistResource(
new AllocationToken.Builder()
.setToken("abc123")
.setTokenType(SINGLE_USE)
.setDomainName("otherdomain.tld")
.build());
clock.advanceOneMilli();
EppException thrown =
assertThrows(AllocationTokenNotValidForDomainException.class, this::runFlow);
assertAboutEppExceptions().that(thrown).marshalsToXml();
assertAllocationTokenWasNotRedeemed("abc123");
}
@Test
void testFailure_nonreservedDomainCreate_allocationTokenIsForADifferentDomain() {
// Try to register a non-reserved domain name with an allocation token valid for a different
// domain name.
setEppInput(
"domain_create_allocationtoken.xml",
ImmutableMap.of("DOMAIN", "example.tld", "YEARS", "2"));
persistContactsAndHosts();
persistResource(
new AllocationToken.Builder()
.setToken("abc123")
.setTokenType(SINGLE_USE)
.setDomainName("otherdomain.tld")
.build());
clock.advanceOneMilli();
EppException thrown =
assertThrows(AllocationTokenNotValidForDomainException.class, this::runFlow);
assertAboutEppExceptions().that(thrown).marshalsToXml();
assertAllocationTokenWasNotRedeemed("abc123");
}
@Test
void testFailure_alreadyRedemeedAllocationToken() {
setEppInput(
@@ -1704,32 +1660,6 @@ class DomainCreateFlowTest extends ResourceFlowTestCase<DomainCreateFlow, Domain
assertThat(billingEvent.getCost()).isEqualTo(Money.of(USD, 204.44));
}
@Test
void testSuccess_promotionDoesNotApplyToPremiumPrice() {
// Discounts only apply to premium domains if the token is explicitly configured to allow it.
createTld("example");
persistContactsAndHosts();
persistResource(
new AllocationToken.Builder()
.setToken("abc123")
.setTokenType(UNLIMITED_USE)
.setDiscountFraction(0.5)
.setTokenStatusTransitions(
ImmutableSortedMap.<DateTime, TokenStatus>naturalOrder()
.put(START_OF_TIME, TokenStatus.NOT_STARTED)
.put(clock.nowUtc().plusMillis(1), TokenStatus.VALID)
.put(clock.nowUtc().plusSeconds(1), TokenStatus.ENDED)
.build())
.build());
clock.advanceOneMilli();
setEppInput(
"domain_create_premium_allocationtoken.xml",
ImmutableMap.of("YEARS", "2", "FEE", "193.50"));
assertAboutEppExceptions()
.that(assertThrows(AllocationTokenInvalidForPremiumNameException.class, this::runFlow))
.marshalsToXml();
}
@Test
void testSuccess_token_premiumDomainZeroPrice_noFeeExtension() throws Exception {
createTld("example");
@@ -1774,30 +1704,6 @@ class DomainCreateFlowTest extends ResourceFlowTestCase<DomainCreateFlow, Domain
.marshalsToXml();
}
@Test
void testSuccess_promoTokenNotValidForTld() {
persistContactsAndHosts();
persistResource(
new AllocationToken.Builder()
.setToken("abc123")
.setTokenType(UNLIMITED_USE)
.setAllowedTlds(ImmutableSet.of("example"))
.setDiscountFraction(0.5)
.setTokenStatusTransitions(
ImmutableSortedMap.<DateTime, TokenStatus>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", "YEARS", "2"));
assertAboutEppExceptions()
.that(assertThrows(AllocationTokenNotValidForTldException.class, this::runFlow))
.marshalsToXml();
}
@Test
void testSuccess_promoTokenNotValidForRegistrar() {
persistContactsAndHosts();
@@ -3671,22 +3577,6 @@ class DomainCreateFlowTest extends ResourceFlowTestCase<DomainCreateFlow, Domain
assertThrows(MissingClaimsNoticeException.class, this::runFlow);
}
@Test
void testFailure_anchorTenant_mismatchedName_viaToken() throws Exception {
persistResource(
new AllocationToken.Builder()
.setToken("abc123")
.setTokenType(SINGLE_USE)
.setRegistrationBehavior(RegistrationBehavior.ANCHOR_TENANT)
.setDomainName("example.tld")
.build());
persistContactsAndHosts();
setEppInput(
"domain_create_allocationtoken.xml",
ImmutableMap.of("DOMAIN", "example-one.tld", "YEARS", "2"));
assertThrows(AllocationTokenNotValidForDomainException.class, this::runFlow);
}
@Test
void testSuccess_bulkToken_addsTokenToDomain() throws Exception {
AllocationToken token =
@@ -28,7 +28,6 @@ import static google.registry.testing.DatabaseHelper.persistPremiumList;
import static google.registry.testing.DatabaseHelper.persistResource;
import static google.registry.util.DateTimeUtils.END_OF_TIME;
import static google.registry.util.DateTimeUtils.START_OF_TIME;
import static org.joda.money.CurrencyUnit.JPY;
import static org.joda.money.CurrencyUnit.USD;
import static org.junit.jupiter.api.Assertions.assertThrows;
@@ -39,8 +38,6 @@ import google.registry.flows.EppException;
import google.registry.flows.HttpSessionMetadata;
import google.registry.flows.SessionMetadata;
import google.registry.flows.custom.DomainPricingCustomLogic;
import google.registry.flows.domain.DomainPricingLogic.AllocationTokenInvalidForCurrencyException;
import google.registry.flows.domain.DomainPricingLogic.AllocationTokenInvalidForPremiumNameException;
import google.registry.model.billing.BillingBase.Reason;
import google.registry.model.billing.BillingBase.RenewalPriceBehavior;
import google.registry.model.billing.BillingRecurrence;
@@ -214,32 +211,6 @@ public class DomainPricingLogicTest {
.build());
}
@Test
void
testGetDomainCreatePrice_withDiscountPriceToken_domainCurrencyDoesNotMatchTokensCurrency_throwsException() {
AllocationToken allocationToken =
persistResource(
new AllocationToken.Builder()
.setToken("abc123")
.setTokenType(SINGLE_USE)
.setDiscountPrice(Money.of(JPY, new BigDecimal("250")))
.setDiscountPremiums(false)
.build());
// Domain's currency is not JPY (is USD).
assertThrows(
AllocationTokenInvalidForCurrencyException.class,
() ->
domainPricingLogic.getCreatePrice(
tld,
"default.example",
clock.nowUtc(),
3,
false,
false,
Optional.of(allocationToken)));
}
@Test
void testGetDomainRenewPrice_oneYear_standardDomain_noBilling_isStandardPrice()
throws EppException {
@@ -335,77 +306,6 @@ public class DomainPricingLogicTest {
.build());
}
@Test
void
testGetDomainRenewPrice_oneYear_premiumDomain_default_withTokenNotValidForPremiums_throwsException() {
AllocationToken allocationToken =
persistResource(
new AllocationToken.Builder()
.setToken("abc123")
.setTokenType(SINGLE_USE)
.setDiscountFraction(0.5)
.setDiscountPremiums(false)
.build());
assertThrows(
AllocationTokenInvalidForPremiumNameException.class,
() ->
domainPricingLogic.getRenewPrice(
tld,
"premium.example",
clock.nowUtc(),
1,
persistDomainAndSetRecurrence("premium.example", DEFAULT, Optional.empty()),
Optional.of(allocationToken)));
}
@Test
void
testGetDomainRenewPrice_oneYear_premiumDomain_default_withDiscountPriceToken_throwsException() {
AllocationToken allocationToken =
persistResource(
new AllocationToken.Builder()
.setToken("abc123")
.setTokenType(SINGLE_USE)
.setDiscountPrice(Money.of(USD, 5))
.setDiscountPremiums(false)
.build());
assertThrows(
AllocationTokenInvalidForPremiumNameException.class,
() ->
domainPricingLogic.getRenewPrice(
tld,
"premium.example",
clock.nowUtc(),
1,
persistDomainAndSetRecurrence("premium.example", DEFAULT, Optional.empty()),
Optional.of(allocationToken)));
}
@Test
void
testGetDomainRenewPrice_withDiscountPriceToken_domainCurrencyDoesNotMatchTokensCurrency_throwsException() {
AllocationToken allocationToken =
persistResource(
new AllocationToken.Builder()
.setToken("abc123")
.setTokenType(SINGLE_USE)
.setDiscountPrice(Money.of(JPY, new BigDecimal("250")))
.setDiscountPremiums(false)
.build());
// Domain's currency is not JPY (is USD).
assertThrows(
AllocationTokenInvalidForCurrencyException.class,
() ->
domainPricingLogic.getRenewPrice(
tld,
"default.example",
clock.nowUtc(),
1,
persistDomainAndSetRecurrence("default.example", DEFAULT, Optional.empty()),
Optional.of(allocationToken)));
}
@Test
void testGetDomainRenewPrice_multiYear_premiumDomain_default_isPremiumCost() throws EppException {
assertThat(
@@ -450,30 +350,6 @@ public class DomainPricingLogicTest {
.build());
}
@Test
void
testGetDomainRenewPrice_multiYear_premiumDomain_default_withTokenNotValidForPremiums_throwsException() {
AllocationToken allocationToken =
persistResource(
new AllocationToken.Builder()
.setToken("abc123")
.setTokenType(SINGLE_USE)
.setDiscountFraction(0.5)
.setDiscountPremiums(false)
.setDiscountYears(2)
.build());
assertThrows(
AllocationTokenInvalidForPremiumNameException.class,
() ->
domainPricingLogic.getRenewPrice(
tld,
"premium.example",
clock.nowUtc(),
5,
persistDomainAndSetRecurrence("premium.example", DEFAULT, Optional.empty()),
Optional.of(allocationToken)));
}
@Test
void testGetDomainRenewPrice_oneYear_standardDomain_default_isNonPremiumPrice()
throws EppException {
@@ -69,13 +69,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;
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.domain.token.AllocationTokenFlowUtils.MissingRemoveBulkPricingTokenOnBulkPricingDomainException;
import google.registry.flows.domain.token.AllocationTokenFlowUtils.NonexistentAllocationTokenException;
import google.registry.flows.domain.token.AllocationTokenFlowUtils.RemoveBulkPricingTokenOnNonBulkPricingDomainException;
import google.registry.flows.exceptions.ResourceStatusProhibitsOperationException;
import google.registry.model.billing.BillingBase.Flag;
@@ -459,10 +458,14 @@ class DomainRenewFlowTest extends ResourceFlowTestCase<DomainRenewFlow, Domain>
ImmutableMap<String, String> customFeeMap =
updateSubstitutions(
FEE_06_MAP,
"NAME", "costly-renew.tld",
"PERIOD", "1",
"EX_DATE", "2001-04-03T22:00:00.0Z",
"FEE", "111.00");
"NAME",
"costly-renew.tld",
"PERIOD",
"1",
"EX_DATE",
"2001-04-03T22:00:00.0Z",
"FEE",
"111.00");
setEppInput("domain_renew_fee.xml", customFeeMap);
persistDomain();
doSuccessfulTest(
@@ -694,7 +697,7 @@ class DomainRenewFlowTest extends ResourceFlowTestCase<DomainRenewFlow, Domain>
"domain_renew_allocationtoken.xml",
ImmutableMap.of("DOMAIN", "example.tld", "YEARS", "2", "TOKEN", "abc123"));
persistDomain();
EppException thrown = assertThrows(InvalidAllocationTokenException.class, this::runFlow);
EppException thrown = assertThrows(NonexistentAllocationTokenException.class, this::runFlow);
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@@ -711,9 +714,12 @@ class DomainRenewFlowTest extends ResourceFlowTestCase<DomainRenewFlow, Domain>
.setDomainName("otherdomain.tld")
.build());
clock.advanceOneMilli();
EppException thrown =
assertThrows(AllocationTokenNotValidForDomainException.class, this::runFlow);
assertAboutEppExceptions().that(thrown).marshalsToXml();
assertAboutEppExceptions()
.that(
assertThrows(
AllocationTokenFlowUtils.AllocationTokenNotValidForDomainException.class,
this::runFlow))
.marshalsToXml();
assertAllocationTokenWasNotRedeemed("abc123");
}
@@ -784,9 +790,10 @@ class DomainRenewFlowTest extends ResourceFlowTestCase<DomainRenewFlow, Domain>
.put(clock.nowUtc().plusDays(1), TokenStatus.ENDED)
.build())
.build());
assertAboutEppExceptions()
.that(assertThrows(AllocationTokenNotValidForTldException.class, this::runFlow))
.marshalsToXml();
runFlowAssertResponse(
loadFile(
"domain_renew_response.xml",
ImmutableMap.of("DOMAIN", "example.tld", "EXDATE", "2002-04-03T22:00:00.0Z")));
assertAllocationTokenWasNotRedeemed("abc123");
}
@@ -52,12 +52,11 @@ import google.registry.flows.ResourceFlowUtils.BadAuthInfoForResourceException;
import google.registry.flows.ResourceFlowUtils.ResourceDoesNotExistException;
import google.registry.flows.ResourceFlowUtils.ResourceNotOwnedException;
import google.registry.flows.domain.DomainFlowUtils.NotAuthorizedForTldException;
import google.registry.flows.domain.token.AllocationTokenFlowUtils;
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.domain.token.AllocationTokenFlowUtils.NonexistentAllocationTokenException;
import google.registry.flows.exceptions.NotPendingTransferException;
import google.registry.model.billing.BillingBase;
import google.registry.model.billing.BillingBase.Reason;
@@ -897,7 +896,7 @@ class DomainTransferApproveFlowTest
@Test
void testFailure_invalidAllocationToken() throws Exception {
setEppInput("domain_transfer_approve_allocation_token.xml");
EppException thrown = assertThrows(InvalidAllocationTokenException.class, this::runFlow);
EppException thrown = assertThrows(NonexistentAllocationTokenException.class, this::runFlow);
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@@ -910,9 +909,12 @@ class DomainTransferApproveFlowTest
.setDomainName("otherdomain.tld")
.build());
setEppInput("domain_transfer_approve_allocation_token.xml");
EppException thrown =
assertThrows(AllocationTokenNotValidForDomainException.class, this::runFlow);
assertAboutEppExceptions().that(thrown).marshalsToXml();
assertAboutEppExceptions()
.that(
assertThrows(
AllocationTokenFlowUtils.AllocationTokenNotValidForDomainException.class,
this::runFlow))
.marshalsToXml();
}
@Test
@@ -971,8 +973,7 @@ class DomainTransferApproveFlowTest
.build())
.build());
setEppInput("domain_transfer_approve_allocation_token.xml");
EppException thrown = assertThrows(AllocationTokenNotValidForTldException.class, this::runFlow);
assertAboutEppExceptions().that(thrown).marshalsToXml();
runFlowAssertResponse(loadFile("domain_transfer_approve_response.xml"));
}
@Test
@@ -79,11 +79,9 @@ import google.registry.flows.domain.DomainFlowUtils.PremiumNameBlockedException;
import google.registry.flows.domain.DomainFlowUtils.RegistrarMustBeActiveForThisOperationException;
import google.registry.flows.domain.DomainFlowUtils.UnsupportedFeeAttributeException;
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.domain.token.AllocationTokenFlowUtils.NonexistentAllocationTokenException;
import google.registry.flows.exceptions.AlreadyPendingTransferException;
import google.registry.flows.exceptions.InvalidTransferPeriodValueException;
import google.registry.flows.exceptions.MissingTransferRequestAuthInfoException;
@@ -505,7 +503,7 @@ class DomainTransferRequestFlowTest
implicitTransferTime,
transferCost,
originalGracePeriods,
/* expectTransferBillingEvent = */ true,
/* expectTransferBillingEvent= */ true,
extraExpectedBillingEvents);
assertPollMessagesEmitted(expectedExpirationTime, implicitTransferTime);
@@ -1859,22 +1857,7 @@ class DomainTransferRequestFlowTest
void testFailure_invalidAllocationToken() throws Exception {
setupDomain("example", "tld");
setEppInput("domain_transfer_request_allocation_token.xml", ImmutableMap.of("TOKEN", "abc123"));
EppException thrown = assertThrows(InvalidAllocationTokenException.class, this::runFlow);
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
void testFailure_allocationTokenIsForDifferentName() throws Exception {
setupDomain("example", "tld");
persistResource(
new AllocationToken.Builder()
.setToken("abc123")
.setTokenType(SINGLE_USE)
.setDomainName("otherdomain.tld")
.build());
setEppInput("domain_transfer_request_allocation_token.xml", ImmutableMap.of("TOKEN", "abc123"));
EppException thrown =
assertThrows(AllocationTokenNotValidForDomainException.class, this::runFlow);
EppException thrown = assertThrows(NonexistentAllocationTokenException.class, this::runFlow);
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@@ -1920,27 +1903,6 @@ class DomainTransferRequestFlowTest
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
void testFailure_allocationTokenNotValidForTld() throws Exception {
setupDomain("example", "tld");
persistResource(
new AllocationToken.Builder()
.setToken("abc123")
.setTokenType(UNLIMITED_USE)
.setAllowedTlds(ImmutableSet.of("example"))
.setDiscountFraction(0.5)
.setTokenStatusTransitions(
ImmutableSortedMap.<DateTime, TokenStatus>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_transfer_request_allocation_token.xml", ImmutableMap.of("TOKEN", "abc123"));
EppException thrown = assertThrows(AllocationTokenNotValidForTldException.class, this::runFlow);
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
void testFailure_allocationTokenAlreadyRedeemed() throws Exception {
setupDomain("example", "tld");
@@ -19,31 +19,25 @@ import static google.registry.model.domain.token.AllocationToken.TokenStatus.CAN
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.DEFAULT_PROMO;
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.persistActiveDomain;
import static google.registry.testing.DatabaseHelper.persistResource;
import static google.registry.testing.EppExceptionSubject.assertAboutEppExceptions;
import static google.registry.util.DateTimeUtils.START_OF_TIME;
import static org.joda.time.DateTimeZone.UTC;
import static org.junit.jupiter.api.Assertions.assertThrows;
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 google.registry.flows.EppException;
import google.registry.flows.domain.token.AllocationTokenFlowUtils.AllocationTokenNotInPromotionException;
import google.registry.flows.domain.token.AllocationTokenFlowUtils.AllocationTokenNotValidForCommandException;
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.Domain;
import google.registry.model.domain.DomainCommand;
import google.registry.flows.domain.token.AllocationTokenFlowUtils.NonexistentAllocationTokenException;
import google.registry.model.domain.fee.FeeQueryCommandExtensionItem.CommandName;
import google.registry.model.domain.token.AllocationToken;
import google.registry.model.domain.token.AllocationToken.TokenStatus;
@@ -52,7 +46,7 @@ import google.registry.model.reporting.HistoryEntry.HistoryEntryId;
import google.registry.model.tld.Tld;
import google.registry.persistence.transaction.JpaTestExtensions;
import google.registry.persistence.transaction.JpaTestExtensions.JpaIntegrationTestExtension;
import google.registry.testing.DatabaseHelper;
import google.registry.testing.FakeClock;
import java.util.Optional;
import org.joda.time.DateTime;
import org.junit.jupiter.api.BeforeEach;
@@ -62,319 +56,322 @@ import org.junit.jupiter.api.extension.RegisterExtension;
/** Unit tests for {@link AllocationTokenFlowUtils}. */
class AllocationTokenFlowUtilsTest {
private final AllocationTokenFlowUtils flowUtils = new AllocationTokenFlowUtils();
private final FakeClock clock = new FakeClock(DateTime.parse("2025-01-10T01:00:00.000Z"));
@RegisterExtension
final JpaIntegrationTestExtension jpa =
new JpaTestExtensions.Builder().buildIntegrationTestExtension();
new JpaTestExtensions.Builder().withClock(clock).buildIntegrationTestExtension();
private final AllocationTokenExtension allocationTokenExtension =
mock(AllocationTokenExtension.class);
private Tld tld;
@BeforeEach
void beforeEach() {
createTld("tld");
tld = createTld("tld");
}
@Test
void test_validateToken_successfullyVerifiesValidTokenOnCreate() throws Exception {
void testSuccess_redeemsToken() {
HistoryEntryId historyEntryId = new HistoryEntryId("repoId", 10L);
assertThat(
AllocationTokenFlowUtils.redeemToken(singleUseTokenBuilder().build(), historyEntryId)
.getRedemptionHistoryId())
.hasValue(historyEntryId);
}
@Test
void testInvalidForPremiumName_validForPremium() {
AllocationToken token = singleUseTokenBuilder().setDiscountPremiums(true).build();
assertThat(AllocationTokenFlowUtils.discountTokenInvalidForPremiumName(token, true)).isFalse();
}
@Test
void testInvalidForPremiumName_notPremium() {
assertThat(
AllocationTokenFlowUtils.discountTokenInvalidForPremiumName(
singleUseTokenBuilder().build(), false))
.isFalse();
}
@Test
void testInvalidForPremiumName_invalidForPremium() {
assertThat(
AllocationTokenFlowUtils.discountTokenInvalidForPremiumName(
singleUseTokenBuilder().build(), true))
.isTrue();
}
@Test
void testSuccess_loadFromExtension() throws Exception {
AllocationToken token =
persistResource(
new AllocationToken.Builder()
.setToken("tokeN")
.setAllowedEppActions(ImmutableSet.of(CommandName.CREATE, CommandName.RESTORE))
.setAllowedEppActions(ImmutableSet.of(CommandName.CREATE))
.setTokenType(SINGLE_USE)
.build());
when(allocationTokenExtension.getAllocationToken()).thenReturn("tokeN");
assertThat(
flowUtils
.verifyAllocationTokenCreateIfPresent(
createCommand("blah.tld"),
Tld.get("tld"),
"TheRegistrar",
DateTime.now(UTC),
Optional.of(allocationTokenExtension))
.get())
.isEqualTo(token);
AllocationTokenFlowUtils.loadAllocationTokenFromExtension(
"TheRegistrar",
"example.tld",
clock.nowUtc(),
Optional.of(allocationTokenExtension)))
.hasValue(token);
}
@Test
void test_validateToken_successfullyVerifiesValidTokenExistingDomain() throws Exception {
void testSuccess_loadOrDefault_fromExtensionEvenWhenDefaultPresent() throws Exception {
persistDefaultToken();
AllocationToken token =
persistResource(
new AllocationToken.Builder()
.setToken("tokeN")
.setAllowedEppActions(ImmutableSet.of(CommandName.CREATE, CommandName.RENEW))
.setAllowedEppActions(ImmutableSet.of(CommandName.CREATE))
.setTokenType(SINGLE_USE)
.build());
when(allocationTokenExtension.getAllocationToken()).thenReturn("tokeN");
assertThat(
flowUtils
.verifyAllocationTokenIfPresent(
DatabaseHelper.newDomain("blah.tld"),
Tld.get("tld"),
"TheRegistrar",
DateTime.now(UTC),
CommandName.RENEW,
Optional.of(allocationTokenExtension))
.get())
.isEqualTo(token);
AllocationTokenFlowUtils.loadTokenFromExtensionOrGetDefault(
"TheRegistrar",
clock.nowUtc(),
Optional.of(allocationTokenExtension),
tld,
"example.tld",
CommandName.CREATE))
.hasValue(token);
}
void test_validateToken_emptyAllowedEppActions_successfullyVerifiesValidTokenExistingDomain()
throws Exception {
AllocationToken token =
persistResource(
new AllocationToken.Builder().setToken("tokeN").setTokenType(SINGLE_USE).build());
when(allocationTokenExtension.getAllocationToken()).thenReturn("tokeN");
@Test
void testSuccess_loadOrDefault_defaultWhenNonePresent() throws Exception {
AllocationToken defaultToken = persistDefaultToken();
assertThat(
flowUtils
.verifyAllocationTokenIfPresent(
DatabaseHelper.newDomain("blah.tld"),
Tld.get("tld"),
"TheRegistrar",
DateTime.now(UTC),
CommandName.RENEW,
Optional.of(allocationTokenExtension))
.get())
.isEqualTo(token);
AllocationTokenFlowUtils.loadTokenFromExtensionOrGetDefault(
"TheRegistrar",
clock.nowUtc(),
Optional.empty(),
tld,
"example.tld",
CommandName.CREATE))
.hasValue(defaultToken);
}
@Test
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.verifyAllocationTokenCreateIfPresent(
createCommand("blah.tld"),
Tld.get("tld"),
"TheRegistrar",
DateTime.now(UTC),
Optional.of(allocationTokenExtension))))
.marshalsToXml();
}
@Test
void test_validateTokenExistingDomain_failsOnNullToken() {
assertAboutEppExceptions()
.that(
assertThrows(
InvalidAllocationTokenException.class,
() ->
flowUtils.verifyAllocationTokenIfPresent(
DatabaseHelper.newDomain("blah.tld"),
Tld.get("tld"),
"TheRegistrar",
DateTime.now(UTC),
CommandName.RENEW,
Optional.of(allocationTokenExtension))))
.marshalsToXml();
}
@Test
void test_validateTokenCreate_invalidForClientId() {
persistResource(
createOneMonthPromoTokenBuilder(DateTime.now(UTC).minusDays(1))
.setAllowedRegistrarIds(ImmutableSet.of("NewRegistrar"))
.build());
assertValidateCreateThrowsEppException(AllocationTokenNotValidForRegistrarException.class);
}
@Test
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());
assertValidateCreateThrowsEppException(AllocationTokenNotValidForTldException.class);
}
@Test
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());
assertValidateCreateThrowsEppException(AllocationTokenNotInPromotionException.class);
}
@Test
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());
assertValidateCreateThrowsEppException(AllocationTokenNotInPromotionException.class);
}
@Test
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))
.setTokenStatusTransitions(
ImmutableSortedMap.<DateTime, TokenStatus>naturalOrder()
.put(START_OF_TIME, NOT_STARTED)
.put(DateTime.now(UTC).minusMonths(1), VALID)
.put(DateTime.now(UTC).minusHours(12), CANCELLED)
.build())
.build());
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.<DateTime, TokenStatus>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
void test_validateTokenCreate_invalidCommand() {
persistResource(
createOneMonthPromoTokenBuilder(DateTime.now(UTC).minusDays(1))
.setAllowedEppActions(ImmutableSet.of(CommandName.RENEW))
.build());
assertValidateCreateThrowsEppException(AllocationTokenNotValidForCommandException.class);
}
@Test
void test_validateTokenExistingDomain_invalidCommand() {
persistResource(
createOneMonthPromoTokenBuilder(DateTime.now(UTC).minusDays(1))
.setAllowedEppActions(ImmutableSet.of(CommandName.CREATE))
.build());
assertValidateExistingDomainThrowsEppException(
AllocationTokenNotValidForCommandException.class);
}
@Test
void test_checkDomainsWithToken_successfullyVerifiesValidToken() {
persistResource(
new AllocationToken.Builder().setToken("tokeN").setTokenType(SINGLE_USE).build());
assertThat(
flowUtils
.checkDomainsWithToken(
ImmutableList.of(
InternetDomainName.from("blah.tld"), InternetDomainName.from("blah2.tld")),
"tokeN",
"TheRegistrar",
DateTime.now(UTC))
.domainCheckResults())
.containsExactlyEntriesIn(
ImmutableMap.of(
InternetDomainName.from("blah.tld"), "", InternetDomainName.from("blah2.tld"), ""))
.inOrder();
}
@Test
void test_checkDomainsWithToken_showsFailureMessageForRedeemedToken() {
Domain domain = persistActiveDomain("example.tld");
HistoryEntryId historyEntryId = new HistoryEntryId(domain.getRepoId(), 1051L);
void testSuccess_loadOrDefault_defaultWhenTokenIsPresentButNotApplicable() throws Exception {
AllocationToken defaultToken = persistDefaultToken();
persistResource(
new AllocationToken.Builder()
.setToken("tokeN")
.setAllowedEppActions(ImmutableSet.of(CommandName.CREATE))
.setTokenType(SINGLE_USE)
.setRedemptionHistoryId(historyEntryId)
.setAllowedTlds(ImmutableSet.of("othertld"))
.build());
when(allocationTokenExtension.getAllocationToken()).thenReturn("tokeN");
assertThat(
flowUtils
.checkDomainsWithToken(
ImmutableList.of(
InternetDomainName.from("blah.tld"), InternetDomainName.from("blah2.tld")),
"tokeN",
"TheRegistrar",
DateTime.now(UTC))
.domainCheckResults())
.containsExactlyEntriesIn(
ImmutableMap.of(
InternetDomainName.from("blah.tld"),
"Alloc token was already redeemed",
InternetDomainName.from("blah2.tld"),
"Alloc token was already redeemed"))
.inOrder();
AllocationTokenFlowUtils.loadTokenFromExtensionOrGetDefault(
"TheRegistrar",
clock.nowUtc(),
Optional.of(allocationTokenExtension),
tld,
"example.tld",
CommandName.CREATE))
.hasValue(defaultToken);
}
private void assertValidateCreateThrowsEppException(Class<? extends EppException> clazz) {
@Test
void testValidAgainstDomain_validAllReasons() {
AllocationToken token = singleUseTokenBuilder().setDiscountPremiums(true).build();
assertThat(
AllocationTokenFlowUtils.tokenIsValidAgainstDomain(
InternetDomainName.from("rich.tld"), token, CommandName.CREATE, clock.nowUtc()))
.isTrue();
}
@Test
void testValidAgainstDomain_invalidPremium() {
AllocationToken token = singleUseTokenBuilder().build();
assertThat(
AllocationTokenFlowUtils.tokenIsValidAgainstDomain(
InternetDomainName.from("rich.tld"), token, CommandName.CREATE, clock.nowUtc()))
.isFalse();
}
@Test
void testValidAgainstDomain_invalidAction() {
AllocationToken token =
singleUseTokenBuilder().setAllowedEppActions(ImmutableSet.of(CommandName.RESTORE)).build();
assertThat(
AllocationTokenFlowUtils.tokenIsValidAgainstDomain(
InternetDomainName.from("domain.tld"), token, CommandName.CREATE, clock.nowUtc()))
.isFalse();
}
@Test
void testValidAgainstDomain_invalidTld() {
createTld("othertld");
AllocationToken token = singleUseTokenBuilder().build();
assertThat(
AllocationTokenFlowUtils.tokenIsValidAgainstDomain(
InternetDomainName.from("domain.othertld"),
token,
CommandName.CREATE,
clock.nowUtc()))
.isFalse();
}
@Test
void testValidAgainstDomain_invalidDomain() {
AllocationToken token = singleUseTokenBuilder().setDomainName("anchor.tld").build();
assertThat(
AllocationTokenFlowUtils.tokenIsValidAgainstDomain(
InternetDomainName.from("domain.tld"), token, CommandName.CREATE, clock.nowUtc()))
.isFalse();
}
@Test
void testFailure_redeemToken_nonSingleUse() {
assertThrows(
IllegalArgumentException.class,
() ->
AllocationTokenFlowUtils.redeemToken(
createOneMonthPromoTokenBuilder(clock.nowUtc()).build(),
new HistoryEntryId("repoId", 10L)));
}
@Test
void testFailure_loadFromExtension_nonexistentToken() {
assertLoadTokenFromExtensionThrowsException(NonexistentAllocationTokenException.class);
}
@Test
void testFailure_loadFromExtension_nullToken() {
when(allocationTokenExtension.getAllocationToken()).thenReturn(null);
assertLoadTokenFromExtensionThrowsException(NonexistentAllocationTokenException.class);
}
@Test
void testFailure_tokenInvalidForRegistrar() {
persistResource(
createOneMonthPromoTokenBuilder(clock.nowUtc().minusDays(1))
.setAllowedRegistrarIds(ImmutableSet.of("NewRegistrar"))
.build());
assertLoadTokenFromExtensionThrowsException(AllocationTokenNotValidForRegistrarException.class);
}
@Test
void testFailure_beforePromoStart() {
persistResource(createOneMonthPromoTokenBuilder(clock.nowUtc().plusDays(1)).build());
assertLoadTokenFromExtensionThrowsException(AllocationTokenNotInPromotionException.class);
}
@Test
void testFailure_afterPromoEnd() {
persistResource(createOneMonthPromoTokenBuilder(clock.nowUtc().minusMonths(2)).build());
assertLoadTokenFromExtensionThrowsException(AllocationTokenNotInPromotionException.class);
}
@Test
void testFailure_promoCancelled() {
// the promo would be valid, but it was cancelled 12 hours ago
persistResource(
createOneMonthPromoTokenBuilder(clock.nowUtc().minusDays(1))
.setTokenStatusTransitions(
ImmutableSortedMap.<DateTime, TokenStatus>naturalOrder()
.put(START_OF_TIME, NOT_STARTED)
.put(clock.nowUtc().minusMonths(1), VALID)
.put(clock.nowUtc().minusHours(12), CANCELLED)
.build())
.build());
assertLoadTokenFromExtensionThrowsException(AllocationTokenNotInPromotionException.class);
}
@Test
void testFailure_loadOrDefault_badTokenProvided() throws Exception {
when(allocationTokenExtension.getAllocationToken()).thenReturn("asdf");
assertThrows(
NonexistentAllocationTokenException.class,
() ->
AllocationTokenFlowUtils.loadTokenFromExtensionOrGetDefault(
"TheRegistrar",
clock.nowUtc(),
Optional.of(allocationTokenExtension),
tld,
"example.tld",
CommandName.CREATE));
}
@Test
void testFailure_loadOrDefault_noValidTokens() throws Exception {
assertThat(
AllocationTokenFlowUtils.loadTokenFromExtensionOrGetDefault(
"TheRegistrar",
clock.nowUtc(),
Optional.empty(),
tld,
"example.tld",
CommandName.CREATE))
.isEmpty();
}
@Test
void testFailure_loadOrDefault_badDomainName() throws Exception {
// Tokens tied to a domain should throw a catastrophic exception if used for a different domain
persistResource(singleUseTokenBuilder().setDomainName("someotherdomain.tld").build());
when(allocationTokenExtension.getAllocationToken()).thenReturn("tokeN");
assertThrows(
AllocationTokenFlowUtils.AllocationTokenNotValidForDomainException.class,
() ->
AllocationTokenFlowUtils.loadTokenFromExtensionOrGetDefault(
"TheRegistrar",
clock.nowUtc(),
Optional.of(allocationTokenExtension),
tld,
"example.tld",
CommandName.CREATE));
}
private AllocationToken persistDefaultToken() {
AllocationToken defaultToken =
persistResource(
new AllocationToken.Builder()
.setToken("defaultToken")
.setDiscountFraction(0.1)
.setAllowedTlds(ImmutableSet.of("tld"))
.setAllowedRegistrarIds(ImmutableSet.of("TheRegistrar"))
.setTokenType(DEFAULT_PROMO)
.build());
tld =
persistResource(
tld.asBuilder()
.setDefaultPromoTokens(ImmutableList.of(defaultToken.createVKey()))
.build());
return defaultToken;
}
private void assertLoadTokenFromExtensionThrowsException(Class<? extends EppException> clazz) {
assertAboutEppExceptions()
.that(
assertThrows(
clazz,
() ->
flowUtils.verifyAllocationTokenCreateIfPresent(
createCommand("blah.tld"),
Tld.get("tld"),
AllocationTokenFlowUtils.loadAllocationTokenFromExtension(
"TheRegistrar",
DateTime.now(UTC),
"example.tld",
clock.nowUtc(),
Optional.of(allocationTokenExtension))))
.marshalsToXml();
}
private void assertValidateExistingDomainThrowsEppException(Class<? extends EppException> clazz) {
assertAboutEppExceptions()
.that(
assertThrows(
clazz,
() ->
flowUtils.verifyAllocationTokenIfPresent(
DatabaseHelper.newDomain("blah.tld"),
Tld.get("tld"),
"TheRegistrar",
DateTime.now(UTC),
CommandName.RENEW,
Optional.of(allocationTokenExtension))))
.marshalsToXml();
}
private static DomainCommand.Create createCommand(String domainName) {
DomainCommand.Create command = mock(DomainCommand.Create.class);
when(command.getDomainName()).thenReturn(domainName);
return command;
private AllocationToken.Builder singleUseTokenBuilder() {
when(allocationTokenExtension.getAllocationToken()).thenReturn("tokeN");
return new AllocationToken.Builder()
.setTokenType(SINGLE_USE)
.setToken("tokeN")
.setAllowedTlds(ImmutableSet.of("tld"))
.setDiscountFraction(0.1)
.setAllowedRegistrarIds(ImmutableSet.of("TheRegistrar"));
}
private AllocationToken.Builder createOneMonthPromoTokenBuilder(DateTime promoStart) {
@@ -6,6 +6,7 @@
<domain:name>example1.tld</domain:name>
<domain:name>example2.example</domain:name>
<domain:name>reserved.tld</domain:name>
<domain:name>rich.example</domain:name>
</domain:check>
</check>
<extension>
@@ -34,6 +35,12 @@
<fee:command>create</fee:command>
<fee:period unit="y">1</fee:period>
</fee:domain>
<fee:domain>
<fee:name>rich.example</fee:name>
<fee:currency>USD</fee:currency>
<fee:command>create</fee:command>
<fee:period unit="y">1</fee:period>
</fee:domain>
<fee:domain>
<fee:name>example1.tld</fee:name>
<fee:currency>USD</fee:currency>
@@ -52,6 +59,12 @@
<fee:command>renew</fee:command>
<fee:period unit="y">1</fee:period>
</fee:domain>
<fee:domain>
<fee:name>rich.example</fee:name>
<fee:currency>USD</fee:currency>
<fee:command>renew</fee:command>
<fee:period unit="y">1</fee:period>
</fee:domain>
</fee:check>
</extension>
<clTRID>ABC-12345</clTRID>
@@ -17,6 +17,10 @@
<domain:name avail="false">reserved.tld</domain:name>
<domain:reason>Alloc token invalid for domain</domain:reason>
</domain:cd>
<domain:cd>
<domain:name avail="false">rich.example</domain:name>
<domain:reason>Alloc token invalid for domain</domain:reason>
</domain:cd>
</domain:chkData>
</resData>
<extension>
@@ -42,6 +46,13 @@
<fee:period unit="y">1</fee:period>
<fee:class>token-not-supported</fee:class>
</fee:cd>
<fee:cd>
<fee:name>rich.example</fee:name>
<fee:currency>USD</fee:currency>
<fee:command>create</fee:command>
<fee:period unit="y">1</fee:period>
<fee:class>token-not-supported</fee:class>
</fee:cd>
<fee:cd>
<fee:name>example1.tld</fee:name>
<fee:currency>USD</fee:currency>
@@ -64,6 +75,13 @@
<fee:period unit="y">1</fee:period>
<fee:class>token-not-supported</fee:class>
</fee:cd>
<fee:cd>
<fee:name>rich.example</fee:name>
<fee:currency>USD</fee:currency>
<fee:command>renew</fee:command>
<fee:period unit="y">1</fee:period>
<fee:class>token-not-supported</fee:class>
</fee:cd>
</fee:chkData>
</extension>
<trID>
@@ -16,6 +16,9 @@
<domain:name avail="false">reserved.tld</domain:name>
<domain:reason>Reserved</domain:reason>
</domain:cd>
<domain:cd>
<domain:name avail="true">rich.example</domain:name>
</domain:cd>
</domain:chkData>
</resData>
<extension>
@@ -41,26 +44,42 @@
<fee:period unit="y">1</fee:period>
<fee:class>reserved</fee:class>
</fee:cd>
<fee:cd>
<fee:name>rich.example</fee:name>
<fee:currency>USD</fee:currency>
<fee:command>create</fee:command>
<fee:period unit="y">1</fee:period>
<fee:fee description="create">100.00</fee:fee>
<fee:class>premium</fee:class>
</fee:cd>
<fee:cd>
<fee:name>example1.tld</fee:name>
<fee:currency>USD</fee:currency>
<fee:command>renew</fee:command>
<fee:period unit="y">1</fee:period>
<fee:class>token-not-supported</fee:class>
<fee:fee description="renew">11.00</fee:fee>
</fee:cd>
<fee:cd>
<fee:name>example2.example</fee:name>
<fee:currency>USD</fee:currency>
<fee:command>renew</fee:command>
<fee:period unit="y">1</fee:period>
<fee:class>token-not-supported</fee:class>
<fee:fee description="renew">11.00</fee:fee>
</fee:cd>
<fee:cd>
<fee:name>reserved.tld</fee:name>
<fee:currency>USD</fee:currency>
<fee:command>renew</fee:command>
<fee:period unit="y">1</fee:period>
<fee:class>token-not-supported</fee:class>
<fee:fee description="renew">11.00</fee:fee>
</fee:cd>
<fee:cd>
<fee:name>rich.example</fee:name>
<fee:currency>USD</fee:currency>
<fee:command>renew</fee:command>
<fee:period unit="y">1</fee:period>
<fee:fee description="renew">100.00</fee:fee>
<fee:class>premium</fee:class>
</fee:cd>
</fee:chkData>
</extension>
@@ -16,7 +16,7 @@
</domain:cd>
<domain:cd>
<domain:name avail="false">reserved.tld</domain:name>
<domain:reason>Reserved</domain:reason>
<domain:reason>Alloc token invalid for domain</domain:reason>
</domain:cd>
<domain:cd>
<domain:name avail="true">specificuse.tld</domain:name>
@@ -17,14 +17,14 @@
<fee:currency>USD</fee:currency>
<fee:command>create</fee:command>
<fee:period unit="y">1</fee:period>
<fee:fee description="create">13.00</fee:fee>
<fee:fee description="create">11.70</fee:fee>
</fee:cd>
<fee:cd xmlns:fee="urn:ietf:params:xml:ns:fee-0.6">
<fee:name>example1.tld</fee:name>
<fee:currency>USD</fee:currency>
<fee:command>renew</fee:command>
<fee:period unit="y">1</fee:period>
<fee:class>token-not-supported</fee:class>
<fee:fee description="renew">11.00</fee:fee>
</fee:cd>
<fee:cd xmlns:fee="urn:ietf:params:xml:ns:fee-0.6">
<fee:name>example1.tld</fee:name>
@@ -38,14 +38,14 @@
<fee:currency>USD</fee:currency>
<fee:command>restore</fee:command>
<fee:period unit="y">1</fee:period>
<fee:class>token-not-supported</fee:class>
<fee:fee description="restore">17.00</fee:fee>
</fee:cd>
<fee:cd xmlns:fee="urn:ietf:params:xml:ns:fee-0.6">
<fee:name>example1.tld</fee:name>
<fee:currency>USD</fee:currency>
<fee:command>update</fee:command>
<fee:period unit="y">1</fee:period>
<fee:class>token-not-supported</fee:class>
<fee:fee description="update">0.00</fee:fee>
</fee:cd>
</fee:chkData>
</extension>
@@ -19,7 +19,7 @@
</fee:object>
<fee:command name="create">
<fee:period unit="y">1</fee:period>
<fee:fee description="create">13.00</fee:fee>
<fee:fee description="create">11.70</fee:fee>
</fee:command>
</fee:cd>
<fee:cd>
@@ -28,7 +28,7 @@
</fee:object>
<fee:command name="renew">
<fee:period unit="y">1</fee:period>
<fee:class>token-not-supported</fee:class>
<fee:fee description="renew">11.00</fee:fee>
</fee:command>
</fee:cd>
<fee:cd>
@@ -46,7 +46,7 @@
</fee:object>
<fee:command name="restore">
<fee:period unit="y">1</fee:period>
<fee:class>token-not-supported</fee:class>
<fee:fee description="restore">17.00</fee:fee>
</fee:command>
</fee:cd>
<fee:cd>
@@ -55,7 +55,7 @@
</fee:object>
<fee:command name="update">
<fee:period unit="y">1</fee:period>
<fee:class>token-not-supported</fee:class>
<fee:fee description="update">0.00</fee:fee>
</fee:command>
</fee:cd>
</fee:chkData>