Implement query abstraction (#1069)

* Implement query abstraction

Implement a query abstraction layer ("QueryComposer") that allows us to
construct fluent-style queries that work across both Objectify and JPA.

As a demonstration of the concept, convert Spec11EmailUtils and its test to
use the new API.

Limitations:
-  The primary limitations of this system are imposed by datastore, for
   example all queryable fields must be indexed, orderBy must coincide with
   the order of any inequality queries, inequality filters are limited to one
   property...
-  JPA queries are limited to a set of where clauses (all of which must match)
   and an "order by" clause.  Joins, functions, complex where logic and
   multi-table queries are simply not allowed.
-  Descending sort order is currently unsupported (this is simple enough to
   add).
This commit is contained in:
Michael Muller 2021-04-16 12:21:03 -04:00 committed by GitHub
parent bc2a5dbc02
commit 1c96cd64fe
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 600 additions and 28 deletions

View file

@ -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 <T> QueryComposer<T> createQueryComposer(Class<T> 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<T> extends QueryComposer<T> {
DatastoreQueryComposerImpl(Class<T> entityClass) {
super(entityClass);
}
Query<T> buildQuery() {
Query<T> 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<T> first() {
return Optional.ofNullable(buildQuery().first().now());
}
@Override
public T getSingleResult() {
List<T> 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<T> stream() {
return Streams.stream(buildQuery());
}
}
}

View file

@ -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<T> {
/** Functional interface that defines the 'where' operator, e.g. {@link CriteriaBuilder#equal}. */
public interface WhereClause<U> {
public interface WhereOperator<U> {
Predicate predicate(Expression<U> expression, U object);
}
@ -50,7 +51,8 @@ public class CriteriaQueryBuilder<T> {
}
/** Adds a WHERE clause to the query, given the specified operation, field, and value. */
public <V> CriteriaQueryBuilder<T> where(String fieldName, WhereClause<V> whereClause, V value) {
public <V> CriteriaQueryBuilder<T> where(
String fieldName, WhereOperator<V> whereClause, V value) {
Expression<V> expression = root.get(fieldName);
return where(whereClause.predicate(expression, value));
}
@ -94,7 +96,12 @@ public class CriteriaQueryBuilder<T> {
/** Creates a query builder that will SELECT from the given class. */
public static <T> CriteriaQueryBuilder<T> create(Class<T> clazz) {
CriteriaQuery<T> query = jpaTm().getEntityManager().getCriteriaBuilder().createQuery(clazz);
return create(jpaTm().getEntityManager(), clazz);
}
/** Creates a query builder for the given entity manager. */
public static <T> CriteriaQueryBuilder<T> create(EntityManager em, Class<T> clazz) {
CriteriaQuery<T> query = em.getCriteriaBuilder().createQuery(clazz);
Root<T> root = query.from(clazz);
query = query.select(root);
return new CriteriaQueryBuilder<>(query, root);

View file

@ -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 <T> QueryComposer<T> createQueryComposer(Class<T> entity) {
return new JpaQueryComposerImpl<T>(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<T> extends QueryComposer<T> {
EntityManager em;
JpaQueryComposerImpl(Class<T> entityClass, EntityManager em) {
super(entityClass);
this.em = em;
}
private TypedQuery<T> buildQuery() {
CriteriaQueryBuilder<T> 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<T> first() {
List<T> 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<T> stream() {
return buildQuery().getResultStream();
}
}
}

View file

@ -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.
*
* <p>Example usage:
*
* <pre>
* tm().createQueryComposer(EntityType.class)
* .where("fieldName", Comparator.EQ, "value")
* .orderBy("fieldName")
* .stream()
* </pre>
*/
public abstract class QueryComposer<T> {
// 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<T> entityClass;
// Field to order by, if any. Null if we don't care about order.
@Nullable protected String orderBy;
protected List<WhereClause<?>> predicates = new ArrayList<WhereClause<?>>();
protected QueryComposer(Class<T> entityClass) {
this.entityClass = entityClass;
}
/**
* Introduce a "where" clause to the query.
*
* <p>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 <U extends Comparable<? super U>> QueryComposer<T> 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.
*
* <p>TODO(mmuller): add the ability to do descending sort order.
*/
public QueryComposer<T> 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<T> first();
/**
* Returns the one and only result of a query.
*
* <p>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<T> 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 <U extends Comparable<? super U>> WhereOperator<U> equal(
CriteriaBuilder criteriaBuilder) {
return criteriaBuilder::equal;
}
public static <U extends Comparable<? super U>> WhereOperator<U> lessThan(
CriteriaBuilder criteriaBuilder) {
return criteriaBuilder::lessThan;
}
public static <U extends Comparable<? super U>> WhereOperator<U> lessThanOrEqualTo(
CriteriaBuilder criteriaBuilder) {
return criteriaBuilder::lessThanOrEqualTo;
}
public static <U extends Comparable<? super U>> WhereOperator<U> greaterThanOrEqualTo(
CriteriaBuilder criteriaBuilder) {
return criteriaBuilder::greaterThanOrEqualTo;
}
public static <U extends Comparable<? super U>> WhereOperator<U> greaterThan(
CriteriaBuilder criteriaBuilder) {
return criteriaBuilder::greaterThan;
}
/**
* Enum used to specify comparison operations, e.g. {@code where("fieldName", Comparator.NE,
* "someval")'}.
*
* <p>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}.
*
* <p>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.
*
* <p>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<CriteriaBuilder, WhereOperator<?>> operatorFactory;
Comparator(
String datastoreString, Function<CriteriaBuilder, WhereOperator<?>> operatorFactory) {
this.datastoreString = datastoreString;
this.operatorFactory = operatorFactory;
}
public String getDatastoreString() {
return datastoreString;
}
public Function<CriteriaBuilder, WhereOperator<?>> getComparisonFactory() {
return operatorFactory;
}
};
protected static class WhereClause<U extends Comparable<? super U>> {
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);
}
}
}

View file

@ -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. */
<T> QueryComposer<T> createQueryComposer(Class<T> entity);
/** Clears the session cache if the underlying database is Datastore, otherwise it is a no-op. */
void clearSessionCache();

View file

@ -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<ThreatMatch> 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);
}

View file

@ -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;
}
}
}

View file

@ -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<String> 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<RegistrarThreatMatches> 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(