diff --git a/core/src/main/java/google/registry/batch/CheckPackagesComplianceAction.java b/core/src/main/java/google/registry/batch/CheckPackagesComplianceAction.java new file mode 100644 index 000000000..aa07d4600 --- /dev/null +++ b/core/src/main/java/google/registry/batch/CheckPackagesComplianceAction.java @@ -0,0 +1,80 @@ +// Copyright 2022 The Nomulus Authors. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +package google.registry.batch; + +import static google.registry.persistence.transaction.TransactionManagerFactory.jpaTm; +import static google.registry.persistence.transaction.TransactionManagerFactory.tm; + +import com.google.common.collect.ImmutableList; +import com.google.common.flogger.FluentLogger; +import google.registry.model.domain.DomainHistory; +import google.registry.model.domain.token.PackagePromotion; +import google.registry.request.Action; +import google.registry.request.Action.Service; +import google.registry.request.auth.Auth; +import java.util.List; + +/** + * An action that checks all {@link PackagePromotion} objects for compliance with their max create + * limit. + */ +@Action( + service = Service.BACKEND, + path = CheckPackagesComplianceAction.PATH, + auth = Auth.AUTH_INTERNAL_OR_ADMIN) +public class CheckPackagesComplianceAction implements Runnable { + + public static final String PATH = "/_dr/task/checkPackagesCompliance"; + private static final FluentLogger logger = FluentLogger.forEnclosingClass(); + + @Override + public void run() { + tm().transact( + () -> { + ImmutableList packages = tm().loadAllOf(PackagePromotion.class); + ImmutableList.Builder packagesOverCreateLimit = + new ImmutableList.Builder<>(); + for (PackagePromotion packagePromo : packages) { + List creates = + jpaTm() + .query( + "FROM DomainHistory WHERE current_package_token = :token AND" + + " modificationTime >= :lastBilling AND type = 'DOMAIN_CREATE'", + DomainHistory.class) + .setParameter("token", packagePromo.getToken().getSqlKey().toString()) + .setParameter( + "lastBilling", packagePromo.getNextBillingDate().minusYears(1)) + .getResultList(); + + if (creates.size() > packagePromo.getMaxCreates()) { + int overage = creates.size() - packagePromo.getMaxCreates(); + logger.atInfo().log( + "Package with package token %s has exceeded their max domain creation limit" + + " by %d name(s).", + packagePromo.getToken().getSqlKey(), overage); + packagesOverCreateLimit.add(packagePromo); + } + } + if (packagesOverCreateLimit.build().isEmpty()) { + logger.atInfo().log("Found no packages over their create limit."); + } else { + logger.atInfo().log( + "Found %d packages over their create limit.", + packagesOverCreateLimit.build().size()); + // TODO(sarahbot@) Send email to registrar and registry informing of creation + // overage once email template is finalized. + } + }); + } +} diff --git a/core/src/test/java/google/registry/batch/CheckPackagesComplianceActionTest.java b/core/src/test/java/google/registry/batch/CheckPackagesComplianceActionTest.java new file mode 100644 index 000000000..b76a186f8 --- /dev/null +++ b/core/src/test/java/google/registry/batch/CheckPackagesComplianceActionTest.java @@ -0,0 +1,243 @@ +// Copyright 2022 The Nomulus Authors. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +package google.registry.batch; + +import static google.registry.persistence.transaction.TransactionManagerFactory.jpaTm; +import static google.registry.testing.DatabaseHelper.createTld; +import static google.registry.testing.DatabaseHelper.persistActiveContact; +import static google.registry.testing.DatabaseHelper.persistEppResource; +import static google.registry.testing.DatabaseHelper.persistResource; +import static google.registry.testing.LogsSubject.assertAboutLogs; + +import com.google.common.testing.TestLogHandler; +import google.registry.model.billing.BillingEvent.RenewalPriceBehavior; +import google.registry.model.contact.Contact; +import google.registry.model.domain.Domain; +import google.registry.model.domain.token.AllocationToken; +import google.registry.model.domain.token.AllocationToken.TokenType; +import google.registry.model.domain.token.PackagePromotion; +import google.registry.testing.AppEngineExtension; +import google.registry.testing.DatabaseHelper; +import google.registry.testing.FakeClock; +import java.util.logging.Level; +import java.util.logging.Logger; +import org.joda.money.CurrencyUnit; +import org.joda.money.Money; +import org.joda.time.DateTime; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; +import org.testcontainers.shaded.com.google.common.collect.ImmutableSet; + +/** Unit tests for {@link CheckPackagesComplianceAction}. */ +public class CheckPackagesComplianceActionTest { + // This is the default creation time for test data. + private final FakeClock clock = new FakeClock(DateTime.parse("2012-03-25TZ")); + + @RegisterExtension + public final AppEngineExtension appEngine = + AppEngineExtension.builder().withCloudSql().withClock(clock).build(); + + private CheckPackagesComplianceAction action; + private AllocationToken token; + private final TestLogHandler logHandler = new TestLogHandler(); + private final Logger loggerToIntercept = + Logger.getLogger(CheckPackagesComplianceAction.class.getCanonicalName()); + + private Contact contact; + private PackagePromotion packagePromotion; + + @BeforeEach + void beforeEach() { + loggerToIntercept.addHandler(logHandler); + createTld("tld"); + action = new CheckPackagesComplianceAction(); + token = + persistResource( + new AllocationToken.Builder() + .setToken("abc123") + .setTokenType(TokenType.PACKAGE) + .setCreationTimeForTest(DateTime.parse("2010-11-12T05:00:00Z")) + .setAllowedTlds(ImmutableSet.of("foo")) + .setAllowedRegistrarIds(ImmutableSet.of("TheRegistrar")) + .setRenewalPriceBehavior(RenewalPriceBehavior.SPECIFIED) + .setDiscountFraction(1) + .build()); + packagePromotion = + new PackagePromotion.Builder() + .setToken(token) + .setMaxDomains(3) + .setMaxCreates(1) + .setPackagePrice(Money.of(CurrencyUnit.USD, 1000)) + .setNextBillingDate(DateTime.parse("2012-11-12T05:00:00Z")) + .setLastNotificationSent(DateTime.parse("2010-11-12T05:00:00Z")) + .build(); + + jpaTm().transact(() -> jpaTm().put(packagePromotion)); + contact = persistActiveContact("contact1234"); + } + + @AfterEach + void afterEach() { + loggerToIntercept.removeHandler(logHandler); + } + + @Test + void testSuccess_noPackageOverCreateLimit() { + Domain domain1 = + persistEppResource( + DatabaseHelper.newDomain("foo.tld", contact) + .asBuilder() + .setCurrentPackageToken(token.createVKey()) + .build()); + + action.run(); + assertAboutLogs() + .that(logHandler) + .hasLogAtLevelWithMessage(Level.INFO, "Found no packages over their create limit."); + } + + @Test + void testSuccess_onePackageOverCreateLimit() { + // Create limit is 1, creating 2 domains to go over the limit + persistEppResource( + DatabaseHelper.newDomain("foo.tld", contact) + .asBuilder() + .setCurrentPackageToken(token.createVKey()) + .build()); + persistEppResource( + DatabaseHelper.newDomain("buzz.tld", contact) + .asBuilder() + .setCurrentPackageToken(token.createVKey()) + .build()); + + action.run(); + assertAboutLogs() + .that(logHandler) + .hasLogAtLevelWithMessage(Level.INFO, "Found 1 packages over their create limit."); + assertAboutLogs() + .that(logHandler) + .hasLogAtLevelWithMessage( + Level.INFO, + "Package with package token abc123 has exceeded their max domain creation limit by 1" + + " name(s)."); + } + + @Test + void testSuccess_multiplePackagesOverCreateLimit() { + // Create limit is 1, creating 2 domains to go over the limit + persistEppResource( + DatabaseHelper.newDomain("foo.tld", contact) + .asBuilder() + .setCurrentPackageToken(token.createVKey()) + .build()); + persistEppResource( + DatabaseHelper.newDomain("buzz.tld", contact) + .asBuilder() + .setCurrentPackageToken(token.createVKey()) + .build()); + + AllocationToken token2 = + persistResource( + new AllocationToken.Builder() + .setToken("token") + .setTokenType(TokenType.PACKAGE) + .setCreationTimeForTest(DateTime.parse("2010-11-12T05:00:00Z")) + .setAllowedTlds(ImmutableSet.of("foo")) + .setAllowedRegistrarIds(ImmutableSet.of("TheRegistrar")) + .setRenewalPriceBehavior(RenewalPriceBehavior.SPECIFIED) + .setDiscountFraction(1) + .build()); + PackagePromotion packagePromotion2 = + new PackagePromotion.Builder() + .setToken(token2) + .setMaxDomains(8) + .setMaxCreates(1) + .setPackagePrice(Money.of(CurrencyUnit.USD, 1000)) + .setNextBillingDate(DateTime.parse("2012-11-12T05:00:00Z")) + .build(); + jpaTm().transact(() -> jpaTm().put(packagePromotion2)); + + persistEppResource( + DatabaseHelper.newDomain("foo2.tld", contact) + .asBuilder() + .setCurrentPackageToken(token2.createVKey()) + .build()); + persistEppResource( + DatabaseHelper.newDomain("buzz2.tld", contact) + .asBuilder() + .setCurrentPackageToken(token2.createVKey()) + .build()); + action.run(); + assertAboutLogs() + .that(logHandler) + .hasLogAtLevelWithMessage(Level.INFO, "Found 2 packages over their create limit."); + + assertAboutLogs() + .that(logHandler) + .hasLogAtLevelWithMessage( + Level.INFO, + "Package with package token abc123 has exceeded their max domain creation limit by 1" + + " name(s)."); + + assertAboutLogs() + .that(logHandler) + .hasLogAtLevelWithMessage( + Level.INFO, + "Package with package token token has exceeded their max domain creation limit by 1" + + " name(s)."); + } + + @Test + void testSuccess_onlyChecksCurrentBillingYear() { + AllocationToken token2 = + persistResource( + new AllocationToken.Builder() + .setToken("token") + .setTokenType(TokenType.PACKAGE) + .setCreationTimeForTest(DateTime.parse("2010-11-12T05:00:00Z")) + .setAllowedTlds(ImmutableSet.of("foo")) + .setAllowedRegistrarIds(ImmutableSet.of("TheRegistrar")) + .setRenewalPriceBehavior(RenewalPriceBehavior.SPECIFIED) + .setDiscountFraction(1) + .build()); + PackagePromotion packagePromotion2 = + new PackagePromotion.Builder() + .setToken(token2) + .setMaxDomains(8) + .setMaxCreates(1) + .setPackagePrice(Money.of(CurrencyUnit.USD, 1000)) + .setNextBillingDate(DateTime.parse("2015-11-12T05:00:00Z")) + .build(); + jpaTm().transact(() -> jpaTm().put(packagePromotion2)); + + // Create limit is 1, creating 2 domains to go over the limit + persistEppResource( + DatabaseHelper.newDomain("foo.tld", contact) + .asBuilder() + .setCurrentPackageToken(token2.createVKey()) + .build()); + persistEppResource( + DatabaseHelper.newDomain("buzz.tld", contact) + .asBuilder() + .setCurrentPackageToken(token2.createVKey()) + .build()); + + action.run(); + assertAboutLogs() + .that(logHandler) + .hasLogAtLevelWithMessage(Level.INFO, "Found no packages over their create limit."); + } +}