Compare commits

...
Author SHA1 Message Date
Weimin YuandGitHub 1f4cf5bdb6 Fix flyway invocation in Gradle script (#993)
* Fix flyway invocation in Gradle script

Script wrongly assumed that Flyway task is invoked if --environment is
set.

Bug was introduced in go/r3pr/940
2021-03-08 13:59:28 -05:00
gbrodmanandGitHub 4176f7dd9c Convert DomainTransferRejectFlow to use tm() methods (#977)
* Convert DomainTransferRejectFlow to use tm() methods

This change includes a few other necessary dependencies to converting
DomainTransferRejectFlowTest to be a dual-database test. Namely:

- The basic "use tm() instead of ofy()" and "branching database
selection on what were previously raw ofy queries"
- Modification of the PollMessage convertVKey methods to do what they
say they do
- Filling the generic pending / response fields in PollMessage based on what type of
poll message it is (this has to be done because SQL is not very good at
storing ambiguous superclasses)
- Setting the generic pending / repsonse fields in PollMessage upon
build
- Filling out the serverApproveEntities field in DomainTransferData with
all necessary poll messages / billing events that should be cancelled on
rejection
- Scattered changes in DatabaseHelper to make sure that we're saving and
loading entities correctly where we weren't before
2021-03-08 13:24:30 -05:00
Michael MullerandGitHub e07139665e Disable whois caching in nomulus tool (#980)
* Disable whois caching in nomulus tool

The whois commands previously served output generated from cached EppResource
objects in most cases.  While this is entirely appropriate from server-side,
it is less useful when these commands are run from nomulus tool and, in fact,
when run from the "shell" command this results in changes that have been
applied from the shell not being visible from a subsequent "whois".  The
command may instead serve information on an earlier, cached version of the
resource instead of the latest version.

This implementation uses dagger for parameterization of cached/non-cached
modes.  I did consider the possibility of simply parameterizing the query
commands in all cases as discussed, however, having gone down the
daggerization path and having gotten it to work, I have to say I find this
approach preferrable.  There's really no case for identifying
cached/non-cached on a per-command basis and doing so would require
propagating the flag throughout all levels of the API and all callsites.

Tested: In addition to the new unit test which explicitly verifies the
caching/noncaching behavior of the new commands, tested the actual failing
sequence from "nomulus -e sandbox shell" and verified that the correct results
are served after a mutation.

* Fixed copyright year

* More copyright date fixes

* Added WhoisCommandFactoryTest to fragile tests

I suspect that this test has some contention with other tests, it's not clear
why.
2021-03-08 12:33:23 -05:00
30 changed files with 740 additions and 262 deletions
+3 -2
View File
@@ -111,8 +111,9 @@ PROPERTIES = [
# Cloud SQL properties
Property('dbServer',
'The host[:port] of a database that accepts direct IP access. '
'This is typically used with a testing database.'),
'Sets the target database of a Flyway task. This may be a '
'registry environment name (e.g., alpha) or the host[:port] '
'of a database that accepts direct IP access.'),
Property('dbName',
'Database name to use in connection.',
'postgres'),
+3
View File
@@ -76,6 +76,9 @@ def fragileTestPatterns = [
"google/registry/model/tmch/ClaimsListShardTest.*",
// Creates large object (64MBytes), occasionally throws OOM error.
"google/registry/model/server/KmsSecretRevisionTest.*",
// Changes cache timeouts and for some reason appears to have contention
// with other tests.
"google/registry/whois/WhoisCommandFactoryTest.*",
] + dockerIncompatibleTestPatterns
sourceSets {
@@ -18,12 +18,11 @@ import static com.google.common.collect.Sets.intersection;
import static google.registry.model.EppResourceUtils.getLinkedDomainKeys;
import static google.registry.model.EppResourceUtils.loadByForeignKey;
import static google.registry.model.index.ForeignKeyIndex.loadAndGetKey;
import static google.registry.model.ofy.ObjectifyService.ofy;
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
import static google.registry.persistence.transaction.TransactionManagerUtil.transactIfJpaTm;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Sets;
import com.googlecode.objectify.Key;
import google.registry.flows.EppException.AuthorizationErrorException;
import google.registry.flows.EppException.InvalidAuthorizationInformationErrorException;
import google.registry.flows.EppException.ObjectDoesNotExistException;
@@ -185,16 +184,15 @@ public final class ResourceFlowUtils {
return;
}
// The roid should match one of the contacts.
Optional<Key<ContactResource>> foundContact =
Optional<VKey<ContactResource>> foundContact =
domain.getReferencedContacts().stream()
.map(VKey::getOfyKey)
.filter(key -> key.getName().equals(authRepoId))
.filter(key -> key.getOfyKey().getName().equals(authRepoId))
.findFirst();
if (!foundContact.isPresent()) {
throw new BadAuthInfoForResourceException();
}
// Check the authInfo against the contact.
verifyAuthInfo(authInfo, ofy().load().key(foundContact.get()).now());
verifyAuthInfo(authInfo, transactIfJpaTm(() -> tm().loadByKey(foundContact.get())));
}
/** Check that the given {@link AuthInfo} is valid for the given contact. */
@@ -25,6 +25,9 @@ import static com.google.common.collect.Iterables.any;
import static com.google.common.collect.Sets.difference;
import static com.google.common.collect.Sets.intersection;
import static com.google.common.collect.Sets.union;
import static google.registry.model.DatabaseMigrationUtils.getPrimaryDatabase;
import static google.registry.model.common.DatabaseTransitionSchedule.PrimaryDatabase.DATASTORE;
import static google.registry.model.common.DatabaseTransitionSchedule.TransitionId.REPLAYED_ENTITIES;
import static google.registry.model.domain.DomainBase.MAX_REGISTRATION_YEARS;
import static google.registry.model.ofy.ObjectifyService.ofy;
import static google.registry.model.registry.Registries.findTldForName;
@@ -38,6 +41,7 @@ import static google.registry.model.registry.label.ReservationType.FULLY_BLOCKED
import static google.registry.model.registry.label.ReservationType.NAME_COLLISION;
import static google.registry.model.registry.label.ReservationType.RESERVED_FOR_ANCHOR_TENANT;
import static google.registry.model.registry.label.ReservationType.RESERVED_FOR_SPECIFIC_USE;
import static google.registry.persistence.transaction.TransactionManagerFactory.jpaTm;
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
import static google.registry.pricing.PricingEngineProxy.isDomainPremium;
import static google.registry.util.CollectionUtils.nullToEmpty;
@@ -88,6 +92,7 @@ import google.registry.model.domain.DomainCommand.Create;
import google.registry.model.domain.DomainCommand.CreateOrUpdate;
import google.registry.model.domain.DomainCommand.InvalidReferencesException;
import google.registry.model.domain.DomainCommand.Update;
import google.registry.model.domain.DomainHistory;
import google.registry.model.domain.ForeignKeyedDesignatedContact;
import google.registry.model.domain.Period;
import google.registry.model.domain.fee.BaseFee;
@@ -355,22 +360,23 @@ public class DomainFlowUtils {
static void validateNoDuplicateContacts(Set<DesignatedContact> contacts)
throws ParameterValuePolicyErrorException {
ImmutableMultimap<Type, Key<ContactResource>> contactsByType =
ImmutableMultimap<Type, VKey<ContactResource>> contactsByType =
contacts.stream()
.collect(
toImmutableSetMultimap(
DesignatedContact::getType, contact -> contact.getContactKey().getOfyKey()));
DesignatedContact::getType, contact -> contact.getContactKey()));
// If any contact type has multiple contacts:
if (contactsByType.asMap().values().stream().anyMatch(v -> v.size() > 1)) {
// Find the duplicates.
Map<Type, Collection<Key<ContactResource>>> dupeKeysMap =
Map<Type, Collection<VKey<ContactResource>>> dupeKeysMap =
Maps.filterEntries(contactsByType.asMap(), e -> e.getValue().size() > 1);
ImmutableList<Key<ContactResource>> dupeKeys =
ImmutableList<VKey<ContactResource>> dupeKeys =
dupeKeysMap.values().stream().flatMap(Collection::stream).collect(toImmutableList());
// Load the duplicates in one batch.
Map<Key<ContactResource>, ContactResource> dupeContacts = ofy().load().keys(dupeKeys);
ImmutableMultimap.Builder<Type, Key<ContactResource>> typesMap =
Map<VKey<? extends ContactResource>, ContactResource> dupeContacts =
tm().loadByKeys(dupeKeys);
ImmutableMultimap.Builder<Type, VKey<ContactResource>> typesMap =
new ImmutableMultimap.Builder<>();
dupeKeysMap.forEach(typesMap::putAll);
// Create an error message showing the type and contact IDs of the duplicates.
@@ -537,13 +543,13 @@ public class DomainFlowUtils {
// If the resultant autorenew poll message would have no poll messages to deliver, then just
// delete it. Otherwise save it with the new end time.
if (isAtOrAfter(updatedAutorenewPollMessage.getEventTime(), newEndTime)) {
autorenewPollMessage.ifPresent(autorenew -> ofy().delete().entity(autorenew));
autorenewPollMessage.ifPresent(autorenew -> tm().delete(autorenew));
} else {
ofy().save().entity(updatedAutorenewPollMessage);
tm().put(updatedAutorenewPollMessage);
}
Recurring recurring = tm().loadByKey(domain.getAutorenewBillingEvent());
ofy().save().entity(recurring.asBuilder().setRecurrenceEndTime(newEndTime).build());
tm().put(recurring.asBuilder().setRecurrenceEndTime(newEndTime).build());
}
/**
@@ -1045,18 +1051,11 @@ public class DomainFlowUtils {
Duration maxSearchPeriod,
final ImmutableSet<TransactionReportField> cancelableFields) {
List<HistoryEntry> recentHistoryEntries =
ofy()
.load()
.type(HistoryEntry.class)
.ancestor(domainBase)
.filter("modificationTime >=", now.minus(maxSearchPeriod))
.order("modificationTime")
.list();
Optional<HistoryEntry> entryToCancel =
List<? extends HistoryEntry> recentHistoryEntries =
findRecentHistoryEntries(domainBase, now, maxSearchPeriod);
Optional<? extends HistoryEntry> entryToCancel =
Streams.findLast(
recentHistoryEntries
.stream()
recentHistoryEntries.stream()
.filter(
historyEntry -> {
// Look for add and renew transaction records that have yet to be reported
@@ -1082,6 +1081,28 @@ public class DomainFlowUtils {
return recordsBuilder.build();
}
private static List<? extends HistoryEntry> findRecentHistoryEntries(
DomainBase domainBase, DateTime now, Duration maxSearchPeriod) {
if (getPrimaryDatabase(REPLAYED_ENTITIES).equals(DATASTORE)) {
return ofy()
.load()
.type(HistoryEntry.class)
.ancestor(domainBase)
.filter("modificationTime >=", now.minus(maxSearchPeriod))
.order("modificationTime")
.list();
} else {
return jpaTm()
.getEntityManager()
.createQuery(
"FROM DomainHistory WHERE modificationTime >= :beginning "
+ "ORDER BY modificationTime ASC",
DomainHistory.class)
.setParameter("beginning", now.minus(maxSearchPeriod))
.getResultList();
}
}
/** Resource linked to this domain does not exist. */
static class LinkedResourcesDoNotExistException extends ObjectDoesNotExistException {
public LinkedResourcesDoNotExistException(Class<?> type, ImmutableSet<String> resourceIds) {
@@ -25,7 +25,6 @@ import static google.registry.flows.domain.DomainFlowUtils.updateAutorenewRecurr
import static google.registry.flows.domain.DomainTransferUtils.createGainingTransferPollMessage;
import static google.registry.flows.domain.DomainTransferUtils.createTransferResponse;
import static google.registry.model.ResourceTransferUtils.denyPendingTransfer;
import static google.registry.model.ofy.ObjectifyService.ofy;
import static google.registry.model.reporting.DomainTransactionRecord.TransactionReportField.TRANSFER_NACKED;
import static google.registry.model.reporting.DomainTransactionRecord.TransactionReportField.TRANSFER_SUCCESSFUL;
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
@@ -41,7 +40,6 @@ import google.registry.flows.FlowModule.Superuser;
import google.registry.flows.FlowModule.TargetId;
import google.registry.flows.TransactionalFlow;
import google.registry.flows.annotations.ReportingSpec;
import google.registry.model.ImmutableObject;
import google.registry.model.domain.DomainBase;
import google.registry.model.domain.metadata.MetadataExtension;
import google.registry.model.eppcommon.AuthInfo;
@@ -102,11 +100,11 @@ public final class DomainTransferRejectFlow implements TransactionalFlow {
}
DomainBase newDomain =
denyPendingTransfer(existingDomain, TransferStatus.CLIENT_REJECTED, now, clientId);
ofy().save().<ImmutableObject>entities(
newDomain,
historyEntry,
createGainingTransferPollMessage(
targetId, newDomain.getTransferData(), null, historyEntry));
tm().putAll(
newDomain,
historyEntry,
createGainingTransferPollMessage(
targetId, newDomain.getTransferData(), null, historyEntry));
// Reopen the autorenew event and poll message that we closed for the implicit transfer. This
// may end up recreating the poll message if it was deleted upon the transfer request.
updateAutorenewRecurrenceEndTime(existingDomain, END_OF_TIME);
@@ -118,9 +118,7 @@ public final class ResourceTransferUtils {
if (resource.getStatusValues().contains(StatusValue.PENDING_TRANSFER)) {
TransferData oldTransferData = resource.getTransferData();
tm().delete(oldTransferData.getServerApproveEntities());
ofy()
.save()
.entity(
tm().put(
new PollMessage.OneTime.Builder()
.setClientId(oldTransferData.getGainingClientId())
.setEventTime(now)
@@ -131,6 +131,11 @@ public class DatastoreTransactionManager implements TransactionManager {
saveEntity(entity);
}
@Override
public void putAll(Object... entities) {
syncIfTransactionless(getOfy().save().entities(entities));
}
@Override
public void putAll(ImmutableCollection<?> entities) {
syncIfTransactionless(getOfy().save().entities(entities));
@@ -359,8 +359,13 @@ public abstract class PollMessage extends ImmutableObject
}
/** Converts an unspecialized VKey&lt;PollMessage&gt; to a VKey of the derived class. */
public static @Nullable VKey<OneTime> convertVKey(@Nullable VKey<OneTime> key) {
return key == null ? null : VKey.create(OneTime.class, key.getSqlKey(), key.getOfyKey());
public static @Nullable VKey<OneTime> convertVKey(@Nullable VKey<? extends PollMessage> key) {
if (key == null) {
return null;
}
Key<OneTime> ofyKey =
Key.create(key.getOfyKey().getParent(), OneTime.class, key.getOfyKey().getId());
return VKey.create(OneTime.class, key.getSqlKey(), ofyKey);
}
@Override
@@ -383,6 +388,7 @@ public abstract class PollMessage extends ImmutableObject
@OnLoad
void onLoad() {
super.onLoad();
// Take the Objectify-specific fields and map them to the SQL-specific fields, if applicable
if (!isNullOrEmpty(contactPendingActionNotificationResponses)) {
pendingActionNotificationResponse = contactPendingActionNotificationResponses.get(0);
}
@@ -390,36 +396,70 @@ public abstract class PollMessage extends ImmutableObject
contactId = contactTransferResponses.get(0).getContactId();
transferResponse = contactTransferResponses.get(0);
}
if (!isNullOrEmpty(domainPendingActionNotificationResponses)) {
pendingActionNotificationResponse = domainPendingActionNotificationResponses.get(0);
}
if (!isNullOrEmpty(domainTransferResponses)) {
fullyQualifiedDomainName = domainTransferResponses.get(0).getFullyQualifiedDomainName();
transferResponse = domainTransferResponses.get(0);
extendedRegistrationExpirationTime =
domainTransferResponses.get(0).getExtendedRegistrationExpirationTime();
}
}
@Override
@PostLoad
void postLoad() {
super.postLoad();
// Take the SQL-specific fields and map them to the Objectify-specific fields, if applicable
if (pendingActionNotificationResponse != null) {
contactPendingActionNotificationResponses =
ImmutableList.of(
ContactPendingActionNotificationResponse.create(
pendingActionNotificationResponse.nameOrId.value,
pendingActionNotificationResponse.getActionResult(),
pendingActionNotificationResponse.getTrid(),
pendingActionNotificationResponse.processedDate));
if (contactId != null) {
contactPendingActionNotificationResponses =
ImmutableList.of(
ContactPendingActionNotificationResponse.create(
pendingActionNotificationResponse.nameOrId.value,
pendingActionNotificationResponse.getActionResult(),
pendingActionNotificationResponse.getTrid(),
pendingActionNotificationResponse.processedDate));
} else if (fullyQualifiedDomainName != null) {
domainPendingActionNotificationResponses =
ImmutableList.of(
DomainPendingActionNotificationResponse.create(
pendingActionNotificationResponse.nameOrId.value,
pendingActionNotificationResponse.getActionResult(),
pendingActionNotificationResponse.getTrid(),
pendingActionNotificationResponse.processedDate));
}
}
if (contactId != null && transferResponse != null) {
// The transferResponse is currently an unspecialized TransferResponse instance, create a
// ContactTransferResponse so that the value is consistently specialized and store it in the
// list representation for datastore.
transferResponse =
new ContactTransferResponse.Builder()
.setContactId(contactId)
.setGainingClientId(transferResponse.getGainingClientId())
.setLosingClientId(transferResponse.getLosingClientId())
.setTransferStatus(transferResponse.getTransferStatus())
.setTransferRequestTime(transferResponse.getTransferRequestTime())
.setPendingTransferExpirationTime(
transferResponse.getPendingTransferExpirationTime())
.build();
contactTransferResponses = ImmutableList.of((ContactTransferResponse) transferResponse);
if (transferResponse != null) {
// The transferResponse is currently an unspecialized TransferResponse instance, create the
// appropriate subclass so that the value is consistently specialized
if (contactId != null) {
transferResponse =
new ContactTransferResponse.Builder()
.setContactId(contactId)
.setGainingClientId(transferResponse.getGainingClientId())
.setLosingClientId(transferResponse.getLosingClientId())
.setTransferStatus(transferResponse.getTransferStatus())
.setTransferRequestTime(transferResponse.getTransferRequestTime())
.setPendingTransferExpirationTime(
transferResponse.getPendingTransferExpirationTime())
.build();
contactTransferResponses = ImmutableList.of((ContactTransferResponse) transferResponse);
} else if (fullyQualifiedDomainName != null) {
transferResponse =
new DomainTransferResponse.Builder()
.setFullyQualifiedDomainName(fullyQualifiedDomainName)
.setGainingClientId(transferResponse.getGainingClientId())
.setLosingClientId(transferResponse.getLosingClientId())
.setTransferStatus(transferResponse.getTransferStatus())
.setTransferRequestTime(transferResponse.getTransferRequestTime())
.setPendingTransferExpirationTime(
transferResponse.getPendingTransferExpirationTime())
.setExtendedRegistrationExpirationTime(extendedRegistrationExpirationTime)
.build();
domainTransferResponses = ImmutableList.of((DomainTransferResponse) transferResponse);
}
}
}
@@ -441,10 +481,7 @@ public abstract class PollMessage extends ImmutableObject
.filter(ContactPendingActionNotificationResponse.class::isInstance)
.map(ContactPendingActionNotificationResponse.class::cast)
.collect(toImmutableList()));
if (getInstance().contactPendingActionNotificationResponses != null) {
getInstance().pendingActionNotificationResponse =
getInstance().contactPendingActionNotificationResponses.get(0);
}
getInstance().contactTransferResponses =
forceEmptyToNull(
responseData
@@ -452,10 +489,6 @@ public abstract class PollMessage extends ImmutableObject
.filter(ContactTransferResponse.class::isInstance)
.map(ContactTransferResponse.class::cast)
.collect(toImmutableList()));
if (getInstance().contactTransferResponses != null) {
getInstance().contactId = getInstance().contactTransferResponses.get(0).getContactId();
getInstance().transferResponse = getInstance().contactTransferResponses.get(0);
}
getInstance().domainPendingActionNotificationResponses =
forceEmptyToNull(
@@ -471,13 +504,36 @@ public abstract class PollMessage extends ImmutableObject
.filter(DomainTransferResponse.class::isInstance)
.map(DomainTransferResponse.class::cast)
.collect(toImmutableList()));
getInstance().hostPendingActionNotificationResponses =
forceEmptyToNull(
responseData
.stream()
responseData.stream()
.filter(HostPendingActionNotificationResponse.class::isInstance)
.map(HostPendingActionNotificationResponse.class::cast)
.collect(toImmutableList()));
// Set the generic pending-action field as appropriate
if (getInstance().contactPendingActionNotificationResponses != null) {
getInstance().pendingActionNotificationResponse =
getInstance().contactPendingActionNotificationResponses.get(0);
} else if (getInstance().domainPendingActionNotificationResponses != null) {
getInstance().pendingActionNotificationResponse =
getInstance().domainPendingActionNotificationResponses.get(0);
} else if (getInstance().hostPendingActionNotificationResponses != null) {
getInstance().pendingActionNotificationResponse =
getInstance().hostPendingActionNotificationResponses.get(0);
}
// Set the generic transfer response field as appropriate
if (getInstance().contactTransferResponses != null) {
getInstance().contactId = getInstance().contactTransferResponses.get(0).getContactId();
getInstance().transferResponse = getInstance().contactTransferResponses.get(0);
} else if (getInstance().domainTransferResponses != null) {
getInstance().fullyQualifiedDomainName =
getInstance().domainTransferResponses.get(0).getFullyQualifiedDomainName();
getInstance().transferResponse = getInstance().domainTransferResponses.get(0);
getInstance().extendedRegistrationExpirationTime =
getInstance().domainTransferResponses.get(0).getExtendedRegistrationExpirationTime();
}
return this;
}
}
@@ -518,8 +574,13 @@ public abstract class PollMessage extends ImmutableObject
}
/** Converts an unspecialized VKey&lt;PollMessage&gt; to a VKey of the derived class. */
public static @Nullable VKey<Autorenew> convertVKey(VKey<Autorenew> key) {
return key == null ? null : VKey.create(Autorenew.class, key.getSqlKey(), key.getOfyKey());
public static @Nullable VKey<Autorenew> convertVKey(VKey<? extends PollMessage> key) {
if (key == null) {
return null;
}
Key<Autorenew> ofyKey =
Key.create(key.getOfyKey().getParent(), Autorenew.class, key.getOfyKey().getId());
return VKey.create(Autorenew.class, key.getSqlKey(), ofyKey);
}
@Override
@@ -14,7 +14,10 @@
package google.registry.model.transfer;
import static google.registry.util.CollectionUtils.forceEmptyToNull;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.ImmutableSet;
import com.googlecode.objectify.Key;
import com.googlecode.objectify.annotation.AlsoLoad;
import com.googlecode.objectify.annotation.Embed;
@@ -34,6 +37,7 @@ import javax.persistence.AttributeOverrides;
import javax.persistence.Column;
import javax.persistence.Embeddable;
import javax.persistence.Embedded;
import javax.persistence.PostLoad;
import org.joda.time.DateTime;
/** Transfer data for domain. */
@@ -214,6 +218,28 @@ public class DomainTransferData extends TransferData<DomainTransferData.Builder>
return serverApproveAutorenewPollMessageHistoryId;
}
@PostLoad
@Override
void postLoad() {
// The superclass's serverApproveEntities should include the billing events if present
super.postLoad();
ImmutableSet.Builder<VKey<? extends TransferServerApproveEntity>> serverApproveEntitiesBuilder =
new ImmutableSet.Builder<>();
if (serverApproveEntities != null) {
serverApproveEntitiesBuilder.addAll(serverApproveEntities);
}
if (serverApproveBillingEvent != null) {
serverApproveEntitiesBuilder.add(serverApproveBillingEvent);
}
if (serverApproveAutorenewEvent != null) {
serverApproveEntitiesBuilder.add(serverApproveAutorenewEvent);
}
if (serverApproveAutorenewPollMessage != null) {
serverApproveEntitiesBuilder.add(serverApproveAutorenewPollMessage);
}
serverApproveEntities = forceEmptyToNull(serverApproveEntitiesBuilder.build());
}
@Override
public boolean isEmpty() {
return EMPTY.equals(this);
@@ -167,7 +167,7 @@ public abstract class TransferData<
return;
}
Key<? extends EppResource> eppKey;
if (getClass().equals(DomainBase.class)) {
if (getClass().equals(DomainTransferData.class)) {
eppKey = Key.create(DomainBase.class, repoId);
} else {
eppKey = Key.create(ContactResource.class, repoId);
@@ -288,6 +288,15 @@ public class JpaTransactionManagerImpl implements JpaTransactionManager {
transactionInfo.get().addUpdate(toPersist);
}
@Override
public void putAll(Object... entities) {
checkArgumentNotNull(entities, "entities must be specified");
assertInTransaction();
for (Object entity : entities) {
put(entity);
}
}
@Override
public void putAll(ImmutableCollection<?> entities) {
checkArgumentNotNull(entities, "entities must be specified");
@@ -123,7 +123,10 @@ public interface TransactionManager {
/** Persists a new entity or update the existing entity in the database. */
void put(Object entity);
/** Persists all new entities or update the existing entities in the database. */
/** Persists all new entities or updates the existing entities in the database. */
void putAll(Object... entities);
/** Persists all new entities or updates the existing entities in the database. */
void putAll(ImmutableCollection<?> entities);
/**
@@ -43,7 +43,7 @@ import google.registry.request.Modules.UrlFetchTransportModule;
import google.registry.request.Modules.UserServiceModule;
import google.registry.tools.AuthModule.LocalCredentialModule;
import google.registry.util.UtilsModule;
import google.registry.whois.WhoisModule;
import google.registry.whois.NonCachingWhoisModule;
import javax.annotation.Nullable;
import javax.inject.Singleton;
@@ -81,7 +81,7 @@ import javax.inject.Singleton;
UserServiceModule.class,
UtilsModule.class,
VoidDnsWriterModule.class,
WhoisModule.class
NonCachingWhoisModule.class
})
interface RegistryToolComponent {
void inject(AckPollMessagesCommand command);
@@ -0,0 +1,52 @@
// 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.whois;
import dagger.Module;
import dagger.Provides;
import google.registry.util.Clock;
import google.registry.whois.WhoisMetrics.WhoisMetric;
import java.io.IOException;
import java.io.Reader;
import javax.servlet.http.HttpServletRequest;
/**
* Dagger base module for the whois package.
*
* <p>Provides whois objects common to both the normal ({@link WhoisModule}) and non-caching ({@link
* NonCachingWhoisModule}) implementations.
*/
@Module
class BaseWhoisModule {
@Provides
@SuppressWarnings("CloseableProvides")
static Reader provideHttpInputReader(HttpServletRequest req) {
try {
return req.getReader();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
/**
* Provides a {@link WhoisMetrics.WhoisMetric.Builder} with the startTimestamp already
* initialized.
*/
@Provides
static WhoisMetric.Builder provideEppMetricBuilder(Clock clock) {
return WhoisMetric.builderForRequest(clock);
}
}
@@ -14,6 +14,7 @@
package google.registry.whois;
import static google.registry.model.EppResourceUtils.loadByForeignKey;
import static google.registry.model.EppResourceUtils.loadByForeignKeyCached;
import com.google.common.net.InternetDomainName;
@@ -25,20 +26,27 @@ import org.joda.time.DateTime;
public class DomainLookupCommand extends DomainOrHostLookupCommand {
private final boolean fullOutput;
private final boolean cached;
private final String whoisRedactedEmailText;
public DomainLookupCommand(
InternetDomainName domainName,
boolean fullOutput,
boolean cached,
String whoisRedactedEmailText) {
super(domainName, "Domain");
this.fullOutput = fullOutput;
this.cached = cached;
this.whoisRedactedEmailText = whoisRedactedEmailText;
}
@Override
protected Optional<WhoisResponse> getResponse(InternetDomainName domainName, DateTime now) {
return loadByForeignKeyCached(DomainBase.class, domainName.toString(), now)
.map(domain -> new DomainWhoisResponse(domain, fullOutput, whoisRedactedEmailText, now));
Optional<DomainBase> domainResource =
cached
? loadByForeignKeyCached(DomainBase.class, domainName.toString(), now)
: loadByForeignKey(DomainBase.class, domainName.toString(), now);
return domainResource.map(
domain -> new DomainWhoisResponse(domain, fullOutput, whoisRedactedEmailText, now));
}
}
@@ -14,6 +14,7 @@
package google.registry.whois;
import static google.registry.model.EppResourceUtils.loadByForeignKey;
import static google.registry.model.EppResourceUtils.loadByForeignKeyCached;
import com.google.common.net.InternetDomainName;
@@ -24,13 +25,19 @@ import org.joda.time.DateTime;
/** Represents a WHOIS lookup on a nameserver based on its hostname. */
public class NameserverLookupByHostCommand extends DomainOrHostLookupCommand {
NameserverLookupByHostCommand(InternetDomainName hostName) {
boolean cached;
NameserverLookupByHostCommand(InternetDomainName hostName, boolean cached) {
super(hostName, "Nameserver");
this.cached = cached;
}
@Override
protected Optional<WhoisResponse> getResponse(InternetDomainName hostName, DateTime now) {
return loadByForeignKeyCached(HostResource.class, hostName.toString(), now)
.map(host -> new NameserverWhoisResponse(host, now));
Optional<HostResource> hostResource =
cached
? loadByForeignKeyCached(HostResource.class, hostName.toString(), now)
: loadByForeignKey(HostResource.class, hostName.toString(), now);
return hostResource.map(host -> new NameserverWhoisResponse(host, now));
}
}
@@ -0,0 +1,39 @@
// 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.whois;
import dagger.Module;
import dagger.Provides;
import google.registry.config.RegistryConfig.Config;
/**
* Whois module for systems that require that we not cache EPP resources (e.g. the nomulus tool).
*
* <h3>Dependencies</h3>
*
* <ul>
* <li>{@link google.registry.request.RequestModule RequestModule}
* </ul>
*
* @see "google.registry.tools.RegistryToolComponent"
*/
@Module
public final class NonCachingWhoisModule extends BaseWhoisModule {
@Provides
@Config("whoisCommandFactory")
static WhoisCommandFactory provideWhoisCommandFactory() {
return WhoisCommandFactory.createNonCached();
}
}
@@ -39,61 +39,66 @@ final class RegistrarLookupCommand implements WhoisCommand {
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
/** True if the command should return cached responses. */
private boolean cached;
/**
* Cache of a map from a stripped-down (letters and digits only) name to the registrar. This map
* includes only active, publicly visible registrars, because the others should be invisible to
* WHOIS.
*/
private static final Supplier<Map<String, Registrar>> REGISTRAR_BY_NORMALIZED_NAME_CACHE =
memoizeWithShortExpiration(
() -> {
Map<String, Registrar> map = new HashMap<>();
// Use the normalized registrar name as a key, and ignore inactive and hidden
// registrars.
for (Registrar registrar : Registrar.loadAllCached()) {
if (!registrar.isLiveAndPubliclyVisible() || registrar.getRegistrarName() == null) {
continue;
}
String normalized = normalizeRegistrarName(registrar.getRegistrarName());
if (map.put(normalized, registrar) != null) {
logger.atWarning().log(
"%s appeared as a normalized registrar name for more than one registrar.",
normalized);
}
}
// Use the normalized registrar name without its last word as a key, assuming there are
// multiple words in the name. This allows searches without LLC or INC, etc. Only insert
// if there isn't already a mapping for this string, so that if there's a registrar with
// a
// two word name (Go Daddy) and no business-type suffix and another registrar with just
// that first word as its name (Go), the latter will win.
for (Registrar registrar : ImmutableList.copyOf(map.values())) {
if (registrar.getRegistrarName() == null) {
continue;
}
List<String> words =
Splitter.on(CharMatcher.whitespace()).splitToList(registrar.getRegistrarName());
if (words.size() > 1) {
String normalized =
normalizeRegistrarName(Joiner.on("").join(words.subList(0, words.size() - 1)));
map.putIfAbsent(normalized, registrar);
}
}
return ImmutableMap.copyOf(map);
});
memoizeWithShortExpiration(RegistrarLookupCommand::loadRegistrarMap);
@VisibleForTesting
final String registrarName;
RegistrarLookupCommand(String registrarName) {
RegistrarLookupCommand(String registrarName, boolean cached) {
checkArgument(!isNullOrEmpty(registrarName), "registrarName");
this.registrarName = registrarName;
this.cached = cached;
}
static Map<String, Registrar> loadRegistrarMap() {
Map<String, Registrar> map = new HashMap<>();
// Use the normalized registrar name as a key, and ignore inactive and hidden
// registrars.
for (Registrar registrar : Registrar.loadAll()) {
if (!registrar.isLiveAndPubliclyVisible() || registrar.getRegistrarName() == null) {
continue;
}
String normalized = normalizeRegistrarName(registrar.getRegistrarName());
if (map.put(normalized, registrar) != null) {
logger.atWarning().log(
"%s appeared as a normalized registrar name for more than one registrar.", normalized);
}
}
// Use the normalized registrar name without its last word as a key, assuming there are
// multiple words in the name. This allows searches without LLC or INC, etc. Only insert
// if there isn't already a mapping for this string, so that if there's a registrar with
// a
// two word name (Go Daddy) and no business-type suffix and another registrar with just
// that first word as its name (Go), the latter will win.
for (Registrar registrar : ImmutableList.copyOf(map.values())) {
if (registrar.getRegistrarName() == null) {
continue;
}
List<String> words =
Splitter.on(CharMatcher.whitespace()).splitToList(registrar.getRegistrarName());
if (words.size() > 1) {
String normalized =
normalizeRegistrarName(Joiner.on("").join(words.subList(0, words.size() - 1)));
map.putIfAbsent(normalized, registrar);
}
}
return ImmutableMap.copyOf(map);
}
@Override
public WhoisResponse executeQuery(DateTime now) throws WhoisException {
Registrar registrar =
REGISTRAR_BY_NORMALIZED_NAME_CACHE.get().get(normalizeRegistrarName(registrarName));
Map<String, Registrar> registrars =
cached ? REGISTRAR_BY_NORMALIZED_NAME_CACHE.get() : loadRegistrarMap();
Registrar registrar = registrars.get(normalizeRegistrarName(registrarName));
// If a registrar is in the cache, we know it must be active and publicly visible.
if (registrar == null) {
throw new WhoisException(now, SC_NOT_FOUND, "No registrar found.");
@@ -26,10 +26,30 @@ import java.net.InetAddress;
*/
public class WhoisCommandFactory {
/** True if the commands should return cached responses. */
private boolean cached = true;
/** Public default constructor, needed so we can construct this from the class name. */
public WhoisCommandFactory() {}
private WhoisCommandFactory(boolean cached) {
this.cached = cached;
}
/** Create a command factory that does not rely on entity caches. */
static WhoisCommandFactory createNonCached() {
return new WhoisCommandFactory(false);
}
/** Create a command factory that may rely on entity caches. */
static WhoisCommandFactory createCached() {
return new WhoisCommandFactory(true);
}
/** Returns a new {@link WhoisCommand} to perform a domain lookup on the specified domain name. */
public WhoisCommand domainLookup(
InternetDomainName domainName, boolean fullOutput, String whoisRedactedEmailText) {
return new DomainLookupCommand(domainName, fullOutput, whoisRedactedEmailText);
return new DomainLookupCommand(domainName, fullOutput, cached, whoisRedactedEmailText);
}
/**
@@ -43,7 +63,7 @@ public class WhoisCommandFactory {
* Returns a new {@link WhoisCommand} to perform a nameserver lookup on the specified host name.
*/
public WhoisCommand nameserverLookupByHost(InternetDomainName hostName) {
return new NameserverLookupByHostCommand(hostName);
return new NameserverLookupByHostCommand(hostName, cached);
}
/**
@@ -51,6 +71,6 @@ public class WhoisCommandFactory {
* name.
*/
public WhoisCommand registrarLookup(String registrar) {
return new RegistrarLookupCommand(registrar);
return new RegistrarLookupCommand(registrar, cached);
}
}
@@ -20,11 +20,6 @@ import static google.registry.util.TypeUtils.instantiate;
import dagger.Module;
import dagger.Provides;
import google.registry.config.RegistryConfig.Config;
import google.registry.util.Clock;
import google.registry.whois.WhoisMetrics.WhoisMetric;
import java.io.IOException;
import java.io.Reader;
import javax.servlet.http.HttpServletRequest;
/**
* Dagger module for the whois package.
@@ -38,31 +33,11 @@ import javax.servlet.http.HttpServletRequest;
* @see "google.registry.module.frontend.FrontendComponent"
*/
@Module
public final class WhoisModule {
@Provides
@SuppressWarnings("CloseableProvides")
static Reader provideHttpInputReader(HttpServletRequest req) {
try {
return req.getReader();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
public final class WhoisModule extends BaseWhoisModule {
@Provides
@Config("whoisCommandFactory")
static WhoisCommandFactory provideWhoisCommandFactory(
@Config("whoisCommandFactoryClass") String factoryClass) {
return instantiate(getClassFromString(factoryClass, WhoisCommandFactory.class));
}
/**
* Provides a {@link WhoisMetrics.WhoisMetric.Builder} with the startTimestamp already
* initialized.
*/
@Provides
static WhoisMetric.Builder provideEppMetricBuilder(Clock clock) {
return WhoisMetric.builderForRequest(clock);
}
}
@@ -194,7 +194,7 @@ class WhoisReader {
if (!tld.isPresent()) {
// This target is not under any configured TLD, so just try it as a registrar name.
logger.atInfo().log("Attempting registrar lookup using %s as a registrar", arg1);
return new RegistrarLookupCommand(arg1);
return commandFactory.registrarLookup(arg1);
}
// If the target is exactly one level above the TLD, then this is a second level domain
@@ -23,11 +23,11 @@ import static google.registry.model.reporting.HistoryEntry.Type.DOMAIN_CREATE;
import static google.registry.model.reporting.HistoryEntry.Type.DOMAIN_TRANSFER_REJECT;
import static google.registry.model.reporting.HistoryEntry.Type.DOMAIN_TRANSFER_REQUEST;
import static google.registry.testing.DatabaseHelper.assertBillingEvents;
import static google.registry.testing.DatabaseHelper.deleteResource;
import static google.registry.testing.DatabaseHelper.getOnlyHistoryEntryOfType;
import static google.registry.testing.DatabaseHelper.getOnlyPollMessage;
import static google.registry.testing.DatabaseHelper.getPollMessages;
import static google.registry.testing.DatabaseHelper.loadRegistrar;
import static google.registry.testing.DatabaseHelper.persistDomainAsDeleted;
import static google.registry.testing.DatabaseHelper.persistResource;
import static google.registry.testing.DomainBaseSubject.assertAboutDomains;
import static google.registry.testing.EppExceptionSubject.assertAboutEppExceptions;
@@ -56,12 +56,14 @@ import google.registry.model.reporting.HistoryEntry;
import google.registry.model.transfer.TransferData;
import google.registry.model.transfer.TransferResponse;
import google.registry.model.transfer.TransferStatus;
import google.registry.testing.DualDatabaseTest;
import google.registry.testing.TestOfyAndSql;
import org.joda.time.DateTime;
import org.joda.time.Duration;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
/** Unit tests for {@link DomainTransferRejectFlow}. */
@DualDatabaseTest
class DomainTransferRejectFlowTest
extends DomainTransferFlowTestCase<DomainTransferRejectFlow, DomainBase> {
@@ -151,31 +153,31 @@ class DomainTransferRejectFlowTest
runFlow();
}
@Test
@TestOfyAndSql
void testSuccess() throws Exception {
doSuccessfulTest("domain_transfer_reject.xml", "domain_transfer_reject_response.xml");
}
@Test
@TestOfyAndSql
void testDryRun() throws Exception {
setEppInput("domain_transfer_reject.xml");
eppLoader.replaceAll("JD1234-REP", contact.getRepoId());
dryRunFlowAssertResponse(loadFile("domain_transfer_reject_response.xml"));
}
@Test
@TestOfyAndSql
void testSuccess_domainAuthInfo() throws Exception {
doSuccessfulTest(
"domain_transfer_reject_domain_authinfo.xml", "domain_transfer_reject_response.xml");
}
@Test
@TestOfyAndSql
void testSuccess_contactAuthInfo() throws Exception {
doSuccessfulTest(
"domain_transfer_reject_contact_authinfo.xml", "domain_transfer_reject_response.xml");
}
@Test
@TestOfyAndSql
void testFailure_notAuthorizedForTld() {
persistResource(
loadRegistrar("TheRegistrar").asBuilder().setAllowedTlds(ImmutableSet.of()).build());
@@ -188,7 +190,7 @@ class DomainTransferRejectFlowTest
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
@TestOfyAndSql
void testSuccess_superuserNotAuthorizedForTld() throws Exception {
persistResource(
loadRegistrar("TheRegistrar").asBuilder().setAllowedTlds(ImmutableSet.of()).build());
@@ -196,7 +198,7 @@ class DomainTransferRejectFlowTest
CommitMode.LIVE, UserPrivileges.SUPERUSER, loadFile("domain_transfer_reject_response.xml"));
}
@Test
@TestOfyAndSql
void testFailure_badContactPassword() {
// Change the contact's password so it does not match the password in the file.
contact =
@@ -212,7 +214,7 @@ class DomainTransferRejectFlowTest
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
@TestOfyAndSql
void testFailure_badDomainPassword() {
// Change the domain's password so it does not match the password in the file.
domain =
@@ -228,7 +230,7 @@ class DomainTransferRejectFlowTest
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
@TestOfyAndSql
void testFailure_neverBeenTransferred() {
changeTransferStatus(null);
EppException thrown =
@@ -237,7 +239,7 @@ class DomainTransferRejectFlowTest
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
@TestOfyAndSql
void testFailure_clientApproved() {
changeTransferStatus(TransferStatus.CLIENT_APPROVED);
EppException thrown =
@@ -246,7 +248,7 @@ class DomainTransferRejectFlowTest
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
@TestOfyAndSql
void testFailure_clientRejected() {
changeTransferStatus(TransferStatus.CLIENT_REJECTED);
EppException thrown =
@@ -255,7 +257,7 @@ class DomainTransferRejectFlowTest
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
@TestOfyAndSql
void testFailure_clientCancelled() {
changeTransferStatus(TransferStatus.CLIENT_CANCELLED);
EppException thrown =
@@ -264,7 +266,7 @@ class DomainTransferRejectFlowTest
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
@TestOfyAndSql
void testFailure_serverApproved() {
changeTransferStatus(TransferStatus.SERVER_APPROVED);
EppException thrown =
@@ -273,7 +275,7 @@ class DomainTransferRejectFlowTest
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
@TestOfyAndSql
void testFailure_serverCancelled() {
changeTransferStatus(TransferStatus.SERVER_CANCELLED);
EppException thrown =
@@ -282,7 +284,7 @@ class DomainTransferRejectFlowTest
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
@TestOfyAndSql
void testFailure_gainingClient() {
setClientIdForFlow("NewRegistrar");
EppException thrown =
@@ -291,7 +293,7 @@ class DomainTransferRejectFlowTest
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
@TestOfyAndSql
void testFailure_unrelatedClient() {
setClientIdForFlow("ClientZ");
EppException thrown =
@@ -300,7 +302,7 @@ class DomainTransferRejectFlowTest
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
@TestOfyAndSql
void testFailure_deletedDomain() throws Exception {
domain =
persistResource(domain.asBuilder().setDeletionTime(clock.nowUtc().minusDays(1)).build());
@@ -310,9 +312,9 @@ class DomainTransferRejectFlowTest
assertThat(thrown).hasMessageThat().contains(String.format("(%s)", getUniqueIdFromCommand()));
}
@Test
@TestOfyAndSql
void testFailure_nonexistentDomain() throws Exception {
deleteResource(domain);
persistDomainAsDeleted(domain, clock.nowUtc());
ResourceDoesNotExistException thrown =
assertThrows(
ResourceDoesNotExistException.class, () -> doFailingTest("domain_transfer_reject.xml"));
@@ -322,7 +324,7 @@ class DomainTransferRejectFlowTest
// NB: No need to test pending delete status since pending transfers will get cancelled upon
// entering pending delete phase. So it's already handled in that test case.
@Test
@TestOfyAndSql
void testIcannActivityReportField_getsLogged() throws Exception {
runFlow();
assertIcannReportingActivityFieldLogged("srs-dom-transfer-reject");
@@ -338,7 +340,7 @@ class DomainTransferRejectFlowTest
.build());
}
@Test
@TestOfyAndSql
void testIcannTransactionRecord_noRecordsToCancel() throws Exception {
setUpGracePeriodDurations();
runFlow();
@@ -348,7 +350,7 @@ class DomainTransferRejectFlowTest
.containsExactly(DomainTransactionRecord.create("tld", clock.nowUtc(), TRANSFER_NACKED, 1));
}
@Test
@TestOfyAndSql
void testIcannTransactionRecord_cancelsPreviousRecords() throws Exception {
setUpGracePeriodDurations();
DomainTransactionRecord previousSuccessRecord =
@@ -21,6 +21,7 @@ import static com.google.common.truth.Truth.assertThat;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Joiner;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
import com.google.common.collect.Sets;
import com.google.common.truth.Correspondence;
import com.google.common.truth.Correspondence.BinaryPredicate;
@@ -50,6 +51,11 @@ public final class ImmutableObjectSubject extends Subject {
this.actual = actual;
}
public void isEqualExceptFields(
@Nullable ImmutableObject expected, Iterable<String> ignoredFields) {
isEqualExceptFields(expected, Iterables.toArray(ignoredFields, String.class));
}
public void isEqualExceptFields(@Nullable ImmutableObject expected, String... ignoredFields) {
if (actual == null) {
assertThat(expected).isNull();
@@ -15,6 +15,7 @@
package google.registry.model.domain;
import static com.google.common.truth.Truth.assertThat;
import static google.registry.flows.domain.DomainTransferUtils.createPendingTransferData;
import static google.registry.model.ImmutableObjectSubject.assertAboutImmutableObjects;
import static google.registry.persistence.transaction.TransactionManagerFactory.jpaTm;
import static google.registry.testing.DatabaseHelper.createTld;
@@ -34,11 +35,13 @@ import google.registry.model.billing.BillingEvent.Reason;
import google.registry.model.common.EntityGroupRoot;
import google.registry.model.contact.ContactResource;
import google.registry.model.domain.DesignatedContact.Type;
import google.registry.model.domain.Period.Unit;
import google.registry.model.domain.launch.LaunchNotice;
import google.registry.model.domain.rgp.GracePeriodStatus;
import google.registry.model.domain.secdns.DelegationSignerData;
import google.registry.model.eppcommon.AuthInfo.PasswordAuth;
import google.registry.model.eppcommon.StatusValue;
import google.registry.model.eppcommon.Trid;
import google.registry.model.host.HostResource;
import google.registry.model.poll.PollMessage;
import google.registry.model.reporting.HistoryEntry;
@@ -54,6 +57,7 @@ import org.joda.money.Money;
import org.joda.time.DateTime;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.extension.RegisterExtension;
import org.testcontainers.shaded.com.google.common.collect.ImmutableList;
/** Verify that we can store/retrieve DomainBase objects from a SQL database. */
@DualDatabaseTest
@@ -617,15 +621,15 @@ public class DomainBaseSqlTest {
.setParent(historyEntry)
.build();
DomainTransferData transferData =
new DomainTransferData.Builder()
.setServerApproveBillingEvent(
createLegacyVKey(BillingEvent.OneTime.class, oneTimeBillingEvent.getId()))
.setServerApproveAutorenewEvent(
createLegacyVKey(BillingEvent.Recurring.class, billEvent.getId()))
.setServerApproveAutorenewPollMessage(
createLegacyVKey(
PollMessage.Autorenew.class, autorenewPollMessage.getId()))
.build();
createPendingTransferData(
new DomainTransferData.Builder()
.setTransferRequestTrid(Trid.create("foo", "bar"))
.setTransferRequestTime(fakeClock.nowUtc())
.setGainingClientId("registrar2")
.setLosingClientId("registrar1")
.setPendingTransferExpirationTime(fakeClock.nowUtc().plusDays(1)),
ImmutableSet.of(oneTimeBillingEvent, billEvent, autorenewPollMessage),
Period.create(0, Unit.YEARS));
gracePeriods =
ImmutableSet.of(
GracePeriod.create(
@@ -708,10 +712,17 @@ public class DomainBaseSqlTest {
// Fix the original creation timestamp (this gets initialized on first write)
DomainBase org = domain.asBuilder().setCreationTime(thatDomain.getCreationTime()).build();
String[] moreExcepts = Arrays.copyOf(excepts, excepts.length + 1);
moreExcepts[moreExcepts.length - 1] = "updateTimestamp";
ImmutableList<String> moreExcepts =
new ImmutableList.Builder<String>()
.addAll(Arrays.asList(excepts))
.add("updateTimestamp")
.add("transferData")
.build();
// Note that the equality comparison forces a lazy load of all fields.
assertAboutImmutableObjects().that(thatDomain).isEqualExceptFields(org, moreExcepts);
// Transfer data cannot be directly compared due to serverApproveEtities inequalities
assertAboutImmutableObjects()
.that(domain.getTransferData())
.isEqualExceptFields(org.getTransferData(), "serverApproveEntities");
}
}
@@ -326,8 +326,7 @@ public class DatabaseHelper {
* Returns a persisted domain that is the passed-in domain modified to be deleted at the specified
* time.
*/
public static DomainBase persistDomainAsDeleted(
DomainBase domain, DateTime deletionTime) {
public static DomainBase persistDomainAsDeleted(DomainBase domain, DateTime deletionTime) {
return persistResource(domain.asBuilder().setDeletionTime(deletionTime).build());
}
@@ -347,7 +346,7 @@ public class DatabaseHelper {
}
public static ReservedList persistReservedList(
String listName, boolean shouldPublish, String... lines) {
String listName, boolean shouldPublish, String... lines) {
ReservedList reservedList =
new ReservedList.Builder()
.setName(listName)
@@ -512,10 +511,7 @@ public class DatabaseHelper {
}
public static BillingEvent.OneTime createBillingEventForTransfer(
DomainBase domain,
HistoryEntry historyEntry,
DateTime costLookupTime,
DateTime eventTime) {
DomainBase domain, HistoryEntry historyEntry, DateTime costLookupTime, DateTime eventTime) {
return new BillingEvent.OneTime.Builder()
.setReason(Reason.TRANSFER)
.setTargetId(domain.getDomainName())
@@ -530,10 +526,7 @@ public class DatabaseHelper {
}
public static ContactResource persistContactWithPendingTransfer(
ContactResource contact,
DateTime requestTime,
DateTime expirationTime,
DateTime now) {
ContactResource contact, DateTime requestTime, DateTime expirationTime, DateTime now) {
HistoryEntry historyEntryContactTransfer =
persistResource(
new HistoryEntry.Builder()
@@ -587,26 +580,29 @@ public class DatabaseHelper {
String domainName = String.format("%s.%s", label, tld);
String repoId = generateNewDomainRoid(tld);
DomainBase domain =
new DomainBase.Builder()
.setRepoId(repoId)
.setDomainName(domainName)
.setPersistedCurrentSponsorClientId("TheRegistrar")
.setCreationClientId("TheRegistrar")
.setCreationTimeForTest(creationTime)
.setRegistrationExpirationTime(expirationTime)
.setRegistrant(contact.createVKey())
.setContacts(
ImmutableSet.of(
DesignatedContact.create(Type.ADMIN, contact.createVKey()),
DesignatedContact.create(Type.TECH, contact.createVKey())))
.setAuthInfo(DomainAuthInfo.create(PasswordAuth.create("fooBAR")))
.addGracePeriod(
GracePeriod.create(GracePeriodStatus.ADD, repoId, now.plusDays(10), "foo", null))
.build();
HistoryEntry historyEntryDomainCreate =
persistResource(
new HistoryEntry.Builder()
new DomainBase.Builder()
.setRepoId(repoId)
.setDomainName(domainName)
.setPersistedCurrentSponsorClientId("TheRegistrar")
.setCreationClientId("TheRegistrar")
.setCreationTimeForTest(creationTime)
.setRegistrationExpirationTime(expirationTime)
.setRegistrant(contact.createVKey())
.setContacts(
ImmutableSet.of(
DesignatedContact.create(Type.ADMIN, contact.createVKey()),
DesignatedContact.create(Type.TECH, contact.createVKey())))
.setAuthInfo(DomainAuthInfo.create(PasswordAuth.create("fooBAR")))
.addGracePeriod(
GracePeriod.create(
GracePeriodStatus.ADD, repoId, now.plusDays(10), "TheRegistrar", null))
.build());
DomainHistory historyEntryDomainCreate =
persistResource(
new DomainHistory.Builder()
.setType(HistoryEntry.Type.DOMAIN_CREATE)
.setModificationTime(now)
.setParent(domain)
.build());
BillingEvent.Recurring autorenewEvent =
@@ -643,18 +639,17 @@ public class DatabaseHelper {
DateTime requestTime,
DateTime expirationTime,
DateTime extendedRegistrationExpirationTime) {
HistoryEntry historyEntryDomainTransfer =
DomainHistory historyEntryDomainTransfer =
persistResource(
new HistoryEntry.Builder()
new DomainHistory.Builder()
.setType(HistoryEntry.Type.DOMAIN_TRANSFER_REQUEST)
.setModificationTime(tm().transact(() -> tm().getTransactionTime()))
.setParent(domain)
.build());
BillingEvent.OneTime transferBillingEvent = persistResource(createBillingEventForTransfer(
domain,
historyEntryDomainTransfer,
requestTime,
expirationTime));
BillingEvent.OneTime transferBillingEvent =
persistResource(
createBillingEventForTransfer(
domain, historyEntryDomainTransfer, requestTime, expirationTime));
BillingEvent.Recurring gainingClientAutorenewEvent =
persistResource(
new BillingEvent.Recurring.Builder()
@@ -678,18 +673,18 @@ public class DatabaseHelper {
.build());
// Modify the existing autorenew event to reflect the pending transfer.
persistResource(
tm().loadByKey(domain.getAutorenewBillingEvent())
.asBuilder()
.setRecurrenceEndTime(expirationTime)
.build());
transactIfJpaTm(
() ->
tm().loadByKey(domain.getAutorenewBillingEvent())
.asBuilder()
.setRecurrenceEndTime(expirationTime)
.build()));
// Update the end time of the existing autorenew poll message. We must delete it if it has no
// events left in it.
PollMessage.Autorenew autorenewPollMessage = tm().loadByKey(domain.getAutorenewPollMessage());
PollMessage.Autorenew autorenewPollMessage =
transactIfJpaTm(() -> tm().loadByKey(domain.getAutorenewPollMessage()));
if (autorenewPollMessage.getEventTime().isBefore(expirationTime)) {
persistResource(
autorenewPollMessage.asBuilder()
.setAutorenewEndTime(expirationTime)
.build());
persistResource(autorenewPollMessage.asBuilder().setAutorenewEndTime(expirationTime).build());
} else {
deleteResource(autorenewPollMessage);
}
@@ -928,11 +923,8 @@ public class DatabaseHelper {
}
public static PollMessage getOnlyPollMessage(
String clientId,
DateTime now,
Class<? extends PollMessage> subType) {
return getPollMessages(clientId, now)
.stream()
String clientId, DateTime now, Class<? extends PollMessage> subType) {
return getPollMessages(clientId, now).stream()
.filter(subType::isInstance)
.map(subType::cast)
.collect(onlyElement());
@@ -957,10 +949,7 @@ public class DatabaseHelper {
return createDomainRepoId(ObjectifyService.allocateId(), tld);
}
/**
* Returns a newly allocated, globally unique contact/host repoId of the format
* HEX_TLD-ROID.
*/
/** Returns a newly allocated, globally unique contact/host repoId of the format HEX_TLD-ROID. */
public static String generateNewContactHostRoid() {
return createRepoId(ObjectifyService.allocateId(), getContactAndHostRoidSuffix());
}
@@ -1131,8 +1120,7 @@ public class DatabaseHelper {
*/
public static ImmutableList<HistoryEntry> getHistoryEntriesOfType(
EppResource resource, final HistoryEntry.Type type) {
return getHistoryEntries(resource)
.stream()
return getHistoryEntries(resource).stream()
.filter(entry -> entry.getType() == type)
.collect(toImmutableList());
}
@@ -1143,7 +1131,7 @@ public class DatabaseHelper {
*/
public static HistoryEntry getOnlyHistoryEntryOfType(
EppResource resource, final HistoryEntry.Type type) {
List<HistoryEntry> historyEntries = getHistoryEntriesOfType(resource, type);
List<HistoryEntry> historyEntries = getHistoryEntriesOfType(resource, type);
assertThat(historyEntries).hasSize(1);
return historyEntries.get(0);
}
@@ -1151,13 +1139,16 @@ public class DatabaseHelper {
private static HistoryEntry.Type getHistoryEntryType(EppResource resource) {
if (resource instanceof ContactResource) {
return resource.getRepoId() != null
? HistoryEntry.Type.CONTACT_CREATE : HistoryEntry.Type.CONTACT_UPDATE;
? HistoryEntry.Type.CONTACT_CREATE
: HistoryEntry.Type.CONTACT_UPDATE;
} else if (resource instanceof HostResource) {
return resource.getRepoId() != null
? HistoryEntry.Type.HOST_CREATE : HistoryEntry.Type.HOST_UPDATE;
? HistoryEntry.Type.HOST_CREATE
: HistoryEntry.Type.HOST_UPDATE;
} else if (resource instanceof DomainBase) {
return resource.getRepoId() != null
? HistoryEntry.Type.DOMAIN_CREATE : HistoryEntry.Type.DOMAIN_UPDATE;
? HistoryEntry.Type.DOMAIN_CREATE
: HistoryEntry.Type.DOMAIN_UPDATE;
} else {
throw new AssertionError();
}
@@ -1215,7 +1206,7 @@ public class DatabaseHelper {
tm().clearSessionCache();
}
/** Force the create and update timestamps to get written into the resource. **/
/** Force the create and update timestamps to get written into the resource. */
public static <R> R cloneAndSetAutoTimestamps(final R resource) {
R result;
if (tm().isOfy()) {
@@ -96,7 +96,7 @@ public class WhoisActionTest {
whoisAction.input = new StringReader(input);
whoisAction.response = response;
whoisAction.whoisReader =
new WhoisReader(new WhoisCommandFactory(), "Please contact registrar");
new WhoisReader(WhoisCommandFactory.createCached(), "Please contact registrar");
whoisAction.whoisMetrics = new WhoisMetrics();
whoisAction.metricBuilder = WhoisMetric.builderForRequest(clock);
whoisAction.disclaimer =
@@ -0,0 +1,240 @@
// 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.whois;
import static com.google.common.truth.Truth.assertThat;
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
import static google.registry.testing.DatabaseHelper.newDomainBase;
import static google.registry.testing.DatabaseHelper.newHostResource;
import static google.registry.testing.DatabaseHelper.newRegistry;
import static google.registry.testing.DatabaseHelper.persistNewRegistrar;
import static google.registry.testing.DatabaseHelper.persistResource;
import com.google.common.collect.ImmutableSet;
import com.google.common.net.InternetDomainName;
import google.registry.config.RegistryConfig;
import google.registry.model.domain.DomainBase;
import google.registry.model.host.HostResource;
import google.registry.model.registrar.Registrar;
import google.registry.testing.AppEngineExtension;
import google.registry.testing.FakeClock;
import google.registry.testing.TestCacheExtension;
import java.net.InetAddress;
import org.joda.time.Duration;
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;
class WhoisCommandFactoryTest {
FakeClock clock = new FakeClock();
@RegisterExtension
final AppEngineExtension appEngine =
AppEngineExtension.builder().withDatastoreAndCloudSql().withClock(clock).build();
@RegisterExtension
final TestCacheExtension testCacheExtension =
new TestCacheExtension.Builder().withEppResourceCache(Duration.millis(1000000000)).build();
WhoisCommandFactory noncachedFactory = WhoisCommandFactory.createNonCached();
WhoisCommandFactory cachedFactory = WhoisCommandFactory.createCached();
DomainBase domain;
HostResource host;
Registrar otherRegistrar;
int origSingletonCacheRefreshSeconds;
@BeforeEach
void setUp() throws Exception {
persistResource(newRegistry("tld", "TLD"));
host =
newHostResource("ns.example.tld")
.asBuilder()
.setInetAddresses(ImmutableSet.of(InetAddress.getByName("1.2.3.4")))
.build();
persistResource(host);
domain = newDomainBase("example.tld", host);
persistResource(domain);
otherRegistrar = persistNewRegistrar("OtherRegistrar");
otherRegistrar =
persistResource(
otherRegistrar
.asBuilder()
.setState(Registrar.State.ACTIVE)
.setPhoneNumber("+1.2223334444")
.build());
// In addition to the TestCacheExtension, we have to set a long singleton cache timeout.
RegistryConfig.CONFIG_SETTINGS.get().caching.singletonCacheRefreshSeconds = 1000000;
}
@AfterEach
void tearDown() {
// Restore the singleton cache timeout. For some reason, this doesn't work if we store the
// original value in an instance variable (I suspect there may be some overlap in test
// execution) so just restore to zero.
RegistryConfig.CONFIG_SETTINGS.get().caching.singletonCacheRefreshSeconds = 0;
}
@Test
void testNonCached_NameserverLookupByHostCommand() throws Exception {
WhoisResponse response =
noncachedFactory
.nameserverLookupByHost(InternetDomainName.from("ns.example.tld"))
.executeQuery(clock.nowUtc());
assertThat(response.getResponse(false, "").plainTextOutput())
.contains("Registrar: The Registrar");
// Note that we can't use persistResource() for these as that clears the cache.
tm().transact(
() ->
tm().put(
host.asBuilder()
.setPersistedCurrentSponsorClientId("OtherRegistrar")
.build()));
response =
noncachedFactory
.nameserverLookupByHost(InternetDomainName.from("ns.example.tld"))
.executeQuery(clock.nowUtc());
assertThat(response.getResponse(false, "").plainTextOutput())
.contains("Registrar: OtherRegistrar name");
}
@Test
void testCached_NameserverLookupByHostCommand() throws Exception {
WhoisResponse response =
cachedFactory
.nameserverLookupByHost(InternetDomainName.from("ns.example.tld"))
.executeQuery(clock.nowUtc());
assertThat(response.getResponse(false, "").plainTextOutput())
.contains("Registrar: The Registrar");
tm().transact(
() ->
tm().put(
host.asBuilder()
.setPersistedCurrentSponsorClientId("OtherRegistrar")
.build()));
response =
cachedFactory
.nameserverLookupByHost(InternetDomainName.from("ns.example.tld"))
.executeQuery(clock.nowUtc());
assertThat(response.getResponse(false, "").plainTextOutput())
.contains("Registrar: The Registrar");
}
@Test
void testNonCached_DomainLookupCommand() throws Exception {
WhoisResponse response =
noncachedFactory
.domainLookup(InternetDomainName.from("example.tld"), true, "REDACTED")
.executeQuery(clock.nowUtc());
assertThat(response.getResponse(false, "").plainTextOutput())
.contains("Registrar: The Registrar");
tm().transact(
() ->
tm().put(
domain
.asBuilder()
.setPersistedCurrentSponsorClientId("OtherRegistrar")
.build()));
response =
noncachedFactory
.domainLookup(InternetDomainName.from("example.tld"), true, "REDACTED")
.executeQuery(clock.nowUtc());
assertThat(response.getResponse(false, "").plainTextOutput())
.contains("Registrar: OtherRegistrar name");
}
@Test
void testCached_DomainLookupCommand() throws Exception {
WhoisResponse response =
cachedFactory
.domainLookup(InternetDomainName.from("example.tld"), true, "REDACTED")
.executeQuery(clock.nowUtc());
assertThat(response.getResponse(false, "").plainTextOutput())
.contains("Registrar: The Registrar");
tm().transact(
() ->
tm().put(
domain
.asBuilder()
.setPersistedCurrentSponsorClientId("OtherRegistrar")
.build()));
response =
cachedFactory
.domainLookup(InternetDomainName.from("example.tld"), true, "REDACTED")
.executeQuery(clock.nowUtc());
assertThat(response.getResponse(false, "").plainTextOutput())
.contains("Registrar: The Registrar");
}
@Test
void testNonCached_RegistrarLookupCommand() throws Exception {
WhoisResponse response =
noncachedFactory.registrarLookup("OtherRegistrar").executeQuery(clock.nowUtc());
assertThat(response.getResponse(false, "").plainTextOutput())
.contains("Phone Number: +1.2223334444");
tm().transact(
() -> tm().put(otherRegistrar.asBuilder().setPhoneNumber("+1.2345677890").build()));
response = noncachedFactory.registrarLookup("OtherRegistrar").executeQuery(clock.nowUtc());
assertThat(response.getResponse(false, "").plainTextOutput())
.contains("Phone Number: +1.2345677890");
}
@Test
void testCached_RegistrarLookupCommand() throws Exception {
WhoisResponse response =
cachedFactory.registrarLookup("OtherRegistrar").executeQuery(clock.nowUtc());
assertThat(response.getResponse(false, "").plainTextOutput())
.contains("Phone Number: +1.2223334444");
tm().transact(
() -> tm().put(otherRegistrar.asBuilder().setPhoneNumber("+1.2345677890").build()));
response = cachedFactory.registrarLookup("OtherRegistrar").executeQuery(clock.nowUtc());
assertThat(response.getResponse(false, "").plainTextOutput())
.contains("Phone Number: +1.2223334444");
}
@Test
void testNonCached_NameserverLookupByIpCommand() throws Exception {
// Note that this lookup currently doesn't cache the hosts, so there's no point in testing the
// "cached" case. This test is here so that it will fail if anyone adds caching.
WhoisResponse response =
noncachedFactory
.nameserverLookupByIp(InetAddress.getByName("1.2.3.4"))
.executeQuery(clock.nowUtc());
assertThat(response.getResponse(false, "").plainTextOutput())
.contains("Registrar: The Registrar");
tm().transact(
() ->
tm().put(
host.asBuilder()
.setPersistedCurrentSponsorClientId("OtherRegistrar")
.build()));
response =
noncachedFactory
.nameserverLookupByIp(InetAddress.getByName("1.2.3.4"))
.executeQuery(clock.nowUtc());
assertThat(response.getResponse(false, "").plainTextOutput())
.contains("Registrar: OtherRegistrar");
}
}
@@ -78,7 +78,7 @@ class WhoisHttpActionTest {
whoisAction.requestPath = WhoisHttpAction.PATH + pathInfo;
whoisAction.response = response;
whoisAction.whoisReader =
new WhoisReader(new WhoisCommandFactory(), "Please contact registrar");
new WhoisReader(WhoisCommandFactory.createCached(), "Please contact registrar");
whoisAction.whoisMetrics = new WhoisMetrics();
whoisAction.metricBuilder = WhoisMetric.builderForRequest(clock);
whoisAction.disclaimer =
@@ -48,7 +48,7 @@ class WhoisReaderTest {
@SuppressWarnings({"TypeParameterUnusedInFormals", "unchecked"})
<T> T readCommand(String commandStr) throws Exception {
return (T)
new WhoisReader(new WhoisCommandFactory(), "Please contact registrar")
new WhoisReader(WhoisCommandFactory.createCached(), "Please contact registrar")
.readCommand(new StringReader(commandStr), false, clock.nowUtc());
}
+9 -10
View File
@@ -55,8 +55,8 @@ ext {
}
getJdbcAccessInfo = {
if (rootProject.projects.keySet().contains(environment)) {
return getSocketFactoryAccessInfo(environment)
if (rootProject.projects.keySet().contains(dbServer)) {
return getSocketFactoryAccessInfo(dbServer)
} else if (!dbServer.isEmpty()) {
return getAccessInfoByHostPort(dbServer)
} else {
@@ -72,10 +72,8 @@ ext {
// production. The role parameter may be superuser. (More roles will be added
// later).
getCloudSqlCredential = { env, role ->
def devProject = project.hasProperty('devProject')
? project.getProperty('devProject') : rootProject.devProject
def gcpProject = project.hasProperty('gcpProject')
? project.getProperty('gcpProject') : rootProject.gcpProject
def devProject = rootProject.devProject
def gcpProject = rootProject.projects[env]
def keyProject = env in restrictedDbEnv? devProject : gcpProject
def command =
"""gsutil cp \
@@ -135,12 +133,13 @@ publishing {
}
}
// Adds flyway tasks such as: flywayInfo, flywayValidate, flywayMigrate (deploying the schema in
// local repository), and flywayClean (dropping all data in the database). The latter two commands
// are disallowed in environments listed in ext.restrictedDbEnv.
// Adds flyway tasks such as: flywayInfo, flywayValidate, flywayMigrate (
// deploying the schema in local repository), and flywayClean (dropping all data
// in the database). The latter two commands are disallowed in environments
// listed in ext.restrictedDbEnv.
//
// Examples:
// Get info in alpha: nom_build :db:flywayInfo --environment=alpha
// Get info in alpha: nom_build :db:flywayInfo --dbServer=alpha
// Deploy schema to a local test instance and override the database name:
// nom_build :db:flywayMigrate --dbServer=localhost:5432 --dbName=not-default \
// --dbUser=... --dbPassword=...