Add REGISTER_BSA allocation type (#2319)

* Add ALLOW_BSA allocation type

Add a new type to allow creation of domains blocked by BSA.
Except for the BSA semantics, the new type behaves exactly
like SINGLE_USE.

* Addressing reviews

* Addressing review
This commit is contained in:
Weimin Yu 2024-02-08 16:45:13 -05:00 committed by GitHub
parent 469d62703a
commit 7b47ecb1f1
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 239 additions and 33 deletions

View file

@ -26,6 +26,7 @@ import static google.registry.flows.domain.DomainFlowUtils.checkHasBillingAccoun
import static google.registry.flows.domain.DomainFlowUtils.getReservationTypes; import static google.registry.flows.domain.DomainFlowUtils.getReservationTypes;
import static google.registry.flows.domain.DomainFlowUtils.handleFeeRequest; import static google.registry.flows.domain.DomainFlowUtils.handleFeeRequest;
import static google.registry.flows.domain.DomainFlowUtils.isAnchorTenant; import static google.registry.flows.domain.DomainFlowUtils.isAnchorTenant;
import static google.registry.flows.domain.DomainFlowUtils.isRegisterBsaCreate;
import static google.registry.flows.domain.DomainFlowUtils.isReserved; import static google.registry.flows.domain.DomainFlowUtils.isReserved;
import static google.registry.flows.domain.DomainFlowUtils.isValidReservedCreate; import static google.registry.flows.domain.DomainFlowUtils.isValidReservedCreate;
import static google.registry.flows.domain.DomainFlowUtils.validateDomainName; import static google.registry.flows.domain.DomainFlowUtils.validateDomainName;
@ -269,13 +270,13 @@ public final class DomainCheckFlow implements TransactionalFlow {
if (tokenResult.isPresent()) { if (tokenResult.isPresent()) {
return tokenResult; return tokenResult;
} }
if (bsaBlockedDomains.contains(domainName)) { if (isRegisterBsaCreate(domainName, allocationToken)
// TODO(weiminyu): extract to a constant for here and CheckApiAction. || !bsaBlockedDomains.contains(domainName)) {
// Excerpt from BSA's custom message. Max len 32 chars by EPP XML schema.
return Optional.of("Blocked by a GlobalBlock service");
} else {
return Optional.empty(); return Optional.empty();
} }
// TODO(weiminyu): extract to a constant for here and CheckApiAction.
// Excerpt from BSA's custom message. Max len 32 chars by EPP XML schema.
return Optional.of("Blocked by a GlobalBlock service");
} }
/** Handle the fee check extension. */ /** Handle the fee check extension. */

View file

@ -330,7 +330,7 @@ public final class DomainCreateFlow implements MutatingFlow {
.verifySignedMarks(launchCreate.get().getSignedMarks(), domainLabel, now) .verifySignedMarks(launchCreate.get().getSignedMarks(), domainLabel, now)
.getId(); .getId();
} }
verifyNotBlockedByBsa(domainLabel, tld, now); verifyNotBlockedByBsa(domainName, tld, now, allocationToken);
flowCustomLogic.afterValidation( flowCustomLogic.afterValidation(
DomainCreateFlowCustomLogic.AfterValidationParameters.newBuilder() DomainCreateFlowCustomLogic.AfterValidationParameters.newBuilder()
.setDomainName(domainName) .setDomainName(domainName)
@ -421,8 +421,7 @@ public final class DomainCreateFlow implements MutatingFlow {
createNameCollisionOneTimePollMessage(targetId, domainHistory, registrarId, now)); createNameCollisionOneTimePollMessage(targetId, domainHistory, registrarId, now));
} }
entitiesToSave.add(domain, domainHistory); entitiesToSave.add(domain, domainHistory);
if (allocationToken.isPresent() if (allocationToken.isPresent() && allocationToken.get().getTokenType().isOneTimeUse()) {
&& TokenType.SINGLE_USE.equals(allocationToken.get().getTokenType())) {
entitiesToSave.add( entitiesToSave.add(
allocationTokenFlowUtils.redeemToken( allocationTokenFlowUtils.redeemToken(
allocationToken.get(), domainHistory.getHistoryEntryId())); allocationToken.get(), domainHistory.getHistoryEntryId()));

View file

@ -27,6 +27,7 @@ import static com.google.common.collect.Sets.intersection;
import static com.google.common.collect.Sets.union; import static com.google.common.collect.Sets.union;
import static google.registry.bsa.persistence.BsaLabelUtils.isLabelBlocked; import static google.registry.bsa.persistence.BsaLabelUtils.isLabelBlocked;
import static google.registry.model.domain.Domain.MAX_REGISTRATION_YEARS; import static google.registry.model.domain.Domain.MAX_REGISTRATION_YEARS;
import static google.registry.model.domain.token.AllocationToken.TokenType.REGISTER_BSA;
import static google.registry.model.tld.Tld.TldState.GENERAL_AVAILABILITY; import static google.registry.model.tld.Tld.TldState.GENERAL_AVAILABILITY;
import static google.registry.model.tld.Tld.TldState.PREDELEGATION; import static google.registry.model.tld.Tld.TldState.PREDELEGATION;
import static google.registry.model.tld.Tld.TldState.QUIET_PERIOD; import static google.registry.model.tld.Tld.TldState.QUIET_PERIOD;
@ -265,9 +266,14 @@ public class DomainFlowUtils {
* Verifies that the {@code domainLabel} is not blocked by any BSA block label for the given * Verifies that the {@code domainLabel} is not blocked by any BSA block label for the given
* {@code tld} at the specified time. * {@code tld} at the specified time.
*/ */
public static void verifyNotBlockedByBsa(String domainLabel, Tld tld, DateTime now) public static void verifyNotBlockedByBsa(
InternetDomainName domainName,
Tld tld,
DateTime now,
Optional<AllocationToken> allocationToken)
throws DomainLabelBlockedByBsaException { throws DomainLabelBlockedByBsaException {
if (isBlockedByBsa(domainLabel, tld, now)) { if (!isRegisterBsaCreate(domainName, allocationToken)
&& isBlockedByBsa(domainName.parts().get(0), tld, now)) {
throw new DomainLabelBlockedByBsaException(); throw new DomainLabelBlockedByBsaException();
} }
} }
@ -311,6 +317,15 @@ public class DomainFlowUtils {
&& token.get().getDomainName().get().equals(domainName.toString()); && token.get().getDomainName().get().equals(domainName.toString());
} }
/** Returns whether a given domain create request may bypass the BSA block check. */
public static boolean isRegisterBsaCreate(
InternetDomainName domainName, Optional<AllocationToken> token) {
return token.isPresent()
&& token.get().getTokenType().equals(REGISTER_BSA)
&& token.get().getDomainName().isPresent()
&& token.get().getDomainName().get().equals(domainName.toString());
}
/** Check if the registrar running the flow has access to the TLD in question. */ /** Check if the registrar running the flow has access to the TLD in question. */
public static void checkAllowedAccessToTld(String registrarId, String tld) throws EppException { public static void checkAllowedAccessToTld(String registrarId, String tld) throws EppException {
if (!Registrar.loadByRegistrarIdCached(registrarId).get().getAllowedTlds().contains(tld)) { if (!Registrar.loadByRegistrarIdCached(registrarId).get().getAllowedTlds().contains(tld)) {

View file

@ -73,7 +73,6 @@ import google.registry.model.domain.fee.FeeTransformResponseExtension;
import google.registry.model.domain.metadata.MetadataExtension; import google.registry.model.domain.metadata.MetadataExtension;
import google.registry.model.domain.rgp.GracePeriodStatus; import google.registry.model.domain.rgp.GracePeriodStatus;
import google.registry.model.domain.token.AllocationToken; import google.registry.model.domain.token.AllocationToken;
import google.registry.model.domain.token.AllocationToken.TokenType;
import google.registry.model.domain.token.AllocationTokenExtension; import google.registry.model.domain.token.AllocationTokenExtension;
import google.registry.model.eppcommon.AuthInfo; import google.registry.model.eppcommon.AuthInfo;
import google.registry.model.eppcommon.StatusValue; import google.registry.model.eppcommon.StatusValue;
@ -258,8 +257,7 @@ public final class DomainRenewFlow implements MutatingFlow {
ImmutableSet.Builder<ImmutableObject> entitiesToSave = new ImmutableSet.Builder<>(); ImmutableSet.Builder<ImmutableObject> entitiesToSave = new ImmutableSet.Builder<>();
entitiesToSave.add( entitiesToSave.add(
newDomain, domainHistory, explicitRenewEvent, newAutorenewEvent, newAutorenewPollMessage); newDomain, domainHistory, explicitRenewEvent, newAutorenewEvent, newAutorenewPollMessage);
if (allocationToken.isPresent() if (allocationToken.isPresent() && allocationToken.get().getTokenType().isOneTimeUse()) {
&& TokenType.SINGLE_USE.equals(allocationToken.get().getTokenType())) {
entitiesToSave.add( entitiesToSave.add(
allocationTokenFlowUtils.redeemToken( allocationTokenFlowUtils.redeemToken(
allocationToken.get(), domainHistory.getHistoryEntryId())); allocationToken.get(), domainHistory.getHistoryEntryId()));

View file

@ -36,7 +36,6 @@ import google.registry.model.domain.fee.FeeQueryCommandExtensionItem.CommandName
import google.registry.model.domain.token.AllocationToken; import google.registry.model.domain.token.AllocationToken;
import google.registry.model.domain.token.AllocationToken.TokenBehavior; import google.registry.model.domain.token.AllocationToken.TokenBehavior;
import google.registry.model.domain.token.AllocationToken.TokenStatus; import google.registry.model.domain.token.AllocationToken.TokenStatus;
import google.registry.model.domain.token.AllocationToken.TokenType;
import google.registry.model.domain.token.AllocationTokenExtension; import google.registry.model.domain.token.AllocationTokenExtension;
import google.registry.model.reporting.HistoryEntry.HistoryEntryId; import google.registry.model.reporting.HistoryEntry.HistoryEntryId;
import google.registry.model.tld.Tld; import google.registry.model.tld.Tld;
@ -105,8 +104,7 @@ public class AllocationTokenFlowUtils {
/** Redeems a SINGLE_USE {@link AllocationToken}, returning the redeemed copy. */ /** Redeems a SINGLE_USE {@link AllocationToken}, returning the redeemed copy. */
public AllocationToken redeemToken(AllocationToken token, HistoryEntryId redemptionHistoryId) { public AllocationToken redeemToken(AllocationToken token, HistoryEntryId redemptionHistoryId) {
checkArgument( checkArgument(
TokenType.SINGLE_USE.equals(token.getTokenType()), token.getTokenType().isOneTimeUse(), "Only SINGLE_USE tokens can be marked as redeemed");
"Only SINGLE_USE tokens can be marked as redeemed");
return token.asBuilder().setRedemptionHistoryId(redemptionHistoryId).build(); return token.asBuilder().setRedemptionHistoryId(redemptionHistoryId).build();
} }

View file

@ -22,6 +22,7 @@ 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.ENDED;
import static google.registry.model.domain.token.AllocationToken.TokenStatus.NOT_STARTED; 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.TokenStatus.VALID;
import static google.registry.model.domain.token.AllocationToken.TokenType.REGISTER_BSA;
import static google.registry.persistence.transaction.TransactionManagerFactory.tm; import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
import static google.registry.util.CollectionUtils.forceEmptyToNull; import static google.registry.util.CollectionUtils.forceEmptyToNull;
import static google.registry.util.CollectionUtils.nullToEmptyImmutableCopy; import static google.registry.util.CollectionUtils.nullToEmptyImmutableCopy;
@ -120,18 +121,37 @@ public class AllocationToken extends UpdateAutoTimestampEntity implements Builda
/** Type of the token that indicates how and where it should be used. */ /** Type of the token that indicates how and where it should be used. */
public enum TokenType { public enum TokenType {
/** Token used for bulk pricing */ /** Token used for bulk pricing */
BULK_PRICING, BULK_PRICING(/* isOneTimeUse= */ false),
/** Token saved on a TLD to use if no other token is passed from the client */ /** Token saved on a TLD to use if no other token is passed from the client */
DEFAULT_PROMO, DEFAULT_PROMO(/* isOneTimeUse= */ false),
/** This is the old name for what is now BULK_PRICING. */ /** This is the old name for what is now BULK_PRICING. */
// TODO(sarahbot@): Remove this type once all tokens of this type have been scrubbed from the // TODO(sarahbot@): Remove this type once all tokens of this type have been scrubbed from the
// database // database
@Deprecated @Deprecated
PACKAGE, PACKAGE(/* isOneTimeUse= */ false),
/** Invalid after use */ /** Invalid after use */
SINGLE_USE, SINGLE_USE(/* isOneTimeUse= */ true),
/** Do not expire after use */ /** Do not expire after use */
UNLIMITED_USE, UNLIMITED_USE(/* isOneTimeUse= */ false),
/**
* Allows bypassing the BSA check during domain creation, otherwise has the same semantics as
* {@link #SINGLE_USE}.
*
* <p>This token applies to a single domain only. If the domain is not blocked by BSA at the
* redemption time this token is processed like {@code SINGLE_USE}, as mentioned above.
*/
REGISTER_BSA(/* isOneTimeUse= */ true);
private final boolean isOneTimeUse;
private TokenType(boolean isOneTimeUse) {
this.isOneTimeUse = isOneTimeUse;
}
/** Returns true if token should be invalidated after use. */
public boolean isOneTimeUse() {
return this.isOneTimeUse;
}
} }
/** /**
@ -361,12 +381,11 @@ public class AllocationToken extends UpdateAutoTimestampEntity implements Builda
|| !getInstance().discountPremiums, || !getInstance().discountPremiums,
"Bulk tokens cannot discount premium names"); "Bulk tokens cannot discount premium names");
checkArgument( checkArgument(
getInstance().domainName == null || TokenType.SINGLE_USE.equals(getInstance().tokenType), getInstance().domainName == null || getInstance().tokenType.isOneTimeUse(),
"Domain name can only be specified for SINGLE_USE tokens"); "Domain name can only be specified for SINGLE_USE or REGISTER_BSA tokens");
checkArgument( checkArgument(
getInstance().redemptionHistoryId == null getInstance().redemptionHistoryId == null || getInstance().tokenType.isOneTimeUse(),
|| TokenType.SINGLE_USE.equals(getInstance().tokenType), "Redemption history entry can only be specified for SINGLE_USE or REGISTER_BSA tokens");
"Redemption history entry can only be specified for SINGLE_USE tokens");
checkArgument( checkArgument(
getInstance().tokenType != TokenType.BULK_PRICING getInstance().tokenType != TokenType.BULK_PRICING
|| (getInstance().allowedClientIds != null || (getInstance().allowedClientIds != null
@ -378,6 +397,10 @@ public class AllocationToken extends UpdateAutoTimestampEntity implements Builda
checkArgument( checkArgument(
getInstance().discountFraction > 0 || getInstance().discountYears == 1, getInstance().discountFraction > 0 || getInstance().discountYears == 1,
"Discount years can only be specified along with a discount fraction"); "Discount years can only be specified along with a discount fraction");
if (getInstance().getTokenType().equals(REGISTER_BSA)) {
checkArgumentNotNull(
getInstance().domainName, "REGISTER_BSA tokens must be tied to a domain");
}
if (getInstance().registrationBehavior.equals(RegistrationBehavior.ANCHOR_TENANT)) { if (getInstance().registrationBehavior.equals(RegistrationBehavior.ANCHOR_TENANT)) {
checkArgumentNotNull( checkArgumentNotNull(
getInstance().domainName, "ANCHOR_TENANT tokens must be tied to a domain"); getInstance().domainName, "ANCHOR_TENANT tokens must be tied to a domain");

View file

@ -18,7 +18,6 @@ import static com.google.common.collect.ImmutableList.toImmutableList;
import static com.google.common.collect.ImmutableSet.toImmutableSet; import static com.google.common.collect.ImmutableSet.toImmutableSet;
import static com.google.common.collect.Iterables.partition; import static com.google.common.collect.Iterables.partition;
import static com.google.common.collect.Streams.stream; import static com.google.common.collect.Streams.stream;
import static google.registry.model.domain.token.AllocationToken.TokenType.SINGLE_USE;
import static google.registry.persistence.transaction.TransactionManagerFactory.tm; import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
import com.beust.jcommander.Parameter; import com.beust.jcommander.Parameter;
@ -78,7 +77,7 @@ final class DeleteAllocationTokensCommand extends UpdateOrDeleteAllocationTokens
ImmutableSet<VKey<AllocationToken>> tokensToDelete = ImmutableSet<VKey<AllocationToken>> tokensToDelete =
tm().loadByKeys(batch).values().stream() tm().loadByKeys(batch).values().stream()
.filter(t -> withDomains || !t.getDomainName().isPresent()) .filter(t -> withDomains || !t.getDomainName().isPresent())
.filter(t -> SINGLE_USE.equals(t.getTokenType())) .filter(t -> t.getTokenType().isOneTimeUse())
.filter(t -> !t.isRedeemed()) .filter(t -> !t.isRedeemed())
.map(AllocationToken::createVKey) .map(AllocationToken::createVKey)
.collect(toImmutableSet()); .collect(toImmutableSet());

View file

@ -19,6 +19,7 @@ import static google.registry.model.billing.BillingBase.RenewalPriceBehavior.DEF
import static google.registry.model.billing.BillingBase.RenewalPriceBehavior.NONPREMIUM; import static google.registry.model.billing.BillingBase.RenewalPriceBehavior.NONPREMIUM;
import static google.registry.model.billing.BillingBase.RenewalPriceBehavior.SPECIFIED; import static google.registry.model.billing.BillingBase.RenewalPriceBehavior.SPECIFIED;
import static google.registry.model.domain.token.AllocationToken.TokenType.DEFAULT_PROMO; import static google.registry.model.domain.token.AllocationToken.TokenType.DEFAULT_PROMO;
import static google.registry.model.domain.token.AllocationToken.TokenType.REGISTER_BSA;
import static google.registry.model.domain.token.AllocationToken.TokenType.SINGLE_USE; import static google.registry.model.domain.token.AllocationToken.TokenType.SINGLE_USE;
import static google.registry.model.domain.token.AllocationToken.TokenType.UNLIMITED_USE; import static google.registry.model.domain.token.AllocationToken.TokenType.UNLIMITED_USE;
import static google.registry.model.eppoutput.CheckData.DomainCheck.create; import static google.registry.model.eppoutput.CheckData.DomainCheck.create;
@ -189,6 +190,40 @@ class DomainCheckFlowTest extends ResourceCheckFlowTestCase<DomainCheckFlow, Dom
create(true, "example3.tld", null)); create(true, "example3.tld", null));
} }
@Test
void testSuccess_bsaBlocked_createAllowedWithToken() throws Exception {
persistBsaLabel("example1");
setEppInput("domain_check_allocationtoken.xml");
persistResource(
new AllocationToken.Builder()
.setToken("abc123")
.setTokenType(REGISTER_BSA)
.setDomainName("example1.tld")
.build());
doCheckTest(
create(true, "example1.tld", null),
create(false, "example2.tld", "Alloc token invalid for domain"),
create(false, "reserved.tld", "Reserved"),
create(false, "specificuse.tld", "Reserved; alloc. token required"));
}
@Test
void testSuccess_bsaBlocked_withIrrelevantTokenType() throws Exception {
persistBsaLabel("example1");
setEppInput("domain_check_allocationtoken.xml");
persistResource(
new AllocationToken.Builder()
.setToken("abc123")
.setTokenType(SINGLE_USE)
.setDomainName("example1.tld")
.build());
doCheckTest(
create(false, "example1.tld", "Blocked by a GlobalBlock service"),
create(false, "example2.tld", "Alloc token invalid for domain"),
create(false, "reserved.tld", "Reserved"),
create(false, "specificuse.tld", "Reserved; alloc. token required"));
}
@Test @Test
void testSuccess_clTridNotSpecified() throws Exception { void testSuccess_clTridNotSpecified() throws Exception {
setEppInput("domain_check_no_cltrid.xml"); setEppInput("domain_check_no_cltrid.xml");

View file

@ -29,6 +29,7 @@ import static google.registry.model.billing.BillingBase.RenewalPriceBehavior.SPE
import static google.registry.model.domain.fee.Fee.FEE_EXTENSION_URIS; import static google.registry.model.domain.fee.Fee.FEE_EXTENSION_URIS;
import static google.registry.model.domain.token.AllocationToken.TokenType.BULK_PRICING; import static google.registry.model.domain.token.AllocationToken.TokenType.BULK_PRICING;
import static google.registry.model.domain.token.AllocationToken.TokenType.DEFAULT_PROMO; import static google.registry.model.domain.token.AllocationToken.TokenType.DEFAULT_PROMO;
import static google.registry.model.domain.token.AllocationToken.TokenType.REGISTER_BSA;
import static google.registry.model.domain.token.AllocationToken.TokenType.SINGLE_USE; import static google.registry.model.domain.token.AllocationToken.TokenType.SINGLE_USE;
import static google.registry.model.domain.token.AllocationToken.TokenType.UNLIMITED_USE; import static google.registry.model.domain.token.AllocationToken.TokenType.UNLIMITED_USE;
import static google.registry.model.eppcommon.EppXmlTransformer.marshal; import static google.registry.model.eppcommon.EppXmlTransformer.marshal;
@ -255,6 +256,14 @@ class DomainCreateFlowTest extends ResourceFlowTestCase<DomainCreateFlow, Domain
persistClaimsList(ImmutableMap.of("example-one", CLAIMS_KEY, "test-validate", CLAIMS_KEY)); persistClaimsList(ImmutableMap.of("example-one", CLAIMS_KEY, "test-validate", CLAIMS_KEY));
} }
private void enrollTldInBsa() {
persistResource(
Tld.get("tld")
.asBuilder()
.setBsaEnrollStartTime(Optional.of(clock.nowUtc().minusSeconds(1)))
.build());
}
/** /**
* Create host and contact entries for testing. * Create host and contact entries for testing.
* *
@ -2593,12 +2602,92 @@ class DomainCreateFlowTest extends ResourceFlowTestCase<DomainCreateFlow, Domain
} }
@Test @Test
void testFailure_blockedByBsa() throws Exception { void testSuccess_blockedByBsa_hasRegisterBsaToken() throws Exception {
enrollTldInBsa();
allocationToken =
persistResource(
new AllocationToken.Builder()
.setToken("abc123")
.setTokenType(REGISTER_BSA)
.setDomainName("example.tld")
.build());
persistBsaLabel("example");
persistContactsAndHosts();
setEppInput(
"domain_create_allocationtoken.xml",
ImmutableMap.of("DOMAIN", "example.tld", "YEARS", "2"));
runFlow();
assertSuccessfulCreate("tld", ImmutableSet.of(), allocationToken);
}
@Test
void testSuccess_blockedByBsa_reservedDomain_viaAllocationTokenExtension() throws Exception {
enrollTldInBsa();
allocationToken =
persistResource(
new AllocationToken.Builder()
.setToken("abc123")
.setTokenType(REGISTER_BSA)
.setDomainName("resdom.tld")
.build());
persistBsaLabel("resdom");
setEppInput(
"domain_create_allocationtoken.xml", ImmutableMap.of("DOMAIN", "resdom.tld", "YEARS", "2"));
persistContactsAndHosts();
runFlowAssertResponse(
loadFile("domain_create_response.xml", ImmutableMap.of("DOMAIN", "resdom.tld")));
assertSuccessfulCreate("tld", ImmutableSet.of(RESERVED), allocationToken);
assertNoLordn();
assertAllocationTokenWasRedeemed("abc123");
}
@Test
void testSuccess_blockedByBsa_quietPeriod_skipTldStateCheckWithToken() throws Exception {
enrollTldInBsa();
AllocationToken token =
persistResource(
new AllocationToken.Builder()
.setToken("abc123")
.setTokenType(REGISTER_BSA)
.setRegistrationBehavior(RegistrationBehavior.BYPASS_TLD_STATE)
.setDomainName("example.tld")
.build());
persistContactsAndHosts();
persistBsaLabel("example");
setEppInput(
"domain_create_allocationtoken.xml",
ImmutableMap.of("DOMAIN", "example.tld", "YEARS", "2"));
persistResource( persistResource(
Tld.get("tld") Tld.get("tld")
.asBuilder() .asBuilder()
.setBsaEnrollStartTime(Optional.of(clock.nowUtc().minusSeconds(1))) .setTldStateTransitions(ImmutableSortedMap.of(START_OF_TIME, QUIET_PERIOD))
.build()); .build());
runFlow();
assertSuccessfulCreate("tld", ImmutableSet.of(), token);
}
@Test
void testSuccess_blockedByBsa_anchorTenant() throws Exception {
enrollTldInBsa();
allocationToken =
persistResource(
new AllocationToken.Builder()
.setToken("abcDEF23456")
.setTokenType(REGISTER_BSA)
.setDomainName("anchor.tld")
.build());
setEppInput("domain_create_anchor_allocationtoken.xml");
persistContactsAndHosts();
persistBsaLabel("anchor");
runFlowAssertResponse(loadFile("domain_create_anchor_response.xml"));
assertSuccessfulCreate("tld", ImmutableSet.of(ANCHOR_TENANT), allocationToken);
assertNoLordn();
assertAllocationTokenWasRedeemed("abcDEF23456");
}
@Test
void testFailure_blockedByBsa() throws Exception {
enrollTldInBsa();
persistBsaLabel("example"); persistBsaLabel("example");
persistContactsAndHosts(); persistContactsAndHosts();
EppException thrown = assertThrows(DomainLabelBlockedByBsaException.class, this::runFlow); EppException thrown = assertThrows(DomainLabelBlockedByBsaException.class, this::runFlow);
@ -2619,6 +2708,41 @@ class DomainCreateFlowTest extends ResourceFlowTestCase<DomainCreateFlow, Domain
.isEqualTo(loadFile("domain_create_blocked_by_bsa.xml")); .isEqualTo(loadFile("domain_create_blocked_by_bsa.xml"));
} }
@Test
void testFailure_blockedByBsa_hasWrongToken() throws Exception {
enrollTldInBsa();
allocationToken =
persistResource(
new AllocationToken.Builder()
.setToken("abc123")
.setTokenType(SINGLE_USE)
.setRegistrationBehavior(RegistrationBehavior.BYPASS_TLD_STATE)
.setDomainName("example.tld")
.build());
persistBsaLabel("example");
persistContactsAndHosts();
setEppInput(
"domain_create_allocationtoken.xml",
ImmutableMap.of("DOMAIN", "example.tld", "YEARS", "2"));
EppException thrown = assertThrows(DomainLabelBlockedByBsaException.class, this::runFlow);
assertAboutEppExceptions()
.that(thrown)
.marshalsToXml()
.and()
.hasMessage("Domain label is blocked by the Brand Safety Alliance");
byte[] responseXmlBytes =
marshal(
EppOutput.create(
new EppResponse.Builder()
.setTrid(Trid.create(null, "server-trid"))
.setResult(thrown.getResult())
.build()),
ValidationMode.STRICT);
assertThat(new String(responseXmlBytes, StandardCharsets.UTF_8))
.isEqualTo(loadFile("domain_create_blocked_by_bsa.xml"));
}
@Test @Test
void testFailure_uppercase() { void testFailure_uppercase() {
doFailingDomainNameTest("Example.tld", BadDomainNameCharacterException.class); doFailingDomainNameTest("Example.tld", BadDomainNameCharacterException.class);

View file

@ -21,6 +21,7 @@ import static google.registry.model.domain.token.AllocationToken.TokenStatus.END
import static google.registry.model.domain.token.AllocationToken.TokenStatus.NOT_STARTED; 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.TokenStatus.VALID;
import static google.registry.model.domain.token.AllocationToken.TokenType.BULK_PRICING; import static google.registry.model.domain.token.AllocationToken.TokenType.BULK_PRICING;
import static google.registry.model.domain.token.AllocationToken.TokenType.REGISTER_BSA;
import static google.registry.model.domain.token.AllocationToken.TokenType.SINGLE_USE; import static google.registry.model.domain.token.AllocationToken.TokenType.SINGLE_USE;
import static google.registry.model.domain.token.AllocationToken.TokenType.UNLIMITED_USE; import static google.registry.model.domain.token.AllocationToken.TokenType.UNLIMITED_USE;
import static google.registry.testing.DatabaseHelper.createTld; import static google.registry.testing.DatabaseHelper.createTld;
@ -245,6 +246,18 @@ public class AllocationTokenTest extends EntityTestCase {
.isEqualTo("Bulk tokens may only be valid for CREATE actions"); .isEqualTo("Bulk tokens may only be valid for CREATE actions");
} }
@Test
void testBuild_registerBsa_missingDomain() {
createTld("tld");
// REGISTER_BSA requires a domain
AllocationToken.Builder token =
new AllocationToken.Builder().setToken("abc").setTokenType(REGISTER_BSA);
assertThat(assertThrows(IllegalArgumentException.class, () -> token.build()))
.hasMessageThat()
.isEqualTo("REGISTER_BSA tokens must be tied to a domain");
token.setDomainName("example.tld").build();
}
@Test @Test
void testFail_bulkTokenNullEppActions() { void testFail_bulkTokenNullEppActions() {
AllocationToken.Builder builder = AllocationToken.Builder builder =
@ -317,7 +330,7 @@ public class AllocationTokenTest extends EntityTestCase {
IllegalArgumentException thrown = assertThrows(IllegalArgumentException.class, builder::build); IllegalArgumentException thrown = assertThrows(IllegalArgumentException.class, builder::build);
assertThat(thrown) assertThat(thrown)
.hasMessageThat() .hasMessageThat()
.isEqualTo("Domain name can only be specified for SINGLE_USE tokens"); .isEqualTo("Domain name can only be specified for SINGLE_USE or REGISTER_BSA tokens");
} }
@Test @Test
@ -347,7 +360,8 @@ public class AllocationTokenTest extends EntityTestCase {
IllegalArgumentException thrown = assertThrows(IllegalArgumentException.class, builder::build); IllegalArgumentException thrown = assertThrows(IllegalArgumentException.class, builder::build);
assertThat(thrown) assertThat(thrown)
.hasMessageThat() .hasMessageThat()
.isEqualTo("Redemption history entry can only be specified for SINGLE_USE tokens"); .isEqualTo(
"Redemption history entry can only be specified for SINGLE_USE or REGISTER_BSA tokens");
} }
@Test @Test

View file

@ -380,7 +380,7 @@ class GenerateAllocationTokensCommandTest extends CommandTestCase<GenerateAlloca
.hasMessageThat() .hasMessageThat()
.isEqualTo( .isEqualTo(
"Invalid value for -t parameter. Allowed values:[BULK_PRICING, DEFAULT_PROMO, PACKAGE," "Invalid value for -t parameter. Allowed values:[BULK_PRICING, DEFAULT_PROMO, PACKAGE,"
+ " SINGLE_USE, UNLIMITED_USE]"); + " SINGLE_USE, UNLIMITED_USE, REGISTER_BSA]");
} }
@Test @Test