diff --git a/core/src/main/java/google/registry/beam/billing/ExpandBillingRecurrencesPipeline.java b/core/src/main/java/google/registry/beam/billing/ExpandBillingRecurrencesPipeline.java index a62c6059f..193103c69 100644 --- a/core/src/main/java/google/registry/beam/billing/ExpandBillingRecurrencesPipeline.java +++ b/core/src/main/java/google/registry/beam/billing/ExpandBillingRecurrencesPipeline.java @@ -36,8 +36,6 @@ import google.registry.config.RegistryConfig.ConfigModule; import google.registry.flows.custom.CustomLogicFactoryModule; import google.registry.flows.custom.CustomLogicModule; import google.registry.flows.domain.DomainPricingLogic; -import google.registry.flows.domain.DomainPricingLogic.AllocationTokenInvalidForCurrencyException; -import google.registry.flows.domain.DomainPricingLogic.AllocationTokenInvalidForPremiumNameException; import google.registry.model.ImmutableObject; import google.registry.model.billing.BillingBase.Flag; import google.registry.model.billing.BillingCancellation; @@ -389,38 +387,30 @@ public class ExpandBillingRecurrencesPipeline implements Serializable { // It is OK to always create a OneTime, even though the domain might be deleted or transferred // later during autorenew grace period, as a cancellation will always be written out in those // instances. - BillingEvent billingEvent = null; - try { - billingEvent = - new BillingEvent.Builder() - .setBillingTime(billingTime) - .setRegistrarId(billingRecurrence.getRegistrarId()) - // Determine the cost for a one-year renewal. - .setCost( - domainPricingLogic - .getRenewPrice( - tld, - billingRecurrence.getTargetId(), - eventTime, - 1, - billingRecurrence, - Optional.empty()) - .getRenewCost()) - .setEventTime(eventTime) - .setFlags(union(billingRecurrence.getFlags(), Flag.SYNTHETIC)) - .setDomainHistory(historyEntry) - .setPeriodYears(1) - .setReason(billingRecurrence.getReason()) - .setSyntheticCreationTime(endTime) - .setCancellationMatchingBillingEvent(billingRecurrence) - .setTargetId(billingRecurrence.getTargetId()) - .build(); - } catch (AllocationTokenInvalidForCurrencyException - | AllocationTokenInvalidForPremiumNameException e) { - // This should not be reached since we are not using an allocation token - return; - } - results.add(billingEvent); + results.add( + new BillingEvent.Builder() + .setBillingTime(billingTime) + .setRegistrarId(billingRecurrence.getRegistrarId()) + // Determine the cost for a one-year renewal. + .setCost( + domainPricingLogic + .getRenewPrice( + tld, + billingRecurrence.getTargetId(), + eventTime, + 1, + billingRecurrence, + Optional.empty()) + .getRenewCost()) + .setEventTime(eventTime) + .setFlags(union(billingRecurrence.getFlags(), Flag.SYNTHETIC)) + .setDomainHistory(historyEntry) + .setPeriodYears(1) + .setReason(billingRecurrence.getReason()) + .setSyntheticCreationTime(endTime) + .setCancellationMatchingBillingEvent(billingRecurrence) + .setTargetId(billingRecurrence.getTargetId()) + .build()); } results.add( billingRecurrence diff --git a/core/src/main/java/google/registry/flows/domain/DomainCheckFlow.java b/core/src/main/java/google/registry/flows/domain/DomainCheckFlow.java index 8542de60f..79bf94cf5 100644 --- a/core/src/main/java/google/registry/flows/domain/DomainCheckFlow.java +++ b/core/src/main/java/google/registry/flows/domain/DomainCheckFlow.java @@ -14,7 +14,6 @@ package google.registry.flows.domain; -import static com.google.common.base.Strings.emptyToNull; import static com.google.common.collect.ImmutableList.toImmutableList; import static com.google.common.collect.ImmutableMap.toImmutableMap; import static com.google.common.collect.ImmutableSet.toImmutableSet; @@ -42,6 +41,7 @@ import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; import com.google.common.collect.Maps; +import com.google.common.flogger.FluentLogger; import com.google.common.net.InternetDomainName; import google.registry.config.RegistryConfig; import google.registry.config.RegistryConfig.Config; @@ -55,14 +55,7 @@ import google.registry.flows.annotations.ReportingSpec; import google.registry.flows.custom.DomainCheckFlowCustomLogic; import google.registry.flows.custom.DomainCheckFlowCustomLogic.BeforeResponseParameters; import google.registry.flows.custom.DomainCheckFlowCustomLogic.BeforeResponseReturnData; -import google.registry.flows.domain.DomainPricingLogic.AllocationTokenInvalidForPremiumNameException; -import google.registry.flows.domain.token.AllocationTokenDomainCheckResults; import google.registry.flows.domain.token.AllocationTokenFlowUtils; -import google.registry.flows.domain.token.AllocationTokenFlowUtils.AllocationTokenNotInPromotionException; -import google.registry.flows.domain.token.AllocationTokenFlowUtils.AllocationTokenNotValidForCommandException; -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.model.EppResource; import google.registry.model.ForeignKeyUtils; import google.registry.model.billing.BillingRecurrence; @@ -87,7 +80,6 @@ import google.registry.model.tld.Tld; import google.registry.model.tld.Tld.TldState; import google.registry.model.tld.label.ReservationType; import google.registry.persistence.VKey; -import google.registry.pricing.PricingEngineProxy; import google.registry.util.Clock; import jakarta.inject.Inject; import java.util.Collection; @@ -131,6 +123,8 @@ import org.joda.time.DateTime; @ReportingSpec(ActivityReportField.DOMAIN_CHECK) public final class DomainCheckFlow implements TransactionalFlow { + private static final FluentLogger logger = FluentLogger.forEnclosingClass(); + private static final String STANDARD_FEE_RESPONSE_CLASS = "STANDARD"; private static final String STANDARD_PROMOTION_FEE_RESPONSE_CLASS = "STANDARD PROMOTION"; @@ -146,7 +140,6 @@ public final class DomainCheckFlow implements TransactionalFlow { @Inject @Superuser boolean isSuperuser; @Inject Clock clock; @Inject EppResponse.Builder responseBuilder; - @Inject AllocationTokenFlowUtils allocationTokenFlowUtils; @Inject DomainCheckFlowCustomLogic flowCustomLogic; @Inject DomainPricingLogic pricingLogic; @@ -195,36 +188,15 @@ public final class DomainCheckFlow implements TransactionalFlow { existingDomains.size() == parsedDomains.size() ? ImmutableSet.of() : getBsaBlockedDomains(parsedDomains.values(), now); - Optional allocationTokenExtension = - eppInput.getSingleExtension(AllocationTokenExtension.class); - Optional tokenDomainCheckResults = - allocationTokenExtension.map( - tokenExtension -> - allocationTokenFlowUtils.checkDomainsWithToken( - ImmutableList.copyOf(parsedDomains.values()), - tokenExtension.getAllocationToken(), - registrarId, - now)); ImmutableList.Builder checksBuilder = new ImmutableList.Builder<>(); ImmutableSet.Builder availableDomains = new ImmutableSet.Builder<>(); ImmutableMap tldStates = Maps.toMap(seenTlds, tld -> Tld.get(tld).getTldState(now)); - ImmutableMap domainCheckResults = - tokenDomainCheckResults - .map(AllocationTokenDomainCheckResults::domainCheckResults) - .orElse(ImmutableMap.of()); - Optional allocationToken = - tokenDomainCheckResults.flatMap(AllocationTokenDomainCheckResults::token); for (String domainName : domainNames) { Optional message = getMessageForCheck( - parsedDomains.get(domainName), - existingDomains, - bsaBlockedDomainNames, - domainCheckResults, - tldStates, - allocationToken); + domainName, existingDomains, bsaBlockedDomainNames, tldStates, parsedDomains, now); boolean isAvailable = message.isEmpty(); checksBuilder.add(DomainCheck.create(isAvailable, domainName, message.orElse(null))); if (isAvailable) { @@ -237,11 +209,7 @@ public final class DomainCheckFlow implements TransactionalFlow { .setDomainChecks(checksBuilder.build()) .setResponseExtensions( getResponseExtensions( - parsedDomains, - existingDomains, - availableDomains.build(), - now, - allocationToken)) + parsedDomains, existingDomains, availableDomains.build(), now)) .setAsOfDate(now) .build()); return responseBuilder @@ -251,10 +219,39 @@ public final class DomainCheckFlow implements TransactionalFlow { } private Optional getMessageForCheck( + String domainName, + ImmutableMap> existingDomains, + ImmutableSet bsaBlockedDomainNames, + ImmutableMap tldStates, + ImmutableMap parsedDomains, + DateTime now) { + InternetDomainName idn = parsedDomains.get(domainName); + Optional token; + try { + // Which token we use may vary based on the domain -- a provided token may be invalid for + // some domains, or there may be DEFAULT PROMO tokens only applicable on some domains + token = + AllocationTokenFlowUtils.loadTokenFromExtensionOrGetDefault( + registrarId, + now, + eppInput.getSingleExtension(AllocationTokenExtension.class), + Tld.get(idn.parent().toString()), + domainName, + FeeQueryCommandExtensionItem.CommandName.CREATE); + } catch (AllocationTokenFlowUtils.NonexistentAllocationTokenException + | AllocationTokenFlowUtils.AllocationTokenInvalidException e) { + // The provided token was catastrophically invalid in some way + logger.atInfo().withCause(e).log("Cannot load/use allocation token."); + return Optional.of(e.getMessage()); + } + return getMessageForCheckWithToken( + idn, existingDomains, bsaBlockedDomainNames, tldStates, token); + } + + private Optional getMessageForCheckWithToken( InternetDomainName domainName, ImmutableMap> existingDomains, ImmutableSet bsaBlockedDomains, - ImmutableMap tokenCheckResults, ImmutableMap tldStates, Optional allocationToken) { if (existingDomains.containsKey(domainName.toString())) { @@ -271,11 +268,6 @@ public final class DomainCheckFlow implements TransactionalFlow { } } } - Optional tokenResult = - Optional.ofNullable(emptyToNull(tokenCheckResults.get(domainName))); - if (tokenResult.isPresent()) { - return tokenResult; - } if (isRegisterBsaCreate(domainName, allocationToken) || !bsaBlockedDomains.contains(domainName)) { return Optional.empty(); @@ -290,8 +282,7 @@ public final class DomainCheckFlow implements TransactionalFlow { ImmutableMap domainNames, ImmutableMap> existingDomains, ImmutableSet availableDomains, - DateTime now, - Optional allocationToken) + DateTime now) throws EppException { Optional feeCheckOpt = eppInput.getSingleExtension(FeeCheckCommandExtension.class); @@ -309,84 +300,24 @@ public final class DomainCheckFlow implements TransactionalFlow { RegistryConfig.getTieredPricingPromotionRegistrarIds().contains(registrarId); for (FeeCheckCommandExtensionItem feeCheckItem : feeCheck.getItems()) { for (String domainName : getDomainNamesToCheckForFee(feeCheckItem, domainNames.keySet())) { - Optional defaultToken = - DomainFlowUtils.checkForDefaultToken( - Tld.get(InternetDomainName.from(domainName).parent().toString()), - domainName, - feeCheckItem.getCommandName(), - registrarId, - now); FeeCheckResponseExtensionItem.Builder builder = feeCheckItem.createResponseBuilder(); Optional domain = Optional.ofNullable(domainObjs.get(domainName)); + Tld tld = Tld.get(domainNames.get(domainName).parent().toString()); + Optional token; try { - if (allocationToken.isPresent()) { - AllocationTokenFlowUtils.validateToken( - InternetDomainName.from(domainName), - allocationToken.get(), - feeCheckItem.getCommandName(), - registrarId, - PricingEngineProxy.isDomainPremium(domainName, now), - now); - } - handleFeeRequest( - feeCheckItem, - builder, - domainNames.get(domainName), - domain, - feeCheck.getCurrency(), - now, - pricingLogic, - allocationToken.isPresent() ? allocationToken : defaultToken, - availableDomains.contains(domainName), - recurrences.getOrDefault(domainName, null)); - // In the case of a registrar that is running a tiered pricing promotion, we issue two - // responses for the CREATE fee check command: one (the default response) with the - // non-promotional price, and one (an extra STANDARD PROMO response) with the actual - // promotional price. - if (defaultToken.isPresent() - && shouldUseTieredPricingPromotion - && feeCheckItem - .getCommandName() - .equals(FeeQueryCommandExtensionItem.CommandName.CREATE)) { - // First, set the promotional (real) price under the STANDARD PROMO class - builder - .setClass(STANDARD_PROMOTION_FEE_RESPONSE_CLASS) - .setCommand( - FeeQueryCommandExtensionItem.CommandName.CUSTOM, - feeCheckItem.getPhase(), - feeCheckItem.getSubphase()); - - // Next, get the non-promotional price and set it as the standard response to the CREATE - // fee check command - FeeCheckResponseExtensionItem.Builder nonPromotionalBuilder = - feeCheckItem.createResponseBuilder(); - handleFeeRequest( - feeCheckItem, - nonPromotionalBuilder, - domainNames.get(domainName), - domain, - feeCheck.getCurrency(), - now, - pricingLogic, - allocationToken, - availableDomains.contains(domainName), - recurrences.getOrDefault(domainName, null)); - responseItems.add( - nonPromotionalBuilder - .setClass(STANDARD_FEE_RESPONSE_CLASS) - .setDomainNameIfSupported(domainName) - .build()); - } - responseItems.add(builder.setDomainNameIfSupported(domainName).build()); - } catch (AllocationTokenInvalidForPremiumNameException - | AllocationTokenNotValidForCommandException - | AllocationTokenNotValidForDomainException - | AllocationTokenNotValidForRegistrarException - | AllocationTokenNotValidForTldException - | AllocationTokenNotInPromotionException e) { - // Allocation token is either not an active token or it is not valid for the EPP command, - // registrar, domain, or TLD. - Tld tld = Tld.get(InternetDomainName.from(domainName).parent().toString()); + // The precise token to use for this fee request may vary based on the domain or even the + // precise command issued (some tokens may be valid only for certain actions) + token = + AllocationTokenFlowUtils.loadTokenFromExtensionOrGetDefault( + registrarId, + now, + eppInput.getSingleExtension(AllocationTokenExtension.class), + tld, + domainName, + feeCheckItem.getCommandName()); + } catch (AllocationTokenFlowUtils.NonexistentAllocationTokenException + | AllocationTokenFlowUtils.AllocationTokenInvalidException e) { + // The provided token was catastrophically invalid in some way responseItems.add( builder .setDomainNameIfSupported(domainName) @@ -398,7 +329,60 @@ public final class DomainCheckFlow implements TransactionalFlow { .setCurrencyIfSupported(tld.getCurrency()) .setClass("token-not-supported") .build()); + continue; } + handleFeeRequest( + feeCheckItem, + builder, + domainNames.get(domainName), + domain, + feeCheck.getCurrency(), + now, + pricingLogic, + token, + availableDomains.contains(domainName), + recurrences.getOrDefault(domainName, null)); + // In the case of a registrar that is running a tiered pricing promotion, we issue two + // responses for the CREATE fee check command: one (the default response) with the + // non-promotional price, and one (an extra STANDARD PROMO response) with the actual + // promotional price. + if (token + .map(t -> t.getTokenType().equals(AllocationToken.TokenType.DEFAULT_PROMO)) + .orElse(false) + && shouldUseTieredPricingPromotion + && feeCheckItem + .getCommandName() + .equals(FeeQueryCommandExtensionItem.CommandName.CREATE)) { + // First, set the promotional (real) price under the STANDARD PROMO class + builder + .setClass(STANDARD_PROMOTION_FEE_RESPONSE_CLASS) + .setCommand( + FeeQueryCommandExtensionItem.CommandName.CUSTOM, + feeCheckItem.getPhase(), + feeCheckItem.getSubphase()); + + // Next, get the non-promotional price and set it as the standard response to the CREATE + // fee check command + FeeCheckResponseExtensionItem.Builder nonPromotionalBuilder = + feeCheckItem.createResponseBuilder(); + handleFeeRequest( + feeCheckItem, + nonPromotionalBuilder, + domainNames.get(domainName), + domain, + feeCheck.getCurrency(), + now, + pricingLogic, + Optional.empty(), + availableDomains.contains(domainName), + recurrences.getOrDefault(domainName, null)); + responseItems.add( + nonPromotionalBuilder + .setClass(STANDARD_FEE_RESPONSE_CLASS) + .setDomainNameIfSupported(domainName) + .build()); + } + responseItems.add(builder.setDomainNameIfSupported(domainName).build()); } } return ImmutableList.of(feeCheck.createResponse(responseItems.build())); 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 db86f8ff4..088501028 100644 --- a/core/src/main/java/google/registry/flows/domain/DomainCreateFlow.java +++ b/core/src/main/java/google/registry/flows/domain/DomainCreateFlow.java @@ -133,11 +133,8 @@ import org.joda.time.Duration; * @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 AllocationTokenFlowUtils.NonexistentAllocationTokenException} * @error {@link google.registry.flows.exceptions.OnlyToolCanPassMetadataException} * @error {@link ResourceAlreadyExistsForThisClientException} * @error {@link ResourceCreateContentionException} @@ -205,7 +202,6 @@ import org.joda.time.Duration; * @error {@link DomainFlowUtils.UnexpectedClaimsNoticeException} * @error {@link DomainFlowUtils.UnsupportedFeeAttributeException} * @error {@link DomainFlowUtils.UnsupportedMarkTypeException} - * @error {@link DomainPricingLogic.AllocationTokenInvalidForPremiumNameException} */ @ReportingSpec(ActivityReportField.DOMAIN_CREATE) public final class DomainCreateFlow implements MutatingFlow { @@ -221,7 +217,6 @@ public final class DomainCreateFlow implements MutatingFlow { @Inject @Superuser boolean isSuperuser; @Inject DomainHistory.Builder historyBuilder; @Inject EppResponse.Builder responseBuilder; - @Inject AllocationTokenFlowUtils allocationTokenFlowUtils; @Inject DomainCreateFlowCustomLogic flowCustomLogic; @Inject DomainFlowTmchUtils tmchUtils; @Inject DomainPricingLogic pricingLogic; @@ -264,25 +259,20 @@ public final class DomainCreateFlow implements MutatingFlow { } boolean isSunriseCreate = hasSignedMarks && (tldState == START_DATE_SUNRISE); Optional allocationToken = - allocationTokenFlowUtils.verifyAllocationTokenCreateIfPresent( - command, - tld, + AllocationTokenFlowUtils.loadTokenFromExtensionOrGetDefault( registrarId, now, - eppInput.getSingleExtension(AllocationTokenExtension.class)); - boolean defaultTokenUsed = false; - if (allocationToken.isEmpty()) { - allocationToken = - DomainFlowUtils.checkForDefaultToken( - tld, command.getDomainName(), CommandName.CREATE, registrarId, now); - if (allocationToken.isPresent()) { - defaultTokenUsed = true; - } - } + eppInput.getSingleExtension(AllocationTokenExtension.class), + tld, + command.getDomainName(), + CommandName.CREATE); + boolean defaultTokenUsed = + allocationToken.map(t -> t.getTokenType().equals(TokenType.DEFAULT_PROMO)).orElse(false); boolean isAnchorTenant = isAnchorTenant( domainName, allocationToken, eppInput.getSingleExtension(MetadataExtension.class)); verifyAnchorTenantValidPeriod(isAnchorTenant, years); + // Superusers can create reserved domains, force creations on domains that require a claims // notice without specifying a claims key, ignore the registry phase, and override blocks on // registering premium domains. @@ -416,7 +406,7 @@ public final class DomainCreateFlow implements MutatingFlow { entitiesToSave.add(domain, domainHistory); if (allocationToken.isPresent() && allocationToken.get().getTokenType().isOneTimeUse()) { entitiesToSave.add( - allocationTokenFlowUtils.redeemToken( + AllocationTokenFlowUtils.redeemToken( allocationToken.get(), domainHistory.getHistoryEntryId())); } if (domain.shouldPublishToDns()) { diff --git a/core/src/main/java/google/registry/flows/domain/DomainFlowUtils.java b/core/src/main/java/google/registry/flows/domain/DomainFlowUtils.java index d1062e0c1..78ee54f40 100644 --- a/core/src/main/java/google/registry/flows/domain/DomainFlowUtils.java +++ b/core/src/main/java/google/registry/flows/domain/DomainFlowUtils.java @@ -42,7 +42,6 @@ import static google.registry.model.tld.label.ReservationType.RESERVED_FOR_ANCHO import static google.registry.model.tld.label.ReservationType.RESERVED_FOR_SPECIFIC_USE; import static google.registry.persistence.transaction.TransactionManagerFactory.tm; import static google.registry.pricing.PricingEngineProxy.isDomainPremium; -import static google.registry.util.CollectionUtils.isNullOrEmpty; import static google.registry.util.CollectionUtils.nullToEmpty; import static google.registry.util.DateTimeUtils.END_OF_TIME; import static google.registry.util.DateTimeUtils.isAtOrAfter; @@ -67,7 +66,6 @@ import com.google.common.collect.Sets; import com.google.common.collect.Streams; import com.google.common.net.InternetDomainName; import google.registry.flows.EppException; -import google.registry.flows.EppException.AssociationProhibitsOperationException; import google.registry.flows.EppException.AuthorizationErrorException; import google.registry.flows.EppException.CommandUseErrorException; import google.registry.flows.EppException.ObjectDoesNotExistException; @@ -77,8 +75,6 @@ import google.registry.flows.EppException.ParameterValueSyntaxErrorException; import google.registry.flows.EppException.RequiredParameterMissingException; import google.registry.flows.EppException.StatusProhibitsOperationException; import google.registry.flows.EppException.UnimplementedOptionException; -import google.registry.flows.domain.DomainPricingLogic.AllocationTokenInvalidForPremiumNameException; -import google.registry.flows.domain.token.AllocationTokenFlowUtils; import google.registry.flows.exceptions.ResourceHasClientUpdateProhibitedException; import google.registry.model.EppResource; import google.registry.model.billing.BillingBase.Flag; @@ -101,7 +97,6 @@ import google.registry.model.domain.fee.BaseFee.FeeType; import google.registry.model.domain.fee.Credit; import google.registry.model.domain.fee.Fee; import google.registry.model.domain.fee.FeeQueryCommandExtensionItem; -import google.registry.model.domain.fee.FeeQueryCommandExtensionItem.CommandName; import google.registry.model.domain.fee.FeeQueryResponseExtensionItem; import google.registry.model.domain.fee.FeeTransformCommandExtension; import google.registry.model.domain.fee.FeeTransformResponseExtension; @@ -1233,52 +1228,6 @@ public class DomainFlowUtils { .getResultList(); } - /** - * Checks if there is a valid default token to be used for a domain create command. - * - *

If there is more than one valid default token for the registration, only the first valid - * token found on the TLD's default token list will be returned. - */ - public static Optional checkForDefaultToken( - Tld tld, String domainName, CommandName commandName, String registrarId, DateTime now) - throws EppException { - if (isNullOrEmpty(tld.getDefaultPromoTokens())) { - return Optional.empty(); - } - Map, Optional> tokens = - AllocationToken.getAll(tld.getDefaultPromoTokens()); - ImmutableList> tokenList = - tld.getDefaultPromoTokens().stream() - .map(tokens::get) - .filter(Optional::isPresent) - .collect(toImmutableList()); - checkState( - !isNullOrEmpty(tokenList), - "Failure while loading default TLD promotions from the database"); - // Check if any of the tokens are valid for this domain registration - for (Optional token : tokenList) { - try { - AllocationTokenFlowUtils.validateToken( - InternetDomainName.from(domainName), - token.get(), - commandName, - registrarId, - isDomainPremium(domainName, now), - now); - } catch (AssociationProhibitsOperationException - | StatusProhibitsOperationException - | AllocationTokenInvalidForPremiumNameException e) { - // Allocation token was not valid for this registration, continue to check the next token in - // the list - continue; - } - // Only use the first valid token in the list - return token; - } - // No valid default token found - return Optional.empty(); - } - /** Resource linked to this domain does not exist. */ static class LinkedResourcesDoNotExistException extends ObjectDoesNotExistException { public LinkedResourcesDoNotExistException(Class type, ImmutableSet resourceIds) { diff --git a/core/src/main/java/google/registry/flows/domain/DomainPricingLogic.java b/core/src/main/java/google/registry/flows/domain/DomainPricingLogic.java index f40bb11a4..d3c8a2429 100644 --- a/core/src/main/java/google/registry/flows/domain/DomainPricingLogic.java +++ b/core/src/main/java/google/registry/flows/domain/DomainPricingLogic.java @@ -16,14 +16,13 @@ package google.registry.flows.domain; import static com.google.common.base.Preconditions.checkArgument; import static google.registry.flows.domain.DomainFlowUtils.zeroInCurrency; -import static google.registry.flows.domain.token.AllocationTokenFlowUtils.validateTokenForPossiblePremiumName; +import static google.registry.flows.domain.token.AllocationTokenFlowUtils.discountTokenInvalidForPremiumName; import static google.registry.pricing.PricingEngineProxy.getPricesForDomainName; import static google.registry.util.PreconditionsUtils.checkArgumentPresent; import com.google.common.net.InternetDomainName; import google.registry.config.RegistryConfig; import google.registry.flows.EppException; -import google.registry.flows.EppException.CommandUseErrorException; import google.registry.flows.custom.DomainPricingCustomLogic; import google.registry.flows.custom.DomainPricingCustomLogic.CreatePriceParameters; import google.registry.flows.custom.DomainPricingCustomLogic.RenewPriceParameters; @@ -129,9 +128,7 @@ public final class DomainPricingLogic { DateTime dateTime, int years, @Nullable BillingRecurrence billingRecurrence, - Optional allocationToken) - throws AllocationTokenInvalidForCurrencyException, - AllocationTokenInvalidForPremiumNameException { + Optional allocationToken) { checkArgument(years > 0, "Number of years must be positive"); Money renewCost; DomainPrices domainPrices = getPricesForDomainName(domainName, dateTime); @@ -260,8 +257,7 @@ public final class DomainPricingLogic { /** Returns the domain create cost with allocation-token-related discounts applied. */ private Money getDomainCreateCostWithDiscount( - DomainPrices domainPrices, int years, Optional allocationToken, Tld tld) - throws EppException { + DomainPrices domainPrices, int years, Optional allocationToken, Tld tld) { return getDomainCostWithDiscount( domainPrices.isPremium(), years, @@ -277,9 +273,7 @@ public final class DomainPricingLogic { DomainPrices domainPrices, DateTime dateTime, int years, - Optional allocationToken) - throws AllocationTokenInvalidForCurrencyException, - AllocationTokenInvalidForPremiumNameException { + Optional allocationToken) { // Short-circuit if the user sent an anchor-tenant or otherwise NONPREMIUM-renewal token if (allocationToken.isPresent()) { AllocationToken token = allocationToken.get(); @@ -315,44 +309,41 @@ public final class DomainPricingLogic { Optional allocationToken, Money firstYearCost, Optional subsequentYearCost, - Tld tld) - throws AllocationTokenInvalidForCurrencyException, - AllocationTokenInvalidForPremiumNameException { + Tld tld) { checkArgument(years > 0, "Registration years to get cost for must be positive."); - validateTokenForPossiblePremiumName(allocationToken, isPremium); Money totalDomainFlowCost = firstYearCost.plus(subsequentYearCost.orElse(firstYearCost).multipliedBy(years - 1)); + if (allocationToken.isEmpty()) { + return totalDomainFlowCost; + } + AllocationToken token = allocationToken.get(); + if (discountTokenInvalidForPremiumName(token, isPremium)) { + return totalDomainFlowCost; + } + if (!token.getTokenBehavior().equals(TokenBehavior.DEFAULT)) { + return totalDomainFlowCost; + } // Apply the allocation token discount, if applicable. - if (allocationToken.isPresent() - && allocationToken.get().getTokenBehavior().equals(TokenBehavior.DEFAULT)) { - if (allocationToken.get().getDiscountPrice().isPresent()) { - if (!tld.getCurrency() - .equals(allocationToken.get().getDiscountPrice().get().getCurrencyUnit())) { - throw new AllocationTokenInvalidForCurrencyException(); - } - - int nonDiscountedYears = Math.max(0, years - allocationToken.get().getDiscountYears()); - totalDomainFlowCost = - allocationToken - .get() - .getDiscountPrice() - .get() - .multipliedBy(allocationToken.get().getDiscountYears()) - .plus(subsequentYearCost.orElse(firstYearCost).multipliedBy(nonDiscountedYears)); - } else { - // Assumes token has discount fraction set. - int discountedYears = Math.min(years, allocationToken.get().getDiscountYears()); + if (token.getDiscountPrice().isPresent() + && tld.getCurrency().equals(token.getDiscountPrice().get().getCurrencyUnit())) { + int nonDiscountedYears = Math.max(0, years - token.getDiscountYears()); + totalDomainFlowCost = + token + .getDiscountPrice() + .get() + .multipliedBy(token.getDiscountYears()) + .plus(subsequentYearCost.orElse(firstYearCost).multipliedBy(nonDiscountedYears)); + } else if (token.getDiscountFraction() > 0) { + int discountedYears = Math.min(years, token.getDiscountYears()); if (discountedYears > 0) { - var discount = - firstYearCost - .plus(subsequentYearCost.orElse(firstYearCost).multipliedBy(discountedYears - 1)) - .multipliedBy( - allocationToken.get().getDiscountFraction(), RoundingMode.HALF_EVEN); + var discount = + firstYearCost + .plus(subsequentYearCost.orElse(firstYearCost).multipliedBy(discountedYears - 1)) + .multipliedBy(token.getDiscountFraction(), RoundingMode.HALF_EVEN); totalDomainFlowCost = totalDomainFlowCost.minus(discount); } } - } return totalDomainFlowCost; } @@ -376,18 +367,4 @@ public final class DomainPricingLogic { : domainPrices.getRenewCost(); return DomainPrices.create(isPremium, createCost, renewCost); } - - /** An allocation token was provided that is invalid for premium domains. */ - public static class AllocationTokenInvalidForPremiumNameException - extends CommandUseErrorException { - public AllocationTokenInvalidForPremiumNameException() { - super("Token not valid for premium name"); - } - } - - public static class AllocationTokenInvalidForCurrencyException extends CommandUseErrorException { - public AllocationTokenInvalidForCurrencyException() { - super("Token and domain currencies do not match."); - } - } } 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 8f31b402e..04bcdff97 100644 --- a/core/src/main/java/google/registry/flows/domain/DomainRenewFlow.java +++ b/core/src/main/java/google/registry/flows/domain/DomainRenewFlow.java @@ -31,7 +31,7 @@ import static google.registry.flows.domain.DomainFlowUtils.validateRegistrationP import static google.registry.flows.domain.DomainFlowUtils.verifyRegistrarIsActive; import static google.registry.flows.domain.DomainFlowUtils.verifyUnitIsYears; import static google.registry.flows.domain.token.AllocationTokenFlowUtils.maybeApplyBulkPricingRemovalToken; -import static google.registry.flows.domain.token.AllocationTokenFlowUtils.verifyTokenAllowedOnDomain; +import static google.registry.flows.domain.token.AllocationTokenFlowUtils.verifyBulkTokenAllowedOnDomain; import static google.registry.model.reporting.HistoryEntry.Type.DOMAIN_RENEW; import static google.registry.persistence.transaction.TransactionManagerFactory.tm; import static google.registry.util.DateTimeUtils.leapSafeAddYears; @@ -124,15 +124,12 @@ import org.joda.time.Duration; * @error {@link RemoveBulkPricingTokenOnNonBulkPricingDomainException} * @error {@link * google.registry.flows.domain.token.AllocationTokenFlowUtils.AllocationTokenNotValidForDomainException} - * @error {@link - * google.registry.flows.domain.token.AllocationTokenFlowUtils.InvalidAllocationTokenException} + * @error {@link AllocationTokenFlowUtils.NonexistentAllocationTokenException} * @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) @@ -154,7 +151,6 @@ public final class DomainRenewFlow implements MutatingFlow { @Inject @Superuser boolean isSuperuser; @Inject DomainHistory.Builder historyBuilder; @Inject EppResponse.Builder responseBuilder; - @Inject AllocationTokenFlowUtils allocationTokenFlowUtils; @Inject DomainRenewFlowCustomLogic flowCustomLogic; @Inject DomainPricingLogic pricingLogic; @Inject DomainRenewFlow() {} @@ -174,22 +170,17 @@ public final class DomainRenewFlow implements MutatingFlow { String tldStr = existingDomain.getTld(); Tld tld = Tld.get(tldStr); Optional allocationToken = - allocationTokenFlowUtils.verifyAllocationTokenIfPresent( - existingDomain, - tld, + AllocationTokenFlowUtils.loadTokenFromExtensionOrGetDefault( registrarId, now, - CommandName.RENEW, - eppInput.getSingleExtension(AllocationTokenExtension.class)); - boolean defaultTokenUsed = false; - if (allocationToken.isEmpty()) { - allocationToken = - DomainFlowUtils.checkForDefaultToken( - tld, existingDomain.getDomainName(), CommandName.RENEW, registrarId, now); - if (allocationToken.isPresent()) { - defaultTokenUsed = true; - } - } + eppInput.getSingleExtension(AllocationTokenExtension.class), + tld, + existingDomain.getDomainName(), + CommandName.RENEW); + boolean defaultTokenUsed = + allocationToken + .map(t -> t.getTokenType().equals(AllocationToken.TokenType.DEFAULT_PROMO)) + .orElse(false); verifyRenewAllowed(authInfo, existingDomain, command, allocationToken); // If client passed an applicable static token this updates the domain @@ -259,7 +250,7 @@ public final class DomainRenewFlow implements MutatingFlow { newDomain, domainHistory, explicitRenewEvent, newAutorenewEvent, newAutorenewPollMessage); if (allocationToken.isPresent() && allocationToken.get().getTokenType().isOneTimeUse()) { entitiesToSave.add( - allocationTokenFlowUtils.redeemToken( + AllocationTokenFlowUtils.redeemToken( allocationToken.get(), domainHistory.getHistoryEntryId())); } EntityChanges entityChanges = @@ -327,7 +318,7 @@ public final class DomainRenewFlow implements MutatingFlow { } verifyUnitIsYears(command.getPeriod()); // We only allow __REMOVE_BULK_PRICING__ token on bulk pricing domains for now - verifyTokenAllowedOnDomain(existingDomain, allocationToken); + verifyBulkTokenAllowedOnDomain(existingDomain, allocationToken); // If the date they specify doesn't match the expiration, fail. (This is an idempotence check). if (!command.getCurrentExpirationDate().equals( existingDomain.getRegistrationExpirationTime().toLocalDate())) { diff --git a/core/src/main/java/google/registry/flows/domain/DomainTransferApproveFlow.java b/core/src/main/java/google/registry/flows/domain/DomainTransferApproveFlow.java index bcae01cf5..da8ef3e87 100644 --- a/core/src/main/java/google/registry/flows/domain/DomainTransferApproveFlow.java +++ b/core/src/main/java/google/registry/flows/domain/DomainTransferApproveFlow.java @@ -54,7 +54,6 @@ import google.registry.model.billing.BillingRecurrence; import google.registry.model.domain.Domain; import google.registry.model.domain.DomainHistory; import google.registry.model.domain.GracePeriod; -import google.registry.model.domain.fee.FeeQueryCommandExtensionItem.CommandName; import google.registry.model.domain.metadata.MetadataExtension; import google.registry.model.domain.rgp.GracePeriodStatus; import google.registry.model.domain.token.AllocationTokenExtension; @@ -94,15 +93,12 @@ import org.joda.time.DateTime; * @error {@link DomainFlowUtils.NotAuthorizedForTldException} * @error {@link * google.registry.flows.domain.token.AllocationTokenFlowUtils.AllocationTokenNotValidForDomainException} - * @error {@link - * google.registry.flows.domain.token.AllocationTokenFlowUtils.InvalidAllocationTokenException} + * @error {@link AllocationTokenFlowUtils.NonexistentAllocationTokenException} * @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_TRANSFER_APPROVE) @@ -116,7 +112,6 @@ public final class DomainTransferApproveFlow implements MutatingFlow { @Inject DomainHistory.Builder historyBuilder; @Inject EppResponse.Builder responseBuilder; @Inject DomainPricingLogic pricingLogic; - @Inject AllocationTokenFlowUtils allocationTokenFlowUtils; @Inject EppInput eppInput; @Inject DomainTransferApproveFlow() {} @@ -132,13 +127,8 @@ public final class DomainTransferApproveFlow implements MutatingFlow { extensionManager.validate(); DateTime now = tm().getTransactionTime(); Domain existingDomain = loadAndVerifyExistence(Domain.class, targetId, now); - allocationTokenFlowUtils.verifyAllocationTokenIfPresent( - existingDomain, - Tld.get(existingDomain.getTld()), - registrarId, - now, - CommandName.TRANSFER, - eppInput.getSingleExtension(AllocationTokenExtension.class)); + AllocationTokenFlowUtils.loadAllocationTokenFromExtension( + registrarId, targetId, now, eppInput.getSingleExtension(AllocationTokenExtension.class)); verifyOptionalAuthInfo(authInfo, existingDomain); verifyHasPendingTransfer(existingDomain); verifyResourceOwnership(registrarId, existingDomain); diff --git a/core/src/main/java/google/registry/flows/domain/DomainTransferRequestFlow.java b/core/src/main/java/google/registry/flows/domain/DomainTransferRequestFlow.java index 89b2e9c37..71b05d99a 100644 --- a/core/src/main/java/google/registry/flows/domain/DomainTransferRequestFlow.java +++ b/core/src/main/java/google/registry/flows/domain/DomainTransferRequestFlow.java @@ -58,7 +58,6 @@ import google.registry.model.domain.Domain; import google.registry.model.domain.DomainCommand.Transfer; import google.registry.model.domain.DomainHistory; import google.registry.model.domain.Period; -import google.registry.model.domain.fee.FeeQueryCommandExtensionItem.CommandName; import google.registry.model.domain.fee.FeeTransferCommandExtension; import google.registry.model.domain.fee.FeeTransformResponseExtension; import google.registry.model.domain.metadata.MetadataExtension; @@ -123,15 +122,12 @@ import org.joda.time.DateTime; * @error {@link DomainFlowUtils.UnsupportedFeeAttributeException} * @error {@link * google.registry.flows.domain.token.AllocationTokenFlowUtils.AllocationTokenNotValidForDomainException} - * @error {@link - * google.registry.flows.domain.token.AllocationTokenFlowUtils.InvalidAllocationTokenException} + * @error {@link AllocationTokenFlowUtils.NonexistentAllocationTokenException} * @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_TRANSFER_REQUEST) @@ -154,7 +150,6 @@ public final class DomainTransferRequestFlow implements MutatingFlow { @Inject AsyncTaskEnqueuer asyncTaskEnqueuer; @Inject EppResponse.Builder responseBuilder; @Inject DomainPricingLogic pricingLogic; - @Inject AllocationTokenFlowUtils allocationTokenFlowUtils; @Inject DomainTransferRequestFlow() {} @@ -170,12 +165,10 @@ public final class DomainTransferRequestFlow implements MutatingFlow { extensionManager.validate(); DateTime now = tm().getTransactionTime(); Domain existingDomain = loadAndVerifyExistence(Domain.class, targetId, now); - allocationTokenFlowUtils.verifyAllocationTokenIfPresent( - existingDomain, - Tld.get(existingDomain.getTld()), + AllocationTokenFlowUtils.loadAllocationTokenFromExtension( gainingClientId, + targetId, now, - CommandName.TRANSFER, eppInput.getSingleExtension(AllocationTokenExtension.class)); Optional superuserExtension = eppInput.getSingleExtension(DomainTransferRequestSuperuserExtension.class); 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 f4f9f27fd..d22d269eb 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 @@ -15,218 +15,97 @@ package google.registry.flows.domain.token; import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkState; +import static com.google.common.collect.ImmutableList.toImmutableList; import static google.registry.persistence.transaction.TransactionManagerFactory.tm; import static google.registry.pricing.PricingEngineProxy.isDomainPremium; +import static google.registry.util.CollectionUtils.isNullOrEmpty; +import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Strings; -import com.google.common.collect.ImmutableMap; -import com.google.common.collect.Maps; +import com.google.common.collect.ImmutableList; import com.google.common.net.InternetDomainName; 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.flows.domain.DomainPricingLogic.AllocationTokenInvalidForPremiumNameException; -import google.registry.model.billing.BillingBase.RenewalPriceBehavior; +import google.registry.model.billing.BillingBase; import google.registry.model.billing.BillingRecurrence; import google.registry.model.domain.Domain; -import google.registry.model.domain.DomainCommand; import google.registry.model.domain.fee.FeeQueryCommandExtensionItem.CommandName; import google.registry.model.domain.token.AllocationToken; import google.registry.model.domain.token.AllocationToken.TokenBehavior; -import google.registry.model.domain.token.AllocationToken.TokenStatus; import google.registry.model.domain.token.AllocationTokenExtension; import google.registry.model.reporting.HistoryEntry.HistoryEntryId; import google.registry.model.tld.Tld; import google.registry.persistence.VKey; -import jakarta.inject.Inject; -import java.util.List; +import java.util.Map; import java.util.Optional; import org.joda.time.DateTime; /** Utility functions for dealing with {@link AllocationToken}s in domain flows. */ public class AllocationTokenFlowUtils { - @Inject - public AllocationTokenFlowUtils() {} - - /** - * Checks if the allocation token applies to the given domain names, used for domain checks. - * - * @return A map of domain names to domain check error response messages. If a message is present - * for a a given domain then it does not validate with this allocation token; domains that do - * validate have blank messages (i.e. no error). - */ - public AllocationTokenDomainCheckResults checkDomainsWithToken( - List domainNames, String token, String registrarId, DateTime now) { - // If the token is completely invalid, return the error message for all domain names - AllocationToken tokenEntity; - try { - tokenEntity = loadToken(token); - } catch (EppException e) { - return new AllocationTokenDomainCheckResults( - Optional.empty(), 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 - ImmutableMap.Builder resultsBuilder = new ImmutableMap.Builder<>(); - for (InternetDomainName domainName : domainNames) { - try { - validateToken( - domainName, - tokenEntity, - CommandName.CREATE, - registrarId, - isDomainPremium(domainName.toString(), now), - now); - resultsBuilder.put(domainName, ""); - } catch (EppException e) { - resultsBuilder.put(domainName, e.getMessage()); - } - } - return new AllocationTokenDomainCheckResults(Optional.of(tokenEntity), resultsBuilder.build()); - } + private AllocationTokenFlowUtils() {} /** Redeems a SINGLE_USE {@link AllocationToken}, returning the redeemed copy. */ - public AllocationToken redeemToken(AllocationToken token, HistoryEntryId redemptionHistoryId) { + public static AllocationToken redeemToken( + AllocationToken token, HistoryEntryId redemptionHistoryId) { checkArgument( token.getTokenType().isOneTimeUse(), "Only SINGLE_USE tokens can be marked as redeemed"); return token.asBuilder().setRedemptionHistoryId(redemptionHistoryId).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, or the token is not valid for a premium name when necessary. - * - * @throws EppException if the token is invalid in any way - */ - public static void validateToken( - InternetDomainName domainName, - AllocationToken token, - CommandName commandName, - String registrarId, - boolean isPremium, - DateTime now) - throws EppException { - - // Only tokens with default behavior require validation - if (!TokenBehavior.DEFAULT.equals(token.getTokenBehavior())) { - return; - } - validateTokenForPossiblePremiumName(Optional.of(token), isPremium); - if (!token.getAllowedEppActions().isEmpty() - && !token.getAllowedEppActions().contains(commandName)) { - throw new AllocationTokenNotValidForCommandException(); - } - if (!token.getAllowedRegistrarIds().isEmpty() - && !token.getAllowedRegistrarIds().contains(registrarId)) { - throw new AllocationTokenNotValidForRegistrarException(); - } - if (!token.getAllowedTlds().isEmpty() - && !token.getAllowedTlds().contains(domainName.parent().toString())) { - throw new AllocationTokenNotValidForTldException(); - } - if (token.getDomainName().isPresent() - && !token.getDomainName().get().equals(domainName.toString())) { - throw new AllocationTokenNotValidForDomainException(); - } - // 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(); - } - } - - /** Validates that the given token is valid for a premium name if the name is premium. */ - public static void validateTokenForPossiblePremiumName( - Optional token, boolean isPremium) - throws AllocationTokenInvalidForPremiumNameException { - if (token.isPresent() - && (token.get().getDiscountFraction() != 0.0 || token.get().getDiscountPrice().isPresent()) + /** Don't apply discounts on premium domains if the token isn't configured that way. */ + public static boolean discountTokenInvalidForPremiumName( + AllocationToken token, boolean isPremium) { + return (token.getDiscountFraction() != 0.0 || token.getDiscountPrice().isPresent()) && isPremium - && !token.get().shouldDiscountPremiums()) { - throw new AllocationTokenInvalidForPremiumNameException(); - } + && !token.shouldDiscountPremiums(); } - /** Loads a given token and validates that it is not redeemed */ - private static AllocationToken loadToken(String token) throws EppException { - if (Strings.isNullOrEmpty(token)) { - // We load the token directly from the input XML. If it's null or empty we should throw - // an InvalidAllocationTokenException before the database load attempt fails. - // See https://tools.ietf.org/html/draft-ietf-regext-allocation-token-04#section-2.1 - throw new InvalidAllocationTokenException(); - } - - Optional maybeTokenEntity = AllocationToken.maybeGetStaticTokenInstance(token); - if (maybeTokenEntity.isPresent()) { - return maybeTokenEntity.get(); - } - - // TODO(b/368069206): `reTransact` needed by tests only. - maybeTokenEntity = - tm().reTransact(() -> tm().loadByKeyIfPresent(VKey.create(AllocationToken.class, token))); - - if (maybeTokenEntity.isEmpty()) { - throw new InvalidAllocationTokenException(); - } - if (maybeTokenEntity.get().isRedeemed()) { - throw new AlreadyRedeemedAllocationTokenException(); - } - return maybeTokenEntity.get(); - } - - /** Verifies and returns the allocation token if one is specified, otherwise does nothing. */ - public Optional verifyAllocationTokenCreateIfPresent( - DomainCommand.Create command, - Tld tld, + /** Loads and verifies the allocation token if one is specified, otherwise does nothing. */ + public static Optional loadAllocationTokenFromExtension( String registrarId, + String domainName, DateTime now, Optional extension) - throws EppException { + throws NonexistentAllocationTokenException, AllocationTokenInvalidException { if (extension.isEmpty()) { return Optional.empty(); } - AllocationToken tokenEntity = loadToken(extension.get().getAllocationToken()); - validateToken( - InternetDomainName.from(command.getDomainName()), - tokenEntity, - CommandName.CREATE, - registrarId, - isDomainPremium(command.getDomainName(), now), - now); - return Optional.of(tokenEntity); + return Optional.of( + loadAndValidateToken(extension.get().getAllocationToken(), registrarId, domainName, now)); } - /** Verifies and returns the allocation token if one is specified, otherwise does nothing. */ - public Optional verifyAllocationTokenIfPresent( - Domain existingDomain, - Tld tld, + /** + * Loads the relevant token, if present, for the given extension + request. + * + *

This may be the allocation token provided in the request, if it is present and valid for the + * request. Otherwise, it may be a default allocation token if one is present and valid for the + * request. + */ + public static Optional loadTokenFromExtensionOrGetDefault( String registrarId, DateTime now, - CommandName commandName, - Optional extension) - throws EppException { - if (extension.isEmpty()) { - return Optional.empty(); + Optional extension, + Tld tld, + String domainName, + CommandName commandName) + throws NonexistentAllocationTokenException, AllocationTokenInvalidException { + Optional fromExtension = + loadAllocationTokenFromExtension(registrarId, domainName, now, extension); + if (fromExtension.isPresent() + && tokenIsValidAgainstDomain( + InternetDomainName.from(domainName), fromExtension.get(), commandName, now)) { + return fromExtension; } - AllocationToken tokenEntity = loadToken(extension.get().getAllocationToken()); - validateToken( - InternetDomainName.from(existingDomain.getDomainName()), - tokenEntity, - commandName, - registrarId, - isDomainPremium(existingDomain.getDomainName(), now), - now); - return Optional.of(tokenEntity); + return checkForDefaultToken(tld, domainName, commandName, registrarId, now); } - public static void verifyTokenAllowedOnDomain( + /** Verifies that the given domain can have a bulk pricing token removed from it. */ + public static void verifyBulkTokenAllowedOnDomain( Domain domain, Optional allocationToken) throws EppException { - boolean domainHasBulkToken = domain.getCurrentBulkToken().isPresent(); boolean hasRemoveBulkPricingToken = allocationToken.isPresent() @@ -239,6 +118,11 @@ public class AllocationTokenFlowUtils { } } + /** + * Removes the bulk pricing token from the provided domain, if applicable. + * + * @param allocationToken the (possibly) REMOVE_BULK_PRICING token provided by the client. + */ public static Domain maybeApplyBulkPricingRemovalToken( Domain domain, Optional allocationToken) { if (allocationToken.isEmpty() @@ -249,7 +133,7 @@ public class AllocationTokenFlowUtils { BillingRecurrence newBillingRecurrence = tm().loadByKey(domain.getAutorenewBillingEvent()) .asBuilder() - .setRenewalPriceBehavior(RenewalPriceBehavior.DEFAULT) + .setRenewalPriceBehavior(BillingBase.RenewalPriceBehavior.DEFAULT) .setRenewalPrice(null) .build(); @@ -267,35 +151,139 @@ public class AllocationTokenFlowUtils { .build(); } + /** + * Checks if the given token is valid for the given request. + * + *

Note that if the token is not valid, that is not a catastrophic error -- we may move on to + * trying a different token or skip token usage entirely. + */ + @VisibleForTesting + static boolean tokenIsValidAgainstDomain( + InternetDomainName domainName, AllocationToken token, CommandName commandName, DateTime now) { + if (discountTokenInvalidForPremiumName(token, isDomainPremium(domainName.toString(), now))) { + return false; + } + if (!token.getAllowedEppActions().isEmpty() + && !token.getAllowedEppActions().contains(commandName)) { + return false; + } + if (!token.getAllowedTlds().isEmpty() + && !token.getAllowedTlds().contains(domainName.parent().toString())) { + return false; + } + return token.getDomainName().isEmpty() + || token.getDomainName().get().equals(domainName.toString()); + } + + /** + * Checks if there is a valid default token to be used for a domain create command. + * + *

If there is more than one valid default token for the registration, only the first valid + * token found on the TLD's default token list will be returned. + */ + private static Optional checkForDefaultToken( + Tld tld, String domainName, CommandName commandName, String registrarId, DateTime now) { + ImmutableList> tokensFromTld = tld.getDefaultPromoTokens(); + if (isNullOrEmpty(tokensFromTld)) { + return Optional.empty(); + } + Map, Optional> tokens = + AllocationToken.getAll(tokensFromTld); + checkState( + !isNullOrEmpty(tokens), "Failure while loading default TLD tokens from the database"); + // Iterate over the list to maintain token ordering (since we return the first valid token) + ImmutableList tokenList = + tokensFromTld.stream() + .map(tokens::get) + .filter(Optional::isPresent) + .map(Optional::get) + .collect(toImmutableList()); + + // Check if any of the tokens are valid for this domain registration + for (AllocationToken token : tokenList) { + try { + validateTokenEntity(token, registrarId, domainName, now); + } catch (AllocationTokenInvalidException e) { + // Token is not valid for this registrar, etc. -- continue trying tokens + continue; + } + if (tokenIsValidAgainstDomain(InternetDomainName.from(domainName), token, commandName, now)) { + return Optional.of(token); + } + } + // No valid default token found + return Optional.empty(); + } + + /** Loads a given token and validates it against the registrar, time, etc */ + private static AllocationToken loadAndValidateToken( + String token, String registrarId, String domainName, DateTime now) + throws NonexistentAllocationTokenException, AllocationTokenInvalidException { + if (Strings.isNullOrEmpty(token)) { + // We load the token directly from the input XML. If it's null or empty we should throw + // an NonexistentAllocationTokenException before the database load attempt fails. + // See https://tools.ietf.org/html/draft-ietf-regext-allocation-token-04#section-2.1 + throw new NonexistentAllocationTokenException(); + } + + Optional maybeTokenEntity = AllocationToken.maybeGetStaticTokenInstance(token); + if (maybeTokenEntity.isPresent()) { + return maybeTokenEntity.get(); + } + + maybeTokenEntity = AllocationToken.get(VKey.create(AllocationToken.class, token)); + if (maybeTokenEntity.isEmpty()) { + throw new NonexistentAllocationTokenException(); + } + AllocationToken tokenEntity = maybeTokenEntity.get(); + validateTokenEntity(tokenEntity, registrarId, domainName, now); + return tokenEntity; + } + + private static void validateTokenEntity( + AllocationToken token, String registrarId, String domainName, DateTime now) + throws AllocationTokenInvalidException { + if (token.isRedeemed()) { + throw new AlreadyRedeemedAllocationTokenException(); + } + if (!token.getAllowedRegistrarIds().isEmpty() + && !token.getAllowedRegistrarIds().contains(registrarId)) { + throw new AllocationTokenNotValidForRegistrarException(); + } + // 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 + && !AllocationToken.TokenStatus.VALID.equals( + token.getTokenStatusTransitions().getValueAtTime(now))) { + throw new AllocationTokenNotInPromotionException(); + } + + if (token.getDomainName().isPresent() && !token.getDomainName().get().equals(domainName)) { + throw new AllocationTokenNotValidForDomainException(); + } + } + // Note: exception messages should be <= 32 characters long for domain check results + /** The allocation token exists but is not valid, e.g. the wrong registrar. */ + public abstract static class AllocationTokenInvalidException + extends StatusProhibitsOperationException { + AllocationTokenInvalidException(String message) { + super(message); + } + } + /** The allocation token is not currently valid. */ public static class AllocationTokenNotInPromotionException - extends StatusProhibitsOperationException { + extends AllocationTokenInvalidException { AllocationTokenNotInPromotionException() { super("Alloc token not in promo period"); } } - /** The allocation token is not valid for this TLD. */ - public static class AllocationTokenNotValidForTldException - extends AssociationProhibitsOperationException { - AllocationTokenNotValidForTldException() { - super("Alloc token invalid for TLD"); - } - } - - /** The allocation token is not valid for this domain. */ - public static class AllocationTokenNotValidForDomainException - extends AssociationProhibitsOperationException { - AllocationTokenNotValidForDomainException() { - super("Alloc token invalid for domain"); - } - } - /** The allocation token is not valid for this registrar. */ public static class AllocationTokenNotValidForRegistrarException - extends AssociationProhibitsOperationException { + extends AllocationTokenInvalidException { AllocationTokenNotValidForRegistrarException() { super("Alloc token invalid for client"); } @@ -303,23 +291,23 @@ public class AllocationTokenFlowUtils { /** The allocation token was already redeemed. */ public static class AlreadyRedeemedAllocationTokenException - extends AssociationProhibitsOperationException { + extends AllocationTokenInvalidException { AlreadyRedeemedAllocationTokenException() { super("Alloc token was already redeemed"); } } - /** The allocation token is not valid for this EPP command. */ - public static class AllocationTokenNotValidForCommandException - extends AssociationProhibitsOperationException { - AllocationTokenNotValidForCommandException() { - super("Allocation token not valid for the EPP command"); + /** The allocation token is not valid for this domain. */ + public static class AllocationTokenNotValidForDomainException + extends AllocationTokenInvalidException { + AllocationTokenNotValidForDomainException() { + super("Alloc token invalid for domain"); + } } - } /** The allocation token is invalid. */ - public static class InvalidAllocationTokenException extends AuthorizationErrorException { - InvalidAllocationTokenException() { + public static class NonexistentAllocationTokenException extends AuthorizationErrorException { + NonexistentAllocationTokenException() { super("The allocation token is invalid"); } } diff --git a/core/src/test/java/google/registry/flows/domain/DomainCheckFlowTest.java b/core/src/test/java/google/registry/flows/domain/DomainCheckFlowTest.java index 6b626ce4c..c9a302926 100644 --- a/core/src/test/java/google/registry/flows/domain/DomainCheckFlowTest.java +++ b/core/src/test/java/google/registry/flows/domain/DomainCheckFlowTest.java @@ -211,8 +211,8 @@ class DomainCheckFlowTest extends ResourceCheckFlowTestCasenaturalOrder() - .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 ResourceCheckFlowTestCasenaturalOrder() + .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 ResourceCheckFlowTestCasenaturalOrder() - .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 ResourceFlowTestCasenaturalOrder() - .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 - 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 { 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 d82efbdd7..f2fdedd44 100644 --- a/core/src/test/java/google/registry/flows/domain/DomainRenewFlowTest.java +++ b/core/src/test/java/google/registry/flows/domain/DomainRenewFlowTest.java @@ -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 ImmutableMap 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 "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 .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 .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"); } diff --git a/core/src/test/java/google/registry/flows/domain/DomainTransferApproveFlowTest.java b/core/src/test/java/google/registry/flows/domain/DomainTransferApproveFlowTest.java index 86d3b9d5d..21d2a8fb2 100644 --- a/core/src/test/java/google/registry/flows/domain/DomainTransferApproveFlowTest.java +++ b/core/src/test/java/google/registry/flows/domain/DomainTransferApproveFlowTest.java @@ -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 diff --git a/core/src/test/java/google/registry/flows/domain/DomainTransferRequestFlowTest.java b/core/src/test/java/google/registry/flows/domain/DomainTransferRequestFlowTest.java index 3a75233c5..d649d555d 100644 --- a/core/src/test/java/google/registry/flows/domain/DomainTransferRequestFlowTest.java +++ b/core/src/test/java/google/registry/flows/domain/DomainTransferRequestFlowTest.java @@ -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.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"); 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 f7a95026a..7e59884ff 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 @@ -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.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.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 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.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 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 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) { diff --git a/core/src/test/resources/google/registry/flows/domain/domain_check_allocationtoken_fee.xml b/core/src/test/resources/google/registry/flows/domain/domain_check_allocationtoken_fee.xml index 1b432f0ec..d0573bf7d 100644 --- a/core/src/test/resources/google/registry/flows/domain/domain_check_allocationtoken_fee.xml +++ b/core/src/test/resources/google/registry/flows/domain/domain_check_allocationtoken_fee.xml @@ -6,6 +6,7 @@ example1.tld example2.example reserved.tld + rich.example @@ -34,6 +35,12 @@ create 1 + + rich.example + USD + create + 1 + example1.tld USD @@ -52,6 +59,12 @@ renew 1 + + rich.example + USD + renew + 1 + ABC-12345 diff --git a/core/src/test/resources/google/registry/flows/domain/domain_check_allocationtoken_fee_anchor_response.xml b/core/src/test/resources/google/registry/flows/domain/domain_check_allocationtoken_fee_anchor_response.xml index b0cb4a7dd..f9c3ba032 100644 --- a/core/src/test/resources/google/registry/flows/domain/domain_check_allocationtoken_fee_anchor_response.xml +++ b/core/src/test/resources/google/registry/flows/domain/domain_check_allocationtoken_fee_anchor_response.xml @@ -17,6 +17,10 @@ reserved.tld Alloc token invalid for domain + + rich.example + Alloc token invalid for domain + @@ -42,6 +46,13 @@ 1 token-not-supported + + rich.example + USD + create + 1 + token-not-supported + example1.tld USD @@ -64,6 +75,13 @@ 1 token-not-supported + + rich.example + USD + renew + 1 + token-not-supported + diff --git a/core/src/test/resources/google/registry/flows/domain/domain_check_allocationtoken_fee_response.xml b/core/src/test/resources/google/registry/flows/domain/domain_check_allocationtoken_fee_response.xml index 4ef967f0e..2f64bbf47 100644 --- a/core/src/test/resources/google/registry/flows/domain/domain_check_allocationtoken_fee_response.xml +++ b/core/src/test/resources/google/registry/flows/domain/domain_check_allocationtoken_fee_response.xml @@ -16,6 +16,9 @@ reserved.tld Reserved + + rich.example + @@ -41,26 +44,42 @@ 1 reserved + + rich.example + USD + create + 1 + 100.00 + premium + example1.tld USD renew 1 - token-not-supported + 11.00 example2.example USD renew 1 - token-not-supported + 11.00 reserved.tld USD renew 1 - token-not-supported + 11.00 + + + rich.example + USD + renew + 1 + 100.00 + premium diff --git a/core/src/test/resources/google/registry/flows/domain/domain_check_allocationtoken_fee_specificuse_response.xml b/core/src/test/resources/google/registry/flows/domain/domain_check_allocationtoken_fee_specificuse_response.xml index e7afe5292..f3530e6a8 100644 --- a/core/src/test/resources/google/registry/flows/domain/domain_check_allocationtoken_fee_specificuse_response.xml +++ b/core/src/test/resources/google/registry/flows/domain/domain_check_allocationtoken_fee_specificuse_response.xml @@ -16,7 +16,7 @@ reserved.tld - Reserved + Alloc token invalid for domain specificuse.tld diff --git a/core/src/test/resources/google/registry/flows/domain/domain_check_fee_multiple_commands_allocationtoken_response_v06.xml b/core/src/test/resources/google/registry/flows/domain/domain_check_fee_multiple_commands_allocationtoken_response_v06.xml index 83b067ea3..6de31723a 100644 --- a/core/src/test/resources/google/registry/flows/domain/domain_check_fee_multiple_commands_allocationtoken_response_v06.xml +++ b/core/src/test/resources/google/registry/flows/domain/domain_check_fee_multiple_commands_allocationtoken_response_v06.xml @@ -17,14 +17,14 @@ USD create 1 - 13.00 + 11.70 example1.tld USD renew 1 - token-not-supported + 11.00 example1.tld @@ -38,14 +38,14 @@ USD restore 1 - token-not-supported + 17.00 example1.tld USD update 1 - token-not-supported + 0.00 diff --git a/core/src/test/resources/google/registry/flows/domain/domain_check_fee_multiple_commands_allocationtoken_response_v12.xml b/core/src/test/resources/google/registry/flows/domain/domain_check_fee_multiple_commands_allocationtoken_response_v12.xml index 35884fc4a..85fe95197 100644 --- a/core/src/test/resources/google/registry/flows/domain/domain_check_fee_multiple_commands_allocationtoken_response_v12.xml +++ b/core/src/test/resources/google/registry/flows/domain/domain_check_fee_multiple_commands_allocationtoken_response_v12.xml @@ -19,7 +19,7 @@ 1 - 13.00 + 11.70 @@ -28,7 +28,7 @@ 1 - token-not-supported + 11.00 @@ -46,7 +46,7 @@ 1 - token-not-supported + 17.00 @@ -55,7 +55,7 @@ 1 - token-not-supported + 0.00