diff --git a/core/src/main/java/google/registry/model/ofy/DatastoreTransactionManager.java b/core/src/main/java/google/registry/model/ofy/DatastoreTransactionManager.java index 1d006d403..a06ce5561 100644 --- a/core/src/main/java/google/registry/model/ofy/DatastoreTransactionManager.java +++ b/core/src/main/java/google/registry/model/ofy/DatastoreTransactionManager.java @@ -37,12 +37,17 @@ import google.registry.model.domain.DomainHistory; import google.registry.model.host.HostHistory; import google.registry.model.reporting.HistoryEntry; import google.registry.persistence.VKey; +import google.registry.persistence.transaction.QueryComposer; import google.registry.persistence.transaction.TransactionManager; +import java.util.List; import java.util.NoSuchElementException; import java.util.Optional; import java.util.function.Supplier; +import java.util.stream.Stream; import java.util.stream.StreamSupport; import javax.annotation.Nullable; +import javax.persistence.NoResultException; +import javax.persistence.NonUniqueResultException; import org.joda.time.DateTime; /** Datastore implementation of {@link TransactionManager}. */ @@ -302,6 +307,11 @@ public class DatastoreTransactionManager implements TransactionManager { syncIfTransactionless(getOfy().deleteWithoutBackup().entity(entity)); } + @Override + public QueryComposer createQueryComposer(Class entity) { + return new DatastoreQueryComposerImpl(entity); + } + @Override public void clearSessionCache() { getOfy().clearSessionCache(); @@ -363,4 +373,45 @@ public class DatastoreTransactionManager implements TransactionManager { } return obj; } + + private static class DatastoreQueryComposerImpl extends QueryComposer { + DatastoreQueryComposerImpl(Class entityClass) { + super(entityClass); + } + + Query buildQuery() { + Query result = ofy().load().type(entityClass); + for (WhereClause pred : predicates) { + result = result.filter(pred.fieldName + pred.comparator.getDatastoreString(), pred.value); + } + + if (orderBy != null) { + result = result.order(orderBy); + } + + return result; + } + + @Override + public Optional first() { + return Optional.ofNullable(buildQuery().first().now()); + } + + @Override + public T getSingleResult() { + List results = buildQuery().limit(2).list(); + if (results.size() == 0) { + // The exception text here is the same as what we get for JPA queries. + throw new NoResultException("No entity found for query"); + } else if (results.size() > 1) { + throw new NonUniqueResultException("More than one result found for getSingleResult query"); + } + return results.get(0); + } + + @Override + public Stream stream() { + return Streams.stream(buildQuery()); + } + } } diff --git a/core/src/main/java/google/registry/persistence/transaction/CriteriaQueryBuilder.java b/core/src/main/java/google/registry/persistence/transaction/CriteriaQueryBuilder.java index ec8cb39ec..c4a206103 100644 --- a/core/src/main/java/google/registry/persistence/transaction/CriteriaQueryBuilder.java +++ b/core/src/main/java/google/registry/persistence/transaction/CriteriaQueryBuilder.java @@ -18,6 +18,7 @@ import static google.registry.persistence.transaction.TransactionManagerFactory. import com.google.common.collect.ImmutableList; import java.util.Collection; +import javax.persistence.EntityManager; import javax.persistence.criteria.CriteriaBuilder; import javax.persistence.criteria.CriteriaQuery; import javax.persistence.criteria.Expression; @@ -35,7 +36,7 @@ import javax.persistence.criteria.Root; public class CriteriaQueryBuilder { /** Functional interface that defines the 'where' operator, e.g. {@link CriteriaBuilder#equal}. */ - public interface WhereClause { + public interface WhereOperator { Predicate predicate(Expression expression, U object); } @@ -50,7 +51,8 @@ public class CriteriaQueryBuilder { } /** Adds a WHERE clause to the query, given the specified operation, field, and value. */ - public CriteriaQueryBuilder where(String fieldName, WhereClause whereClause, V value) { + public CriteriaQueryBuilder where( + String fieldName, WhereOperator whereClause, V value) { Expression expression = root.get(fieldName); return where(whereClause.predicate(expression, value)); } @@ -94,7 +96,12 @@ public class CriteriaQueryBuilder { /** Creates a query builder that will SELECT from the given class. */ public static CriteriaQueryBuilder create(Class clazz) { - CriteriaQuery query = jpaTm().getEntityManager().getCriteriaBuilder().createQuery(clazz); + return create(jpaTm().getEntityManager(), clazz); + } + + /** Creates a query builder for the given entity manager. */ + public static CriteriaQueryBuilder create(EntityManager em, Class clazz) { + CriteriaQuery query = em.getCriteriaBuilder().createQuery(clazz); Root root = query.from(clazz); query = query.select(root); return new CriteriaQueryBuilder<>(query, root); diff --git a/core/src/main/java/google/registry/persistence/transaction/JpaTransactionManagerImpl.java b/core/src/main/java/google/registry/persistence/transaction/JpaTransactionManagerImpl.java index 82bea3b11..7c14557fb 100644 --- a/core/src/main/java/google/registry/persistence/transaction/JpaTransactionManagerImpl.java +++ b/core/src/main/java/google/registry/persistence/transaction/JpaTransactionManagerImpl.java @@ -43,10 +43,12 @@ import google.registry.util.Clock; import google.registry.util.Retrier; import google.registry.util.SystemSleeper; import java.lang.reflect.Field; +import java.util.List; import java.util.Map; import java.util.NoSuchElementException; import java.util.Optional; import java.util.function.Supplier; +import java.util.stream.Stream; import java.util.stream.StreamSupport; import javax.persistence.EntityManager; import javax.persistence.EntityManagerFactory; @@ -530,6 +532,11 @@ public class JpaTransactionManagerImpl implements JpaTransactionManager { delete(entity); } + @Override + public QueryComposer createQueryComposer(Class entity) { + return new JpaQueryComposerImpl(entity, getEntityManager()); + } + @Override public void clearSessionCache() { // This is an intended no-op method as there is no session cache in Postgresql. @@ -681,4 +688,44 @@ public class JpaTransactionManagerImpl implements JpaTransactionManager { } } } + + private static class JpaQueryComposerImpl extends QueryComposer { + + EntityManager em; + + JpaQueryComposerImpl(Class entityClass, EntityManager em) { + super(entityClass); + this.em = em; + } + + private TypedQuery buildQuery() { + CriteriaQueryBuilder queryBuilder = CriteriaQueryBuilder.create(em, entityClass); + + for (WhereClause pred : predicates) { + pred.addToCriteriaQueryBuilder(queryBuilder); + } + + if (orderBy != null) { + queryBuilder.orderByAsc(orderBy); + } + + return em.createQuery(queryBuilder.build()); + } + + @Override + public Optional first() { + List results = buildQuery().setMaxResults(1).getResultList(); + return results.size() > 0 ? Optional.of(results.get(0)) : Optional.empty(); + } + + @Override + public T getSingleResult() { + return buildQuery().getSingleResult(); + } + + @Override + public Stream stream() { + return buildQuery().getResultStream(); + } + } } diff --git a/core/src/main/java/google/registry/persistence/transaction/QueryComposer.java b/core/src/main/java/google/registry/persistence/transaction/QueryComposer.java new file mode 100644 index 000000000..2785bb2f1 --- /dev/null +++ b/core/src/main/java/google/registry/persistence/transaction/QueryComposer.java @@ -0,0 +1,190 @@ +// Copyright 2021 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.persistence.transaction; + +import static google.registry.persistence.transaction.TransactionManagerFactory.jpaTm; + +import com.google.common.base.Function; +import google.registry.persistence.transaction.CriteriaQueryBuilder.WhereOperator; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.stream.Stream; +import javax.annotation.Nullable; +import javax.persistence.criteria.CriteriaBuilder; + +/** + * Creates queries that can be used both for objectify and JPA. + * + *

Example usage: + * + *

+ *    tm().createQueryComposer(EntityType.class)
+ *        .where("fieldName", Comparator.EQ, "value")
+ *        .orderBy("fieldName")
+ *        .stream()
+ * 
+ */ +public abstract class QueryComposer { + + // The class whose entities we're querying. Note that this limits us to single table queries in + // SQL. In datastore, there's really no other kind of query. + protected Class entityClass; + + // Field to order by, if any. Null if we don't care about order. + @Nullable protected String orderBy; + + protected List> predicates = new ArrayList>(); + + protected QueryComposer(Class entityClass) { + this.entityClass = entityClass; + } + + /** + * Introduce a "where" clause to the query. + * + *

Causes the query to return only results where the field and value have the relationship + * specified by the comparator. For example, "field EQ value", "field GT value" etc. + */ + public > QueryComposer where( + String fieldName, Comparator comparator, U value) { + predicates.add(new WhereClause(fieldName, comparator, value)); + return this; + } + + /** + * Order the query results by the value of the specified field. + * + *

TODO(mmuller): add the ability to do descending sort order. + */ + public QueryComposer orderBy(String fieldName) { + orderBy = fieldName; + return this; + } + + /** Returns the first result of the query or an empty optional if there is none. */ + public abstract Optional first(); + + /** + * Returns the one and only result of a query. + * + *

Throws a {@link javax.persistence.NonUniqueResultException} if there is more than one + * result, throws {@link javax.persistence.NoResultException} if no results are found. + */ + public abstract T getSingleResult(); + + /** Returns the results of the query as a stream. */ + public abstract Stream stream(); + + // We have to wrap the CriteriaQueryBuilder predicate factories in our own functions because at + // the point where we pass them to the Comparator constructor, the compiler can't determine which + // of the overloads to use since there is no "value" object for context. + + public static > WhereOperator equal( + CriteriaBuilder criteriaBuilder) { + return criteriaBuilder::equal; + } + + public static > WhereOperator lessThan( + CriteriaBuilder criteriaBuilder) { + return criteriaBuilder::lessThan; + } + + public static > WhereOperator lessThanOrEqualTo( + CriteriaBuilder criteriaBuilder) { + return criteriaBuilder::lessThanOrEqualTo; + } + + public static > WhereOperator greaterThanOrEqualTo( + CriteriaBuilder criteriaBuilder) { + return criteriaBuilder::greaterThanOrEqualTo; + } + + public static > WhereOperator greaterThan( + CriteriaBuilder criteriaBuilder) { + return criteriaBuilder::greaterThan; + } + + /** + * Enum used to specify comparison operations, e.g. {@code where("fieldName", Comparator.NE, + * "someval")'}. + * + *

These contain values that specify the comparison behavior for both objectify and criteria + * queries. For objectify, we provide a string to be appended to the field name in a {@code + * filter()} expression. For criteria queries we provide a function that knows how to obtain a + * {@link WhereOperator} from a {@link CriteriaBuilder}. + * + *

Note that the objectify strings for comparators other than equality are preceded by a space + * because {@code filter()} expects the fieldname to be separated from the operator by a space. + */ + public enum Comparator { + /** + * Return only records whose field is equal to the value. + * + *

Note that the datastore string for this is empty, which is consistent with the way {@code + * filter()} works (it uses an unadorned field name to check for equality). + */ + EQ("", QueryComposer::equal), + + /** Return only records whose field is less than the value. */ + LT(" <", QueryComposer::lessThan), + + /** Return only records whose field is less than or equal to the value. */ + LTE(" <=", QueryComposer::lessThanOrEqualTo), + + /** Return only records whose field is greater than or equal to the value. */ + GTE(" >=", QueryComposer::greaterThanOrEqualTo), + + /** Return only records whose field is greater than the value. */ + GT(" >", QueryComposer::greaterThan); + + private final String datastoreString; + + @SuppressWarnings("ImmutableEnumChecker") // Functions are immutable. + private final Function> operatorFactory; + + Comparator( + String datastoreString, Function> operatorFactory) { + this.datastoreString = datastoreString; + this.operatorFactory = operatorFactory; + } + + public String getDatastoreString() { + return datastoreString; + } + + public Function> getComparisonFactory() { + return operatorFactory; + } + }; + + protected static class WhereClause> { + public String fieldName; + public Comparator comparator; + public U value; + + WhereClause(String fieldName, Comparator comparator, U value) { + this.fieldName = fieldName; + this.comparator = comparator; + this.value = value; + } + + public void addToCriteriaQueryBuilder(CriteriaQueryBuilder queryBuilder) { + CriteriaBuilder criteriaBuilder = jpaTm().getEntityManager().getCriteriaBuilder(); + queryBuilder.where( + fieldName, comparator.getComparisonFactory().apply(criteriaBuilder), value); + } + } +} diff --git a/core/src/main/java/google/registry/persistence/transaction/TransactionManager.java b/core/src/main/java/google/registry/persistence/transaction/TransactionManager.java index 5711af47f..06e7823cc 100644 --- a/core/src/main/java/google/registry/persistence/transaction/TransactionManager.java +++ b/core/src/main/java/google/registry/persistence/transaction/TransactionManager.java @@ -273,6 +273,9 @@ public interface TransactionManager { */ void deleteWithoutBackup(Object entity); + /** Returns a QueryComposer which can be used to perform queries against the current database. */ + QueryComposer createQueryComposer(Class entity); + /** Clears the session cache if the underlying database is Datastore, otherwise it is a no-op. */ void clearSessionCache(); diff --git a/core/src/main/java/google/registry/reporting/spec11/Spec11EmailUtils.java b/core/src/main/java/google/registry/reporting/spec11/Spec11EmailUtils.java index 0cb7783c7..47ce0cfec 100644 --- a/core/src/main/java/google/registry/reporting/spec11/Spec11EmailUtils.java +++ b/core/src/main/java/google/registry/reporting/spec11/Spec11EmailUtils.java @@ -17,7 +17,9 @@ package google.registry.reporting.spec11; import static com.google.common.base.Throwables.getRootCause; import static com.google.common.collect.ImmutableList.toImmutableList; import static com.google.common.io.Resources.getResource; -import static google.registry.model.ofy.ObjectifyService.ofy; +import static google.registry.persistence.transaction.QueryComposer.Comparator; +import static google.registry.persistence.transaction.TransactionManagerFactory.tm; +import static google.registry.persistence.transaction.TransactionManagerUtil.transactIfJpaTm; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; @@ -129,17 +131,20 @@ public class Spec11EmailUtils { private RegistrarThreatMatches filterOutNonPublishedMatches( RegistrarThreatMatches registrarThreatMatches) { ImmutableList filteredMatches = - registrarThreatMatches.threatMatches().stream() - .filter( - threatMatch -> - ofy() - .load() - .type(DomainBase.class) - .filter("fullyQualifiedDomainName", threatMatch.fullyQualifiedDomainName()) - .first() - .now() - .shouldPublishToDns()) - .collect(toImmutableList()); + transactIfJpaTm( + () -> { + return registrarThreatMatches.threatMatches().stream() + .filter( + threatMatch -> + tm().createQueryComposer(DomainBase.class) + .where( + "fullyQualifiedDomainName", + Comparator.EQ, + threatMatch.fullyQualifiedDomainName()) + .getSingleResult() + .shouldPublishToDns()) + .collect(toImmutableList()); + }); return RegistrarThreatMatches.create(registrarThreatMatches.clientId(), filteredMatches); } diff --git a/core/src/test/java/google/registry/persistence/transaction/QueryComposerTest.java b/core/src/test/java/google/registry/persistence/transaction/QueryComposerTest.java new file mode 100644 index 000000000..6c9ff31da --- /dev/null +++ b/core/src/test/java/google/registry/persistence/transaction/QueryComposerTest.java @@ -0,0 +1,269 @@ +// Copyright 2021 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.persistence.transaction; + +import static com.google.common.collect.ImmutableList.toImmutableList; +import static com.google.common.truth.Truth.assertThat; +import static google.registry.persistence.transaction.QueryComposer.Comparator; +import static google.registry.persistence.transaction.TransactionManagerFactory.tm; +import static google.registry.persistence.transaction.TransactionManagerUtil.transactIfJpaTm; +import static org.junit.Assert.assertThrows; + +import com.google.common.collect.ImmutableList; +import com.googlecode.objectify.annotation.Entity; +import com.googlecode.objectify.annotation.Id; +import com.googlecode.objectify.annotation.Index; +import google.registry.model.ImmutableObject; +import google.registry.testing.AppEngineExtension; +import google.registry.testing.DualDatabaseTest; +import google.registry.testing.FakeClock; +import google.registry.testing.TestOfyAndSql; +import java.util.Optional; +import javax.persistence.Column; +import javax.persistence.NoResultException; +import javax.persistence.NonUniqueResultException; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.extension.RegisterExtension; + +@DualDatabaseTest +public class QueryComposerTest { + + private final FakeClock fakeClock = new FakeClock(); + + TestEntity alpha = new TestEntity("alpha", 3); + TestEntity bravo = new TestEntity("bravo", 2); + TestEntity charlie = new TestEntity("charlie", 1); + + @RegisterExtension + public final AppEngineExtension appEngine = + AppEngineExtension.builder() + .withClock(fakeClock) + .withDatastoreAndCloudSql() + .withOfyTestEntities(TestEntity.class) + .withJpaUnitTestEntities(TestEntity.class) + .build(); + + public QueryComposerTest() {} + + @BeforeEach + void setUp() { + tm().transact( + () -> { + tm().insert(alpha); + tm().insert(bravo); + tm().insert(charlie); + }); + } + + @TestOfyAndSql + public void testFirstQueries() { + assertThat( + transactIfJpaTm( + () -> + tm().createQueryComposer(TestEntity.class) + .where("name", Comparator.EQ, "bravo") + .first() + .get())) + .isEqualTo(bravo); + assertThat( + transactIfJpaTm( + () -> + tm().createQueryComposer(TestEntity.class) + .where("name", Comparator.GT, "bravo") + .first() + .get())) + .isEqualTo(charlie); + assertThat( + transactIfJpaTm( + () -> + tm().createQueryComposer(TestEntity.class) + .where("name", Comparator.GTE, "charlie") + .first() + .get())) + .isEqualTo(charlie); + assertThat( + transactIfJpaTm( + () -> + tm().createQueryComposer(TestEntity.class) + .where("name", Comparator.LT, "bravo") + .first() + .get())) + .isEqualTo(alpha); + assertThat( + transactIfJpaTm( + () -> + tm().createQueryComposer(TestEntity.class) + .where("name", Comparator.LTE, "alpha") + .first() + .get())) + .isEqualTo(alpha); + } + + @TestOfyAndSql + public void testGetSingleResult() { + assertThat( + transactIfJpaTm( + () -> + tm().createQueryComposer(TestEntity.class) + .where("name", Comparator.EQ, "alpha") + .getSingleResult())) + .isEqualTo(alpha); + } + + @TestOfyAndSql + public void testGetSingleResult_noResults() { + assertThrows( + NoResultException.class, + () -> + transactIfJpaTm( + () -> + tm().createQueryComposer(TestEntity.class) + .where("name", Comparator.EQ, "ziggy") + .getSingleResult())); + } + + @TestOfyAndSql + public void testGetSingleResult_nonUniqueResult() { + assertThrows( + NonUniqueResultException.class, + () -> + transactIfJpaTm( + () -> + tm().createQueryComposer(TestEntity.class) + .where("name", Comparator.GT, "alpha") + .getSingleResult())); + } + + @TestOfyAndSql + public void testStreamQueries() { + assertThat( + transactIfJpaTm( + () -> + tm() + .createQueryComposer(TestEntity.class) + .where("name", Comparator.EQ, "alpha") + .stream() + .collect(toImmutableList()))) + .isEqualTo(ImmutableList.of(alpha)); + assertThat( + transactIfJpaTm( + () -> + tm() + .createQueryComposer(TestEntity.class) + .where("name", Comparator.GT, "alpha") + .stream() + .collect(toImmutableList()))) + .isEqualTo(ImmutableList.of(bravo, charlie)); + assertThat( + transactIfJpaTm( + () -> + tm() + .createQueryComposer(TestEntity.class) + .where("name", Comparator.GTE, "bravo") + .stream() + .collect(toImmutableList()))) + .isEqualTo(ImmutableList.of(bravo, charlie)); + assertThat( + transactIfJpaTm( + () -> + tm() + .createQueryComposer(TestEntity.class) + .where("name", Comparator.LT, "charlie") + .stream() + .collect(toImmutableList()))) + .isEqualTo(ImmutableList.of(alpha, bravo)); + assertThat( + transactIfJpaTm( + () -> + tm() + .createQueryComposer(TestEntity.class) + .where("name", Comparator.LTE, "bravo") + .stream() + .collect(toImmutableList()))) + .isEqualTo(ImmutableList.of(alpha, bravo)); + } + + @TestOfyAndSql + public void testNonPrimaryKey() { + assertThat( + transactIfJpaTm( + () -> + tm().createQueryComposer(TestEntity.class) + .where("val", Comparator.EQ, 2) + .first() + .get())) + .isEqualTo(bravo); + } + + @TestOfyAndSql + public void testOrderBy() { + assertThat( + transactIfJpaTm( + () -> + tm() + .createQueryComposer(TestEntity.class) + .where("val", Comparator.GT, 1) + .orderBy("val") + .stream() + .collect(toImmutableList()))) + .isEqualTo(ImmutableList.of(bravo, alpha)); + } + + @TestOfyAndSql + public void testEmptyQueries() { + assertThat( + transactIfJpaTm( + () -> + tm().createQueryComposer(TestEntity.class) + .where("name", Comparator.GT, "foxtrot") + .first())) + .isEqualTo(Optional.empty()); + assertThat( + transactIfJpaTm( + () -> + tm() + .createQueryComposer(TestEntity.class) + .where("name", Comparator.GT, "foxtrot") + .stream() + .collect(toImmutableList()))) + .isEqualTo(ImmutableList.of()); + } + + @javax.persistence.Entity + @Entity(name = "QueryComposerTestEntity") + private static class TestEntity extends ImmutableObject { + @javax.persistence.Id @Id private String name; + + @Index + // Renaming this implicitly verifies that property names work for hibernate queries. + @Column(name = "some_value") + private int val; + + public TestEntity() {} + + public TestEntity(String name, int val) { + this.name = name; + this.val = val; + } + + public int getVal() { + return val; + } + + public String getName() { + return name; + } + } +} diff --git a/core/src/test/java/google/registry/reporting/spec11/Spec11EmailUtilsTest.java b/core/src/test/java/google/registry/reporting/spec11/Spec11EmailUtilsTest.java index c778b3f43..28014304a 100644 --- a/core/src/test/java/google/registry/reporting/spec11/Spec11EmailUtilsTest.java +++ b/core/src/test/java/google/registry/reporting/spec11/Spec11EmailUtilsTest.java @@ -17,11 +17,11 @@ package google.registry.reporting.spec11; import static com.google.common.truth.Truth.assertThat; import static google.registry.model.eppcommon.StatusValue.CLIENT_HOLD; import static google.registry.model.eppcommon.StatusValue.SERVER_HOLD; -import static google.registry.model.ofy.ObjectifyService.ofy; import static google.registry.reporting.spec11.Spec11RegistrarThreatMatchesParserTest.getMatchA; import static google.registry.reporting.spec11.Spec11RegistrarThreatMatchesParserTest.getMatchB; import static google.registry.reporting.spec11.Spec11RegistrarThreatMatchesParserTest.sampleThreatMatches; import static google.registry.testing.DatabaseHelper.createTld; +import static google.registry.testing.DatabaseHelper.loadByEntity; import static google.registry.testing.DatabaseHelper.newDomainBase; import static google.registry.testing.DatabaseHelper.persistActiveHost; import static google.registry.testing.DatabaseHelper.persistResource; @@ -39,6 +39,8 @@ import google.registry.model.domain.DomainBase; import google.registry.model.host.HostResource; import google.registry.reporting.spec11.soy.Spec11EmailSoyInfo; import google.registry.testing.AppEngineExtension; +import google.registry.testing.DualDatabaseTest; +import google.registry.testing.TestOfyAndSql; import google.registry.util.EmailMessage; import google.registry.util.SendEmailService; import java.util.LinkedHashSet; @@ -48,11 +50,11 @@ import javax.mail.MessagingException; import javax.mail.internet.InternetAddress; import org.joda.time.LocalDate; import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; import org.mockito.ArgumentCaptor; /** Unit tests for {@link Spec11EmailUtils}. */ +@DualDatabaseTest class Spec11EmailUtilsTest { private static final ImmutableList FAKE_RESOURCES = ImmutableList.of("foo"); @@ -128,7 +130,7 @@ class Spec11EmailUtilsTest { persistDomainWithHost("c.com", host); } - @Test + @TestOfyAndSql void testSuccess_emailMonthlySpec11Reports() throws Exception { emailUtils.emailSpec11Reports( date, @@ -166,7 +168,7 @@ class Spec11EmailUtilsTest { Optional.empty()); } - @Test + @TestOfyAndSql void testSuccess_emailDailySpec11Reports() throws Exception { emailUtils.emailSpec11Reports( date, @@ -204,13 +206,11 @@ class Spec11EmailUtilsTest { Optional.empty()); } - @Test + @TestOfyAndSql void testSuccess_skipsInactiveDomain() throws Exception { // CLIENT_HOLD and SERVER_HOLD mean no DNS so we don't need to email it out - persistResource( - ofy().load().entity(aDomain).now().asBuilder().addStatusValue(SERVER_HOLD).build()); - persistResource( - ofy().load().entity(bDomain).now().asBuilder().addStatusValue(CLIENT_HOLD).build()); + persistResource(loadByEntity(aDomain).asBuilder().addStatusValue(SERVER_HOLD).build()); + persistResource(loadByEntity(bDomain).asBuilder().addStatusValue(CLIENT_HOLD).build()); emailUtils.emailSpec11Reports( date, Spec11EmailSoyInfo.MONTHLY_SPEC_11_EMAIL, @@ -237,7 +237,7 @@ class Spec11EmailUtilsTest { Optional.empty()); } - @Test + @TestOfyAndSql void testOneFailure_sendsAlert() throws Exception { // If there is one failure, we should still send the other message and then an alert email LinkedHashSet matches = new LinkedHashSet<>(); @@ -292,7 +292,7 @@ class Spec11EmailUtilsTest { Optional.empty()); } - @Test + @TestOfyAndSql void testSuccess_sendAlertEmail() throws Exception { emailUtils.sendAlertEmail("Spec11 Pipeline Alert: 2018-07", "Alert!"); verify(emailService).sendEmail(contentCaptor.capture()); @@ -306,7 +306,7 @@ class Spec11EmailUtilsTest { Optional.empty()); } - @Test + @TestOfyAndSql void testSuccess_useWhoisAbuseEmailIfAvailable() throws Exception { // if John Doe is the whois abuse contact, email them instead of the regular email persistResource( @@ -325,7 +325,7 @@ class Spec11EmailUtilsTest { .containsExactly(new InternetAddress("johndoe@theregistrar.com")); } - @Test + @TestOfyAndSql void testFailure_badClientId() { RuntimeException thrown = assertThrows(