// Copyright 2016 The Domain Registry 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.testing; import static com.google.common.base.Preconditions.checkNotNull; import static com.google.common.base.Preconditions.checkState; import static com.google.common.base.Suppliers.memoize; import static com.google.common.collect.Iterables.toArray; import static com.google.common.truth.Truth.assertThat; import static com.google.common.truth.Truth.assertWithMessage; import static google.registry.flows.ResourceFlowUtils.createTransferResponse; import static google.registry.model.EppResourceUtils.createContactHostRoid; import static google.registry.model.EppResourceUtils.createDomainRoid; import static google.registry.model.domain.launch.ApplicationStatus.VALIDATED; import static google.registry.model.ofy.ObjectifyService.ofy; import static google.registry.pricing.PricingEngineProxy.getDomainRenewCost; import static google.registry.util.CollectionUtils.difference; import static google.registry.util.CollectionUtils.union; import static google.registry.util.DateTimeUtils.END_OF_TIME; import static google.registry.util.DateTimeUtils.START_OF_TIME; import static google.registry.util.DomainNameUtils.ACE_PREFIX_REGEX; import static google.registry.util.DomainNameUtils.getTldFromSld; import static google.registry.util.ResourceUtils.readResourceUtf8; import static java.util.Arrays.asList; import static org.joda.money.CurrencyUnit.USD; import com.google.common.base.Function; import com.google.common.base.Optional; import com.google.common.base.Predicate; import com.google.common.base.Splitter; import com.google.common.base.Supplier; import com.google.common.collect.FluentIterable; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableSet; import com.google.common.collect.ImmutableSortedMap; import com.google.common.collect.Iterables; import com.googlecode.objectify.Key; import com.googlecode.objectify.Ref; import com.googlecode.objectify.VoidWork; import com.googlecode.objectify.Work; import com.googlecode.objectify.cmd.Saver; import google.registry.config.RegistryEnvironment; import google.registry.model.Buildable; import google.registry.model.EppResource; import google.registry.model.EppResource.ForeignKeyedEppResource; import google.registry.model.ImmutableObject; import google.registry.model.billing.BillingEvent; import google.registry.model.billing.BillingEvent.Flag; import google.registry.model.billing.BillingEvent.Reason; import google.registry.model.contact.ContactAuthInfo; import google.registry.model.contact.ContactResource; import google.registry.model.domain.DesignatedContact; import google.registry.model.domain.DesignatedContact.Type; import google.registry.model.domain.DomainApplication; import google.registry.model.domain.DomainAuthInfo; import google.registry.model.domain.DomainResource; import google.registry.model.domain.launch.LaunchPhase; 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.index.DomainApplicationIndex; import google.registry.model.index.EppResourceIndex; import google.registry.model.index.ForeignKeyIndex; import google.registry.model.ofy.ObjectifyService; import google.registry.model.poll.PollMessage; import google.registry.model.pricing.StaticPremiumListPricingEngine; import google.registry.model.registrar.Registrar; import google.registry.model.registry.Registry; import google.registry.model.registry.Registry.TldState; import google.registry.model.registry.label.PremiumList; import google.registry.model.registry.label.ReservedList; import google.registry.model.reporting.HistoryEntry; import google.registry.model.smd.EncodedSignedMark; import google.registry.model.transfer.TransferData; import google.registry.model.transfer.TransferData.Builder; import google.registry.model.transfer.TransferData.TransferServerApproveEntity; import google.registry.model.transfer.TransferStatus; import google.registry.tmch.LordnTask; import org.joda.money.Money; import org.joda.time.DateTime; import java.util.List; /** Static utils for setting up test resources. */ public class DatastoreHelper { private static final Supplier DEFAULT_PREMIUM_LIST_CONTENTS = memoize(new Supplier() { @Override public String[] get() { return toArray( Splitter.on('\n').split( readResourceUtf8(DatastoreHelper.class, "default_premium_list_testdata.csv")), String.class); }}); public static HostResource newHostResource(String hostName) { return new HostResource.Builder() .setFullyQualifiedHostName(hostName) .setCurrentSponsorClientId("TheRegistrar") .setCreationTimeForTest(START_OF_TIME) .setRepoId(generateNewContactHostRoid()) .build(); } public static DomainResource newDomainResource(String domainName) { String repoId = generateNewDomainRoid(getTldFromSld(domainName)); return newDomainResource(domainName, repoId, persistActiveContact("contact1234")); } public static DomainResource newDomainResource(String domainName, ContactResource contact) { return newDomainResource( domainName, generateNewDomainRoid(getTldFromSld(domainName)), contact); } public static DomainResource newDomainResource( String domainName, String repoId, ContactResource contact) { Ref contactRef = Ref.create(contact); return new DomainResource.Builder() .setRepoId(repoId) .setFullyQualifiedDomainName(domainName) .setCreationClientId("TheRegistrar") .setCurrentSponsorClientId("TheRegistrar") .setCreationTimeForTest(START_OF_TIME) .setAuthInfo(DomainAuthInfo.create(PasswordAuth.create("2fooBAR"))) .setRegistrant(contactRef) .setContacts(ImmutableSet.of( DesignatedContact.create(Type.ADMIN, contactRef), DesignatedContact.create(Type.TECH, contactRef))) .setRegistrationExpirationTime(END_OF_TIME) .build(); } public static DomainApplication newDomainApplication(String domainName) { // This ensures that the domain application gets the next available repoId before the created // contact does, which is usually the applicationId 1. return newDomainApplication( domainName, generateNewDomainRoid(getTldFromSld(domainName)), persistActiveContact("contact1234"), LaunchPhase.SUNRISE); } public static DomainApplication newDomainApplication(String domainName, ContactResource contact) { return newDomainApplication(domainName, contact, LaunchPhase.SUNRISE); } public static DomainApplication newDomainApplication( String domainName, ContactResource contact, LaunchPhase phase) { return newDomainApplication( domainName, generateNewDomainRoid(getTldFromSld(domainName)), contact, phase); } public static DomainApplication newDomainApplication( String domainName, String repoId, ContactResource contact, LaunchPhase phase) { Ref contactRef = Ref.create(contact); return new DomainApplication.Builder() .setRepoId(repoId) .setFullyQualifiedDomainName(domainName) .setCurrentSponsorClientId("TheRegistrar") .setAuthInfo(DomainAuthInfo.create(PasswordAuth.create("2fooBAR"))) .setRegistrant(contactRef) .setContacts(ImmutableSet.of( DesignatedContact.create(Type.ADMIN, contactRef), DesignatedContact.create(Type.TECH, contactRef))) .setPhase(phase) .setApplicationStatus(VALIDATED) .addStatusValue(StatusValue.PENDING_CREATE) .build(); } public static DomainApplication newSunriseApplication(String domainName) { return newSunriseApplication(domainName, persistActiveContact("contact1234")); } public static DomainApplication newSunriseApplication( String domainName, ContactResource contact) { return newDomainApplication(domainName, contact, LaunchPhase.SUNRISE) .asBuilder() .setEncodedSignedMarks(ImmutableList.of(EncodedSignedMark.create("base64", "abcdef"))) .build(); } /** * Returns a newly created {@link ContactResource} for the given contactId (which is the foreign * key) with an auto-generated repoId. */ public static ContactResource newContactResource(String contactId) { return newContactResourceWithRoid(contactId, generateNewContactHostRoid()); } public static ContactResource newContactResourceWithRoid(String contactId, String repoId) { return new ContactResource.Builder() .setRepoId(repoId) .setContactId(contactId) .setCurrentSponsorClientId("TheRegistrar") .setAuthInfo(ContactAuthInfo.create(PasswordAuth.create("2fooBAR"))) .build(); } public static Registry newRegistry(String tld, String roidSuffix) { return newRegistry( tld, roidSuffix, ImmutableSortedMap.of(START_OF_TIME, TldState.GENERAL_AVAILABILITY)); } public static Registry newRegistry( String tld, String roidSuffix, ImmutableSortedMap tldStates) { return new Registry.Builder() .setTldStr(tld) .setRoidSuffix(roidSuffix) .setTldStateTransitions(tldStates) // Set billing costs to distinct small primes to avoid masking bugs in tests. .setRenewBillingCostTransitions(ImmutableSortedMap.of(START_OF_TIME, Money.of(USD, 11))) .setCreateBillingCost(Money.of(USD, 13)) .setRestoreBillingCost(Money.of(USD, 17)) .setServerStatusChangeBillingCost(Money.of(USD, 19)) // Always set a default premium list. Tests that don't want it can delete it. .setPremiumList(persistPremiumList(tld, DEFAULT_PREMIUM_LIST_CONTENTS.get())) .setPricingEngineClass(StaticPremiumListPricingEngine.class) .build(); } public static ContactResource persistActiveContact(String contactId) { return persistResource(newContactResource(contactId)); } public static ContactResource persistDeletedContact(String contactId, DateTime now) { return persistResource( newContactResource(contactId) .asBuilder() .setDeletionTime(now.minusDays(1)) .build()); } public static HostResource persistActiveHost(String hostName) { return persistResource(newHostResource(hostName)); } public static HostResource persistActiveSubordinateHost( String hostName, DomainResource superordinateDomain) { checkNotNull(superordinateDomain); return persistResource( newHostResource(hostName) .asBuilder() .setSuperordinateDomain(Ref.create(superordinateDomain)) .build()); } public static HostResource persistDeletedHost(String hostName, DateTime now) { return persistResource( newHostResource(hostName).asBuilder().setDeletionTime(now.minusDays(1)).build()); } public static DomainResource persistActiveDomain(String domainName) { return persistResource(newDomainResource(domainName)); } public static DomainApplication persistActiveDomainApplication(String domainName) { return persistResource(newDomainApplication(domainName)); } public static DomainApplication persistActiveDomainApplication( String domainName, ContactResource contact, LaunchPhase phase) { return persistResource(newDomainApplication(domainName, contact, phase)); } public static DomainApplication persistDeletedDomainApplication(String domainName, DateTime now) { return persistResource(newDomainApplication(domainName).asBuilder() .setDeletionTime(now.minusDays(1)).build()); } public static DomainResource persistDeletedDomain(String domainName, DateTime now) { return persistDomainAsDeleted(newDomainResource(domainName), now); } public static DomainResource persistDomainAsDeleted(DomainResource domain, DateTime now) { return persistResource(domain.asBuilder().setDeletionTime(now.minusDays(1)).build()); } /** Persists a domain and enqueues a LORDN task of the appropriate type for it. */ public static DomainResource persistDomainAndEnqueueLordn(final DomainResource domain) { final DomainResource persistedDomain = persistResource(domain); // Calls {@link LordnTask#enqueueDomainResourceTask} wrapped in an ofy transaction so that the // transaction time is set correctly. ofy().transactNew(new VoidWork() { @Override public void vrun() { LordnTask.enqueueDomainResourceTask(persistedDomain); }}); return persistedDomain; } public static ReservedList persistReservedList(String listName, String... lines) { return persistReservedList(listName, true, lines); } public static ReservedList persistReservedList( String listName, boolean shouldPublish, String... lines) { return persistResource( new ReservedList.Builder() .setName(listName) .setReservedListMapFromLines(ImmutableList.copyOf(lines)) .setShouldPublish(shouldPublish) .build()); } public static PremiumList persistPremiumList(String listName, String... lines) { Optional existing = PremiumList.get(listName); return persistPremiumList( (existing.isPresent() ? existing.get().asBuilder() : new PremiumList.Builder()) .setName(listName) .setPremiumListMapFromLines(ImmutableList.copyOf(lines)) .build()); } private static PremiumList persistPremiumList(PremiumList premiumList) { // Persist the list and its child entities directly, rather than using its helper method, so // that we can avoid writing commit logs. This would cause issues since many tests replace the // clock in Ofy with a non-advancing FakeClock, and commit logs currently require // monotonically increasing timestamps. ofy().saveWithoutBackup().entity(premiumList).now(); ofy().saveWithoutBackup().entities(premiumList.getPremiumListEntries().values()).now(); return premiumList; } /** Creates and persists a tld. */ public static void createTld(String tld) { createTld(tld, TldState.GENERAL_AVAILABILITY); } public static void createTld(String tld, String roidSuffix) { createTld(tld, roidSuffix, ImmutableSortedMap.of(START_OF_TIME, TldState.GENERAL_AVAILABILITY)); } /** Creates and persists the given TLDs. */ public static void createTlds(String... tlds) { for (String tld : tlds) { createTld(tld, TldState.GENERAL_AVAILABILITY); } } public static void createTld(String tld, TldState tldState) { createTld(tld, ImmutableSortedMap.of(START_OF_TIME, tldState)); } public static void createTld(String tld, ImmutableSortedMap tldStates) { createTld(tld, tld.replaceFirst(ACE_PREFIX_REGEX, "").toUpperCase(), tldStates); } public static void createTld( String tld, String roidSuffix, ImmutableSortedMap tldStates) { persistResource(newRegistry(tld, roidSuffix, tldStates)); allowRegistrarAccess("TheRegistrar", tld); allowRegistrarAccess("NewRegistrar", tld); } public static void deleteTld(String tld) { deleteResource(Registry.get(tld)); disallowRegistrarAccess("TheRegistrar", tld); disallowRegistrarAccess("NewRegistrar", tld); } public static void allowRegistrarAccess(String clientId, String tld) { Registrar registrar = Registrar.loadByClientId(clientId); persistResource( registrar.asBuilder().setAllowedTlds(union(registrar.getAllowedTlds(), tld)).build()); } private static void disallowRegistrarAccess(String clientId, String tld) { Registrar registrar = Registrar.loadByClientId(clientId); persistResource( registrar.asBuilder().setAllowedTlds(difference(registrar.getAllowedTlds(), tld)).build()); } private static Builder createTransferDataBuilder(DateTime requestTime, DateTime expirationTime) { return new TransferData.Builder() .setTransferStatus(TransferStatus.PENDING) .setGainingClientId("NewRegistrar") .setTransferRequestTime(requestTime) .setLosingClientId("TheRegistrar") .setPendingTransferExpirationTime(expirationTime); } private static Builder createTransferDataBuilder( DateTime requestTime, DateTime expirationTime, Integer extendedRegistrationYears) { return createTransferDataBuilder(requestTime, expirationTime) .setExtendedRegistrationYears(extendedRegistrationYears); } public static PollMessage.OneTime createPollMessageForImplicitTransfer( EppResource contact, HistoryEntry historyEntry, String clientId, DateTime requestTime, DateTime expirationTime, DateTime now) { return new PollMessage.OneTime.Builder() .setClientId(clientId) .setEventTime(expirationTime) .setMsg("Transfer server approved.") .setResponseData(ImmutableList.of( createTransferResponse(contact, createTransferDataBuilder(requestTime, expirationTime) .build(), now))) .setParent(historyEntry) .build(); } public static BillingEvent.OneTime createBillingEventForTransfer( DomainResource domain, HistoryEntry historyEntry, String gainingClientId, DateTime costLookupTime, DateTime eventTime, Integer extendedRegistrationYears) { return new BillingEvent.OneTime.Builder() .setReason(Reason.TRANSFER) .setTargetId(domain.getFullyQualifiedDomainName()) .setEventTime(eventTime) .setBillingTime( eventTime.plus(Registry.get(domain.getTld()).getTransferGracePeriodLength())) .setClientId("NewRegistrar") .setPeriodYears(extendedRegistrationYears) .setCost(getDomainRenewCost( domain.getFullyQualifiedDomainName(), costLookupTime, gainingClientId, extendedRegistrationYears)) .setParent(historyEntry) .build(); } public static ContactResource persistContactWithPendingTransfer( ContactResource contact, DateTime requestTime, DateTime expirationTime, DateTime now) { HistoryEntry historyEntryContactTransfer = persistResource( new HistoryEntry.Builder() .setType(HistoryEntry.Type.CONTACT_TRANSFER_REQUEST) .setParent(contact) .build()); return persistResource( contact.asBuilder() .setCurrentSponsorClientId("TheRegistrar") .addStatusValue(StatusValue.PENDING_TRANSFER) .setTransferData(createTransferDataBuilder(requestTime, expirationTime) .setPendingTransferExpirationTime(now.plus( RegistryEnvironment.get().config().getContactAutomaticTransferLength())) .setServerApproveEntities( ImmutableSet.>of( // Pretend it's 3 days since the request Key.create(persistResource( createPollMessageForImplicitTransfer( contact, historyEntryContactTransfer, "NewRegistrar", requestTime, expirationTime, now))), Key.create(persistResource( createPollMessageForImplicitTransfer( contact, historyEntryContactTransfer, "TheRegistrar", requestTime, expirationTime, now))))) .setTransferRequestTrid(Trid.create("transferClient-trid", "transferServer-trid")) .build()) .build()); } public static DomainResource persistDomainWithPendingTransfer( DomainResource domain, DateTime requestTime, DateTime expirationTime, DateTime extendedRegistrationExpirationTime, int extendedRegistrationYears, DateTime now) { HistoryEntry historyEntryDomainTransfer = persistResource( new HistoryEntry.Builder() .setType(HistoryEntry.Type.DOMAIN_TRANSFER_REQUEST) .setParent(domain) .build()); BillingEvent.OneTime transferBillingEvent = persistResource(createBillingEventForTransfer( domain, historyEntryDomainTransfer, "NewRegistrar", requestTime, expirationTime, extendedRegistrationYears)); BillingEvent.Recurring gainingClientAutorenewEvent = persistResource( new BillingEvent.Recurring.Builder() .setFlags(ImmutableSet.of(Flag.AUTO_RENEW)) .setReason(Reason.RENEW) .setTargetId(domain.getFullyQualifiedDomainName()) .setClientId("NewRegistrar") .setEventTime(extendedRegistrationExpirationTime) .setRecurrenceEndTime(END_OF_TIME) .setParent(historyEntryDomainTransfer) .build()); PollMessage.Autorenew gainingClientAutorenewPollMessage = persistResource( new PollMessage.Autorenew.Builder() .setTargetId(domain.getFullyQualifiedDomainName()) .setClientId("NewRegistrar") .setEventTime(extendedRegistrationExpirationTime) .setAutorenewEndTime(END_OF_TIME) .setMsg("Domain was auto-renewed.") .setParent(historyEntryDomainTransfer) .build()); // Modify the existing autorenew event to reflect the pending transfer. persistResource( domain.getAutorenewBillingEvent().get().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. if (domain.getAutorenewPollMessage().get().getEventTime().isBefore(expirationTime)) { persistResource( domain.getAutorenewPollMessage().get().asBuilder() .setAutorenewEndTime(expirationTime) .build()); } else { deleteResource(domain.getAutorenewPollMessage().get()); } Builder transferDataBuilder = createTransferDataBuilder( requestTime, expirationTime, extendedRegistrationYears); return persistResource(domain.asBuilder() .setCurrentSponsorClientId("TheRegistrar") .addStatusValue(StatusValue.PENDING_TRANSFER) .setTransferData(transferDataBuilder .setPendingTransferExpirationTime(expirationTime) .setServerApproveBillingEvent(Ref.create(transferBillingEvent)) .setServerApproveAutorenewEvent(Ref.create(gainingClientAutorenewEvent)) .setServerApproveAutorenewPollMessage(Ref.create(gainingClientAutorenewPollMessage)) .setServerApproveEntities(ImmutableSet.>of( Key.create(transferBillingEvent), Key.create(gainingClientAutorenewEvent), Key.create(gainingClientAutorenewPollMessage), Key.create(persistResource( createPollMessageForImplicitTransfer( domain, historyEntryDomainTransfer, "NewRegistrar", requestTime, expirationTime, now))), Key.create(persistResource( createPollMessageForImplicitTransfer( domain, historyEntryDomainTransfer, "TheRegistrar", requestTime, expirationTime, now))))) .setTransferRequestTrid(Trid.create("transferClient-trid", "transferServer-trid")) .setExtendedRegistrationYears(extendedRegistrationYears) .build()) .build()); } private static Iterable getBillingEvents() { return Iterables.concat( ofy().load().type(BillingEvent.OneTime.class), ofy().load().type(BillingEvent.Recurring.class), ofy().load().type(BillingEvent.Cancellation.class)); } private static Iterable getBillingEvents(EppResource resource) { return Iterables.concat( ofy().load().type(BillingEvent.OneTime.class).ancestor(resource), ofy().load().type(BillingEvent.Recurring.class).ancestor(resource), ofy().load().type(BillingEvent.Cancellation.class).ancestor(resource)); } /** Assert that the expected billing events are exactly the ones found in the fake datastore. */ public static void assertBillingEvents(BillingEvent... expected) throws Exception { assertThat(FluentIterable.from(getBillingEvents()).transform(BILLING_EVENT_ID_STRIPPER)) .containsExactlyElementsIn( FluentIterable.from(asList(expected)).transform(BILLING_EVENT_ID_STRIPPER)); } /** * Assert that the expected billing events are exactly the ones found for the given EppResource. */ public static void assertBillingEventsForResource( EppResource resource, BillingEvent... expected) throws Exception { assertThat(FluentIterable.from(getBillingEvents(resource)).transform(BILLING_EVENT_ID_STRIPPER)) .containsExactlyElementsIn( FluentIterable.from(asList(expected)).transform(BILLING_EVENT_ID_STRIPPER)); } /** Assert that there are no billing events. */ public static void assertNoBillingEvents() { assertThat(getBillingEvents()).isEmpty(); } /** Helper to effectively erase the billing event ID to facilitate comparison. */ public static final Function BILLING_EVENT_ID_STRIPPER = new Function() { @Override public BillingEvent apply(BillingEvent billingEvent) { // Can't use id=0 because that causes the builder to generate a new id. return billingEvent.asBuilder().setId(1L).build(); }}; public static ImmutableList getPollMessages() { return FluentIterable.from(ofy().load().type(PollMessage.class)).toList(); } public static ImmutableList getPollMessages(String clientId) { return FluentIterable .from(ofy().load().type(PollMessage.class).filter("clientId", clientId)) .toList(); } public static ImmutableList getPollMessages(String clientId, DateTime now) { return FluentIterable .from(ofy() .load() .type(PollMessage.class) .filter("clientId", clientId) .filter("eventTime <=", now.toDate())) .toList(); } /** Gets all PollMessages associated with the given EppResource. */ public static ImmutableList getPollMessages( EppResource resource, String clientId, DateTime now) { return FluentIterable .from(ofy() .load() .type(PollMessage.class) .ancestor(resource) .filter("clientId", clientId) .filter("eventTime <=", now.toDate())) .toList(); } public static PollMessage getOnlyPollMessage(String clientId) { return Iterables.getOnlyElement(getPollMessages(clientId)); } public static PollMessage getOnlyPollMessage(String clientId, DateTime now) { return Iterables.getOnlyElement(getPollMessages(clientId, now)); } public static PollMessage getOnlyPollMessage( String clientId, DateTime now, Class subType) { return Iterables.getOnlyElement(Iterables.filter(getPollMessages(clientId, now), subType)); } public static PollMessage getOnlyPollMessage( EppResource resource, String clientId, DateTime now, Class subType) { return Iterables.getOnlyElement( Iterables.filter(getPollMessages(resource, clientId, now), subType)); } /** Returns a newly allocated, globally unique domain repoId of the format HEX-TLD. */ public static String generateNewDomainRoid(String tld) { return createDomainRoid(ObjectifyService.allocateId(), tld); } /** * Returns a newly allocated, globally unique contact/host repoId of the format * HEX_TLD-ROID. */ public static String generateNewContactHostRoid() { return createContactHostRoid(ObjectifyService.allocateId()); } /** * Persists a test resource to Datastore and returns it. * *

Tests should always use this method (or the shortcut persist methods in this class) to * persist test data, to avoid potentially subtle bugs related to race conditions and a stale * ofy() session cache. Specifically, this method calls .now() on the save to force the write to * actually get sent to datastore (although it does not force it to be applied) and clears the * session cache. If necessary, this method also updates the relevant {@link EppResourceIndex}, * {@link ForeignKeyIndex} and {@link DomainApplicationIndex}. * *

Note: Your resource will not be enrolled in a commit log. If you want backups, use * {@link #persistResourceWithCommitLog(Object)}. */ public static R persistResource(final R resource) { return persistResource(resource, false); } /** Same as {@link #persistResource(Object)} with backups enabled. */ public static R persistResourceWithCommitLog(final R resource) { return persistResource(resource, true); } private static R persistResource(final R resource, final boolean wantBackup) { assertWithMessage("Attempting to persist a Builder is almost certainly an error in test code") .that(resource) .isNotInstanceOf(Buildable.Builder.class); ofy().transact(new VoidWork() { @Override public void vrun() { Saver saver = wantBackup ? ofy().save() : ofy().saveWithoutBackup(); saver.entity(resource); if (resource instanceof EppResource) { EppResource eppResource = (EppResource) resource; assertWithMessage("Cannot persist an EppResource with a missing repoId in tests") .that(eppResource.getRepoId()).isNotEmpty(); Key eppResourceKey = Key.create(eppResource); saver.entity(EppResourceIndex.create(eppResourceKey)); if (resource instanceof ForeignKeyedEppResource) { saver.entity(ForeignKeyIndex.create(eppResource, eppResource.getDeletionTime())); } if (resource instanceof DomainApplication) { saver.entity( DomainApplicationIndex.createUpdatedInstance((DomainApplication) resource)); } } }}); // Force the session to be cleared so that when we read it back, we read from the datastore // and not from the transaction cache or memcache. ofy().clearSessionCache(); return ofy().load().entity(resource).now(); } /** * Saves an {@link EppResource} with partial history and commit log entries. * *

This was coded for testing RDE since its queries depend on the associated entries. * *

Warning: If you call this multiple times in a single test, you need to inject Ofy's * clock field and forward it by a millisecond between each subsequent call. * * @see #persistResource(Object) */ public static R persistEppResource(final R resource) { checkState(!ofy().inTransaction()); ofy().transact(new VoidWork() { @Override public void vrun() { ofy().save().entities( resource, new HistoryEntry.Builder() .setParent(resource) .setType(getHistoryEntryType(resource)) .setModificationTime(ofy().getTransactionTime()) .build()); ofy().save().entity(ForeignKeyIndex.create(resource, resource.getDeletionTime())); }}); ofy().clearSessionCache(); return ofy().load().entity(resource).safe(); } /** Returns all of the history entries that are parented off the given EppResource. */ public static List getHistoryEntries(EppResource resource) { return ofy().load() .type(HistoryEntry.class) .ancestor(resource) .order("modificationTime") .list(); } /** * Returns all of the history entries that are parented off the given EppResource with the given * type. */ public static List getHistoryEntriesOfType( EppResource resource, final HistoryEntry.Type type) { return FluentIterable.from(getHistoryEntries(resource)) .filter(new Predicate() { @Override public boolean apply(HistoryEntry entry) { return entry.getType() == type; }}) .toList(); } /** * Returns the only history entry of the given type, and throws an AssertionError if there are * zero or more than one. */ public static HistoryEntry getOnlyHistoryEntryOfType( EppResource resource, final HistoryEntry.Type type) { List historyEntries = getHistoryEntriesOfType(resource, type); assertThat(historyEntries).hasSize(1); return historyEntries.get(0); } private static HistoryEntry.Type getHistoryEntryType(EppResource resource) { if (resource instanceof ContactResource) { return resource.getRepoId() != null ? 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; } else if (resource instanceof DomainResource) { return resource.getRepoId() != null ? HistoryEntry.Type.DOMAIN_CREATE : HistoryEntry.Type.DOMAIN_UPDATE; } else { throw new AssertionError(); } } public static PollMessage getOnlyPollMessageForHistoryEntry(HistoryEntry historyEntry) { return Iterables.getOnlyElement(ofy().load() .type(PollMessage.class) .ancestor(historyEntry)); } public static HistoryEntry createHistoryEntryForEppResource( T parentResource) { return persistResource(new HistoryEntry.Builder().setParent(parentResource).build()); } /** Persists a single Objectify resource, without adjusting foreign resources or keys. */ public static R persistSimpleResource(final R resource) { return persistSimpleResources(ImmutableList.of(resource)).get(0); } /** * Persists a single Objectify resource in the global namespace, without adjusting foreign * resources or keys. */ public static R persistSimpleGlobalResource(final R resource) { return persistSimpleResources(ImmutableList.of(resource)).get(0); } /** * Like persistResource but for multiple entities, with no helper for saving * ForeignKeyedEppResources. All entities are persisted into the global namespace. * * @see "http://docs.objectify-appengine.googlecode.com/git/apidocs/com/googlecode/objectify/cmd/Loader.htmls#entities(java.lang.Iterable)" */ public static ImmutableList persistSimpleGlobalResources(Iterable resources) { return persistSimpleResources(resources); } /** * Like persistResource but for multiple entities, with no helper for saving * ForeignKeyedEppResources. * * @see "http://docs.objectify-appengine.googlecode.com/git/apidocs/com/googlecode/objectify/cmd/Loader.htmls#entities(java.lang.Iterable)" */ public static ImmutableList persistSimpleResources(final Iterable resources) { ofy().transact(new VoidWork(){ @Override public void vrun() { ofy().saveWithoutBackup().entities(resources); }}); // Force the session to be cleared so that when we read it back, we read from the datastore // and not from the transaction cache or memcache. ofy().clearSessionCache(); return ImmutableList.copyOf(ofy().load().entities(resources).values()); } public static void deleteResource(final Object resource) { ofy().deleteWithoutBackup().entity(resource).now(); // Force the session to be cleared so that when we read it back, we read from the datastore and // not from the transaction cache or memcache. ofy().clearSessionCache(); } /** Force the create and update timestamps to get written into the resource. **/ public static R cloneAndSetAutoTimestamps(final R resource) { return ofy().transact(new Work() { @Override public R run() { return ofy().load().fromEntity(ofy().save().toEntity(resource)); }}); } private DatastoreHelper() {} }