Implement EPP Allocation Token Extension for domain:renew (#1693)

* Implement EPP Allocation Token Extension for domain:renew

* Add more tests

* Fix AllocationTokenFlowUtilsTest

* Combine loadTokenAndValidateDomain with verifyAllocationTokenIfPresent

* Change to Optional.empty

* Remove unused variable
This commit is contained in:
sarahcaseybot
2022-07-26 10:28:32 -04:00
committed by GitHub
parent 1ca80010c0
commit 40c58bb6f3
8 changed files with 481 additions and 83 deletions
@@ -15,10 +15,13 @@
package google.registry.flows.domain;
import static com.google.common.truth.Truth.assertThat;
import static com.google.common.truth.Truth8.assertThat;
import static google.registry.flows.domain.DomainTransferFlowTestCase.persistWithPendingTransfer;
import static google.registry.model.billing.BillingEvent.RenewalPriceBehavior.DEFAULT;
import static google.registry.model.billing.BillingEvent.RenewalPriceBehavior.NONPREMIUM;
import static google.registry.model.billing.BillingEvent.RenewalPriceBehavior.SPECIFIED;
import static google.registry.model.domain.token.AllocationToken.TokenType.SINGLE_USE;
import static google.registry.model.domain.token.AllocationToken.TokenType.UNLIMITED_USE;
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
import static google.registry.testing.DatabaseHelper.assertBillingEvents;
import static google.registry.testing.DatabaseHelper.assertPollMessages;
@@ -46,6 +49,7 @@ import static org.junit.jupiter.api.Assertions.assertThrows;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.ImmutableSortedMap;
import com.googlecode.objectify.Key;
import google.registry.flows.EppException;
import google.registry.flows.EppRequestSource;
import google.registry.flows.FlowUtils.NotLoggedInException;
@@ -64,6 +68,12 @@ import google.registry.flows.domain.DomainFlowUtils.NotAuthorizedForTldException
import google.registry.flows.domain.DomainFlowUtils.RegistrarMustBeActiveForThisOperationException;
import google.registry.flows.domain.DomainFlowUtils.UnsupportedFeeAttributeException;
import google.registry.flows.domain.DomainRenewFlow.IncorrectCurrentExpirationDateException;
import google.registry.flows.domain.token.AllocationTokenFlowUtils.AllocationTokenNotInPromotionException;
import google.registry.flows.domain.token.AllocationTokenFlowUtils.AllocationTokenNotValidForDomainException;
import google.registry.flows.domain.token.AllocationTokenFlowUtils.AllocationTokenNotValidForRegistrarException;
import google.registry.flows.domain.token.AllocationTokenFlowUtils.AllocationTokenNotValidForTldException;
import google.registry.flows.domain.token.AllocationTokenFlowUtils.AlreadyRedeemedAllocationTokenException;
import google.registry.flows.domain.token.AllocationTokenFlowUtils.InvalidAllocationTokenException;
import google.registry.flows.exceptions.ResourceStatusProhibitsOperationException;
import google.registry.model.billing.BillingEvent;
import google.registry.model.billing.BillingEvent.Flag;
@@ -73,6 +83,8 @@ import google.registry.model.domain.DomainBase;
import google.registry.model.domain.DomainHistory;
import google.registry.model.domain.GracePeriod;
import google.registry.model.domain.rgp.GracePeriodStatus;
import google.registry.model.domain.token.AllocationToken;
import google.registry.model.domain.token.AllocationToken.TokenStatus;
import google.registry.model.eppcommon.StatusValue;
import google.registry.model.poll.PollMessage;
import google.registry.model.registrar.Registrar;
@@ -81,6 +93,8 @@ import google.registry.model.reporting.DomainTransactionRecord;
import google.registry.model.reporting.DomainTransactionRecord.TransactionReportField;
import google.registry.model.reporting.HistoryEntry;
import google.registry.model.tld.Registry;
import google.registry.persistence.VKey;
import google.registry.testing.DatabaseHelper;
import google.registry.testing.SetClockExtension;
import java.util.Map;
import javax.annotation.Nullable;
@@ -321,6 +335,13 @@ class DomainRenewFlowTest extends ResourceFlowTestCase<DomainRenewFlow, DomainBa
renewBillingEvent));
}
@Test
private void assertAllocationTokenWasNotRedeemed(String token) {
AllocationToken reloadedToken =
tm().transact(() -> tm().loadByKey(VKey.create(AllocationToken.class, token)));
assertThat(reloadedToken.isRedeemed()).isFalse();
}
@Test
void testNotLoggedIn() {
sessionMetadata.setRegistrarId(null);
@@ -577,6 +598,164 @@ class DomainRenewFlowTest extends ResourceFlowTestCase<DomainRenewFlow, DomainBa
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
void testSuccess_allocationToken() throws Exception {
setEppInput(
"domain_renew_allocationtoken.xml", ImmutableMap.of("DOMAIN", "example.tld", "YEARS", "2"));
persistDomain();
AllocationToken allocationToken =
persistResource(
new AllocationToken.Builder()
.setToken("abc123")
.setTokenType(SINGLE_USE)
.setDomainName("example.tld")
.build());
runFlowAssertResponse(
loadFile(
"domain_renew_response.xml",
ImmutableMap.of("DOMAIN", "example.tld", "EXDATE", "2002-04-03T22:00:00.0Z")));
assertThat(DatabaseHelper.loadByEntity(allocationToken).getRedemptionHistoryEntry())
.isPresent();
}
@Test
void testSuccess_allocationTokenMultiUse() throws Exception {
setEppInput(
"domain_renew_allocationtoken.xml", ImmutableMap.of("DOMAIN", "example.tld", "YEARS", "2"));
persistDomain();
persistResource(
new AllocationToken.Builder().setToken("abc123").setTokenType(UNLIMITED_USE).build());
runFlowAssertResponse(
loadFile(
"domain_renew_response.xml",
ImmutableMap.of("DOMAIN", "example.tld", "EXDATE", "2002-04-03T22:00:00.0Z")));
clock.advanceOneMilli();
setEppInput(
"domain_renew_allocationtoken.xml",
ImmutableMap.of("DOMAIN", "other-example.tld", "YEARS", "2"));
persistDomain();
runFlowAssertResponse(
loadFile(
"domain_renew_response.xml",
ImmutableMap.of("DOMAIN", "other-example.tld", "EXDATE", "2002-04-03T22:00:00.0Z")));
}
@Test
void testFailure_invalidAllocationToken() throws Exception {
setEppInput(
"domain_renew_allocationtoken.xml", ImmutableMap.of("DOMAIN", "example.tld", "YEARS", "2"));
persistDomain();
EppException thrown = assertThrows(InvalidAllocationTokenException.class, this::runFlow);
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
void testFailure_allocationTokenIsForADifferentDomain() throws Exception {
setEppInput(
"domain_renew_allocationtoken.xml", ImmutableMap.of("DOMAIN", "example.tld", "YEARS", "2"));
persistDomain();
persistResource(
new AllocationToken.Builder()
.setToken("abc123")
.setTokenType(SINGLE_USE)
.setDomainName("otherdomain.tld")
.build());
clock.advanceOneMilli();
EppException thrown =
assertThrows(AllocationTokenNotValidForDomainException.class, this::runFlow);
assertAboutEppExceptions().that(thrown).marshalsToXml();
assertAllocationTokenWasNotRedeemed("abc123");
}
@Test
void testFailure_promotionNotActive() throws Exception {
setEppInput(
"domain_renew_allocationtoken.xml", ImmutableMap.of("DOMAIN", "example.tld", "YEARS", "2"));
persistDomain();
persistResource(
new AllocationToken.Builder()
.setToken("abc123")
.setTokenType(UNLIMITED_USE)
.setDiscountFraction(0.5)
.setTokenStatusTransitions(
ImmutableSortedMap.<DateTime, TokenStatus>naturalOrder()
.put(START_OF_TIME, TokenStatus.NOT_STARTED)
.put(clock.nowUtc().plusDays(1), TokenStatus.VALID)
.put(clock.nowUtc().plusDays(60), TokenStatus.ENDED)
.build())
.build());
assertAboutEppExceptions()
.that(assertThrows(AllocationTokenNotInPromotionException.class, this::runFlow))
.marshalsToXml();
}
@Test
void testFailure_promoTokenNotValidForRegistrar() throws Exception {
setEppInput(
"domain_renew_allocationtoken.xml", ImmutableMap.of("DOMAIN", "example.tld", "YEARS", "2"));
persistDomain();
persistResource(
new AllocationToken.Builder()
.setToken("abc123")
.setTokenType(UNLIMITED_USE)
.setAllowedRegistrarIds(ImmutableSet.of("someClientId"))
.setDiscountFraction(0.5)
.setTokenStatusTransitions(
ImmutableSortedMap.<DateTime, TokenStatus>naturalOrder()
.put(START_OF_TIME, TokenStatus.NOT_STARTED)
.put(clock.nowUtc().minusDays(1), TokenStatus.VALID)
.put(clock.nowUtc().plusDays(1), TokenStatus.ENDED)
.build())
.build());
assertAboutEppExceptions()
.that(assertThrows(AllocationTokenNotValidForRegistrarException.class, this::runFlow))
.marshalsToXml();
assertAllocationTokenWasNotRedeemed("abc123");
}
@Test
void testFailure_promoTokenNotValidForTld() throws Exception {
setEppInput(
"domain_renew_allocationtoken.xml", ImmutableMap.of("DOMAIN", "example.tld", "YEARS", "2"));
persistDomain();
persistResource(
new AllocationToken.Builder()
.setToken("abc123")
.setTokenType(UNLIMITED_USE)
.setAllowedTlds(ImmutableSet.of("example"))
.setDiscountFraction(0.5)
.setTokenStatusTransitions(
ImmutableSortedMap.<DateTime, TokenStatus>naturalOrder()
.put(START_OF_TIME, TokenStatus.NOT_STARTED)
.put(clock.nowUtc().minusDays(1), TokenStatus.VALID)
.put(clock.nowUtc().plusDays(1), TokenStatus.ENDED)
.build())
.build());
assertAboutEppExceptions()
.that(assertThrows(AllocationTokenNotValidForTldException.class, this::runFlow))
.marshalsToXml();
assertAllocationTokenWasNotRedeemed("abc123");
}
@Test
void testFailure_alreadyRedemeedAllocationToken() throws Exception {
setEppInput(
"domain_renew_allocationtoken.xml", ImmutableMap.of("DOMAIN", "example.tld", "YEARS", "2"));
persistDomain();
DomainBase domain = persistActiveDomain("foo.tld");
Key<HistoryEntry> historyEntryKey = Key.create(Key.create(domain), HistoryEntry.class, 505L);
persistResource(
new AllocationToken.Builder()
.setToken("abc123")
.setTokenType(SINGLE_USE)
.setRedemptionHistoryEntry(HistoryEntry.createVKey(historyEntryKey))
.build());
clock.advanceOneMilli();
EppException thrown =
assertThrows(AlreadyRedeemedAllocationTokenException.class, this::runFlow);
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
void testFailure_suspendedRegistrarCantRenewDomain() {
doFailingTest_invalidRegistrarState(State.SUSPENDED);
@@ -22,6 +22,7 @@ import static google.registry.model.domain.token.AllocationToken.TokenStatus.VAL
import static google.registry.model.domain.token.AllocationToken.TokenType.SINGLE_USE;
import static google.registry.model.domain.token.AllocationToken.TokenType.UNLIMITED_USE;
import static google.registry.testing.DatabaseHelper.createTld;
import static google.registry.testing.DatabaseHelper.newDomainBase;
import static google.registry.testing.DatabaseHelper.persistActiveDomain;
import static google.registry.testing.DatabaseHelper.persistResource;
import static google.registry.testing.EppExceptionSubject.assertAboutEppExceptions;
@@ -47,9 +48,11 @@ import google.registry.model.domain.DomainBase;
import google.registry.model.domain.DomainCommand;
import google.registry.model.domain.token.AllocationToken;
import google.registry.model.domain.token.AllocationToken.TokenStatus;
import google.registry.model.domain.token.AllocationTokenExtension;
import google.registry.model.reporting.HistoryEntry;
import google.registry.model.tld.Registry;
import google.registry.testing.AppEngineExtension;
import java.util.Optional;
import org.joda.time.DateTime;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
@@ -64,98 +67,195 @@ class AllocationTokenFlowUtilsTest {
@RegisterExtension
final AppEngineExtension appEngine = AppEngineExtension.builder().withCloudSql().build();
private final AllocationTokenExtension allocationTokenExtension =
mock(AllocationTokenExtension.class);
@BeforeEach
void beforeEach() {
createTld("tld");
}
@Test
void test_validateToken_successfullyVerifiesValidToken() throws Exception {
void test_validateToken_successfullyVerifiesValidTokenOnCreate() throws Exception {
AllocationToken token =
persistResource(
new AllocationToken.Builder().setToken("tokeN").setTokenType(SINGLE_USE).build());
when(allocationTokenExtension.getAllocationToken()).thenReturn("tokeN");
assertThat(
flowUtils.loadTokenAndValidateDomainCreate(
createCommand("blah.tld"),
"tokeN",
Registry.get("tld"),
"TheRegistrar",
DateTime.now(UTC)))
flowUtils
.verifyAllocationTokenCreateIfPresent(
createCommand("blah.tld"),
Registry.get("tld"),
"TheRegistrar",
DateTime.now(UTC),
Optional.of(allocationTokenExtension))
.get())
.isEqualTo(token);
}
@Test
void test_validateToken_failsOnNonexistentToken() {
assertValidateThrowsEppException(InvalidAllocationTokenException.class);
void test_validateToken_successfullyVerifiesValidTokenExistingDomain() throws Exception {
AllocationToken token =
persistResource(
new AllocationToken.Builder().setToken("tokeN").setTokenType(SINGLE_USE).build());
when(allocationTokenExtension.getAllocationToken()).thenReturn("tokeN");
assertThat(
flowUtils
.verifyAllocationTokenIfPresent(
newDomainBase("blah.tld"),
Registry.get("tld"),
"TheRegistrar",
DateTime.now(UTC),
Optional.of(allocationTokenExtension))
.get())
.isEqualTo(token);
}
@Test
void test_validateToken_failsOnNullToken() {
void test_validateTokenCreate_failsOnNonexistentToken() {
assertValidateCreateThrowsEppException(InvalidAllocationTokenException.class);
}
@Test
void test_validateTokenExistingDomain_failsOnNonexistentToken() {
assertValidateExistingDomainThrowsEppException(InvalidAllocationTokenException.class);
}
@Test
void test_validateTokenCreate_failsOnNullToken() {
assertAboutEppExceptions()
.that(
assertThrows(
InvalidAllocationTokenException.class,
() ->
flowUtils.loadTokenAndValidateDomainCreate(
flowUtils.verifyAllocationTokenCreateIfPresent(
createCommand("blah.tld"),
null,
Registry.get("tld"),
"TheRegistrar",
DateTime.now(UTC))))
DateTime.now(UTC),
Optional.of(allocationTokenExtension))))
.marshalsToXml();
}
@Test
void test_validateToken_callsCustomLogic() {
void test_validateTokenExistingDomain_failsOnNullToken() {
assertAboutEppExceptions()
.that(
assertThrows(
InvalidAllocationTokenException.class,
() ->
flowUtils.verifyAllocationTokenIfPresent(
newDomainBase("blah.tld"),
Registry.get("tld"),
"TheRegistrar",
DateTime.now(UTC),
Optional.of(allocationTokenExtension))))
.marshalsToXml();
}
@Test
void test_validateTokenCreate_callsCustomLogic() {
AllocationTokenFlowUtils failingFlowUtils =
new AllocationTokenFlowUtils(new FailingAllocationTokenCustomLogic());
persistResource(
new AllocationToken.Builder().setToken("tokeN").setTokenType(SINGLE_USE).build());
when(allocationTokenExtension.getAllocationToken()).thenReturn("tokeN");
Exception thrown =
assertThrows(
IllegalStateException.class,
() ->
failingFlowUtils.loadTokenAndValidateDomainCreate(
failingFlowUtils.verifyAllocationTokenCreateIfPresent(
createCommand("blah.tld"),
"tokeN",
Registry.get("tld"),
"TheRegistrar",
DateTime.now(UTC)));
DateTime.now(UTC),
Optional.of(allocationTokenExtension)));
assertThat(thrown).hasMessageThat().isEqualTo("failed for tests");
}
@Test
void test_validateToken_invalidForClientId() {
void test_validateTokenExistingDomain_callsCustomLogic() {
AllocationTokenFlowUtils failingFlowUtils =
new AllocationTokenFlowUtils(new FailingAllocationTokenCustomLogic());
persistResource(
new AllocationToken.Builder().setToken("tokeN").setTokenType(SINGLE_USE).build());
when(allocationTokenExtension.getAllocationToken()).thenReturn("tokeN");
Exception thrown =
assertThrows(
IllegalStateException.class,
() ->
failingFlowUtils.verifyAllocationTokenIfPresent(
newDomainBase("blah.tld"),
Registry.get("tld"),
"TheRegistrar",
DateTime.now(UTC),
Optional.of(allocationTokenExtension)));
assertThat(thrown).hasMessageThat().isEqualTo("failed for tests");
}
@Test
void test_validateTokenCreate_invalidForClientId() {
persistResource(
createOneMonthPromoTokenBuilder(DateTime.now(UTC).minusDays(1))
.setAllowedRegistrarIds(ImmutableSet.of("NewRegistrar"))
.build());
assertValidateThrowsEppException(AllocationTokenNotValidForRegistrarException.class);
assertValidateCreateThrowsEppException(AllocationTokenNotValidForRegistrarException.class);
}
@Test
void test_validateToken_invalidForTld() {
void test_validateTokenExistingDomain_invalidForClientId() {
persistResource(
createOneMonthPromoTokenBuilder(DateTime.now(UTC).minusDays(1))
.setAllowedRegistrarIds(ImmutableSet.of("NewRegistrar"))
.build());
assertValidateExistingDomainThrowsEppException(
AllocationTokenNotValidForRegistrarException.class);
}
@Test
void test_validateTokenCreate_invalidForTld() {
persistResource(
createOneMonthPromoTokenBuilder(DateTime.now(UTC).minusDays(1))
.setAllowedTlds(ImmutableSet.of("nottld"))
.build());
assertValidateThrowsEppException(AllocationTokenNotValidForTldException.class);
assertValidateCreateThrowsEppException(AllocationTokenNotValidForTldException.class);
}
@Test
void test_validateToken_beforePromoStart() {
void test_validateTokenExistingDomain_invalidForTld() {
persistResource(
createOneMonthPromoTokenBuilder(DateTime.now(UTC).minusDays(1))
.setAllowedTlds(ImmutableSet.of("nottld"))
.build());
assertValidateExistingDomainThrowsEppException(AllocationTokenNotValidForTldException.class);
}
@Test
void test_validateTokenCreate_beforePromoStart() {
persistResource(createOneMonthPromoTokenBuilder(DateTime.now(UTC).plusDays(1)).build());
assertValidateThrowsEppException(AllocationTokenNotInPromotionException.class);
assertValidateCreateThrowsEppException(AllocationTokenNotInPromotionException.class);
}
@Test
void test_validateToken_afterPromoEnd() {
void test_validateTokenExistingDomain_beforePromoStart() {
persistResource(createOneMonthPromoTokenBuilder(DateTime.now(UTC).plusDays(1)).build());
assertValidateExistingDomainThrowsEppException(AllocationTokenNotInPromotionException.class);
}
@Test
void test_validateTokenCreate_afterPromoEnd() {
persistResource(createOneMonthPromoTokenBuilder(DateTime.now(UTC).minusMonths(2)).build());
assertValidateThrowsEppException(AllocationTokenNotInPromotionException.class);
assertValidateCreateThrowsEppException(AllocationTokenNotInPromotionException.class);
}
@Test
void test_validateToken_promoCancelled() {
void test_validateTokenExistingDomain_afterPromoEnd() {
persistResource(createOneMonthPromoTokenBuilder(DateTime.now(UTC).minusMonths(2)).build());
assertValidateExistingDomainThrowsEppException(AllocationTokenNotInPromotionException.class);
}
@Test
void test_validateTokenCreate_promoCancelled() {
// the promo would be valid but it was cancelled 12 hours ago
persistResource(
createOneMonthPromoTokenBuilder(DateTime.now(UTC).minusDays(1))
@@ -166,7 +266,22 @@ class AllocationTokenFlowUtilsTest {
.put(DateTime.now(UTC).minusHours(12), CANCELLED)
.build())
.build());
assertValidateThrowsEppException(AllocationTokenNotInPromotionException.class);
assertValidateCreateThrowsEppException(AllocationTokenNotInPromotionException.class);
}
@Test
void test_validateTokenExistingDomain_promoCancelled() {
// the promo would be valid but it was cancelled 12 hours ago
persistResource(
createOneMonthPromoTokenBuilder(DateTime.now(UTC).minusDays(1))
.setTokenStatusTransitions(
ImmutableSortedMap.<DateTime, TokenStatus>naturalOrder()
.put(START_OF_TIME, NOT_STARTED)
.put(DateTime.now(UTC).minusMonths(1), VALID)
.put(DateTime.now(UTC).minusHours(12), CANCELLED)
.build())
.build());
assertValidateExistingDomainThrowsEppException(AllocationTokenNotInPromotionException.class);
}
@Test
@@ -259,18 +374,33 @@ class AllocationTokenFlowUtilsTest {
.inOrder();
}
private void assertValidateThrowsEppException(Class<? extends EppException> clazz) {
private void assertValidateCreateThrowsEppException(Class<? extends EppException> clazz) {
assertAboutEppExceptions()
.that(
assertThrows(
clazz,
() ->
flowUtils.loadTokenAndValidateDomainCreate(
flowUtils.verifyAllocationTokenCreateIfPresent(
createCommand("blah.tld"),
"tokeN",
Registry.get("tld"),
"TheRegistrar",
DateTime.now(UTC))))
DateTime.now(UTC),
Optional.of(allocationTokenExtension))))
.marshalsToXml();
}
private void assertValidateExistingDomainThrowsEppException(Class<? extends EppException> clazz) {
assertAboutEppExceptions()
.that(
assertThrows(
clazz,
() ->
flowUtils.verifyAllocationTokenIfPresent(
newDomainBase("blah.tld"),
Registry.get("tld"),
"TheRegistrar",
DateTime.now(UTC),
Optional.of(allocationTokenExtension))))
.marshalsToXml();
}
@@ -280,7 +410,8 @@ class AllocationTokenFlowUtilsTest {
return command;
}
private static AllocationToken.Builder createOneMonthPromoTokenBuilder(DateTime promoStart) {
private AllocationToken.Builder createOneMonthPromoTokenBuilder(DateTime promoStart) {
when(allocationTokenExtension.getAllocationToken()).thenReturn("tokeN");
return new AllocationToken.Builder()
.setToken("tokeN")
.setTokenType(UNLIMITED_USE)
@@ -305,6 +436,16 @@ class AllocationTokenFlowUtilsTest {
throw new IllegalStateException("failed for tests");
}
@Override
public AllocationToken validateToken(
DomainBase domain,
AllocationToken token,
Registry registry,
String registrarId,
DateTime now) {
throw new IllegalStateException("failed for tests");
}
@Override
public ImmutableMap<InternetDomainName, String> checkDomainsWithToken(
ImmutableList<InternetDomainName> domainNames,
@@ -0,0 +1,20 @@
<epp xmlns="urn:ietf:params:xml:ns:epp-1.0">
<command>
<renew>
<domain:renew
xmlns:domain="urn:ietf:params:xml:ns:domain-1.0">
<domain:name>%DOMAIN%</domain:name>
<domain:curExpDate>2000-04-03</domain:curExpDate>
<domain:period unit="y">%YEARS%</domain:period>
</domain:renew>
</renew>
<extension>
<allocationToken:allocationToken
xmlns:allocationToken=
"urn:ietf:params:xml:ns:allocationToken-1.0">
abc123
</allocationToken:allocationToken>
</extension>
<clTRID>ABC-12345</clTRID>
</command>
</epp>