diff --git a/java/google/registry/rde/imports/BUILD b/java/google/registry/rde/imports/BUILD index a7b372e17..b6f0516b0 100644 --- a/java/google/registry/rde/imports/BUILD +++ b/java/google/registry/rde/imports/BUILD @@ -10,9 +10,11 @@ java_library( deps = [ "//java/google/registry/config", "//java/google/registry/dns", + "//java/google/registry/flows", "//java/google/registry/gcs", "//java/google/registry/mapreduce", "//java/google/registry/model", + "//java/google/registry/pricing", "//java/google/registry/request", "//java/google/registry/util", "//java/google/registry/xjc", @@ -28,5 +30,6 @@ java_library( "@com_google_guava", "@javax_servlet_api", "@joda_time", + "@org_joda_money", ], ) diff --git a/java/google/registry/rde/imports/RdeContactImportAction.java b/java/google/registry/rde/imports/RdeContactImportAction.java index 451fccedc..e97ef23d1 100644 --- a/java/google/registry/rde/imports/RdeContactImportAction.java +++ b/java/google/registry/rde/imports/RdeContactImportAction.java @@ -140,7 +140,7 @@ public class RdeContactImportAction implements Runnable { public void vrun() { ContactResource contact = XjcToContactResourceConverter.convertContact(xjcContact); - getImportUtils().importContact(contact); + getImportUtils().importEppResource(contact); } }); // Record number of contacts imported diff --git a/java/google/registry/rde/imports/RdeDomainImportAction.java b/java/google/registry/rde/imports/RdeDomainImportAction.java index ee9949304..3eaa4d636 100644 --- a/java/google/registry/rde/imports/RdeDomainImportAction.java +++ b/java/google/registry/rde/imports/RdeDomainImportAction.java @@ -14,10 +14,18 @@ package google.registry.rde.imports; +import static google.registry.flows.domain.DomainTransferUtils.createLosingTransferPollMessage; +import static google.registry.flows.domain.DomainTransferUtils.createPendingTransferData; +import static google.registry.flows.domain.DomainTransferUtils.createTransferServerApproveEntities; import static google.registry.mapreduce.MapreduceRunner.PARAM_MAP_SHARDS; import static google.registry.model.ofy.ObjectifyService.ofy; +import static google.registry.pricing.PricingEngineProxy.getDomainRenewCost; +import static google.registry.rde.imports.RdeImportUtils.createAutoRenewBillingEventForDomainImport; +import static google.registry.rde.imports.RdeImportUtils.createAutoRenewPollMessageForDomainImport; +import static google.registry.rde.imports.RdeImportUtils.createHistoryEntryForDomainImport; import static google.registry.rde.imports.RdeImportsModule.PATH; import static google.registry.util.PipelineUtils.createJobPath; +import static google.registry.util.PreconditionsUtils.checkArgumentNotNull; import com.google.appengine.tools.cloudstorage.GcsService; import com.google.appengine.tools.cloudstorage.GcsServiceFactory; @@ -25,13 +33,20 @@ import com.google.appengine.tools.cloudstorage.RetryParams; import com.google.appengine.tools.mapreduce.Mapper; import com.google.common.base.Optional; import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSet; import com.googlecode.objectify.VoidWork; import google.registry.config.RegistryConfig.Config; import google.registry.config.RegistryConfig.ConfigModule; import google.registry.dns.DnsQueue; import google.registry.gcs.GcsUtils; import google.registry.mapreduce.MapreduceRunner; +import google.registry.model.billing.BillingEvent; import google.registry.model.domain.DomainResource; +import google.registry.model.poll.PollMessage; +import google.registry.model.reporting.HistoryEntry; +import google.registry.model.transfer.TransferData; +import google.registry.model.transfer.TransferData.TransferServerApproveEntity; +import google.registry.model.transfer.TransferStatus; import google.registry.request.Action; import google.registry.request.Parameter; import google.registry.request.Response; @@ -41,6 +56,7 @@ import google.registry.xjc.JaxbFragment; import google.registry.xjc.rdedomain.XjcRdeDomain; import google.registry.xjc.rdedomain.XjcRdeDomainElement; import javax.inject.Inject; +import org.joda.money.Money; /** * A mapreduce that imports domains from an escrow file. @@ -145,17 +161,70 @@ public class RdeDomainImportAction implements Runnable { public void map(JaxbFragment fragment) { final XjcRdeDomain xjcDomain = fragment.getInstance().getValue(); try { - logger.infofmt("Converting xml for domain %s", xjcDomain.getName()); // Record number of attempted map operations getContext().incrementCounter("domain imports attempted"); + logger.infofmt("Saving domain %s", xjcDomain.getName()); ofy().transact(new VoidWork() { @Override public void vrun() { - DomainResource domain = - XjcToDomainResourceConverter.convertDomain(xjcDomain); - getImportUtils().importDomain(domain); + HistoryEntry historyEntry = createHistoryEntryForDomainImport(xjcDomain); + BillingEvent.Recurring autorenewBillingEvent = + createAutoRenewBillingEventForDomainImport(xjcDomain, historyEntry); + PollMessage.Autorenew autorenewPollMessage = + createAutoRenewPollMessageForDomainImport(xjcDomain, historyEntry); + DomainResource domain = XjcToDomainResourceConverter.convertDomain( + xjcDomain, autorenewBillingEvent, autorenewPollMessage); getDnsQueue().addDomainRefreshTask(domain.getFullyQualifiedDomainName()); + // Keep a list of "extra objects" that need to be saved along with the domain + // and add to it if necessary. + ImmutableSet extraEntitiesToSave = + getImportUtils().createIndexesForEppResource(domain); + // Create speculative server approval entities for pending transfers + if (domain.getTransferData().getTransferStatus() == TransferStatus.PENDING) { + TransferData transferData = domain.getTransferData(); + checkArgumentNotNull(transferData, + "Domain %s is in pending transfer but has no transfer data", + domain.getFullyQualifiedDomainName()); + Money transferCost = getDomainRenewCost( + domain.getFullyQualifiedDomainName(), + transferData.getPendingTransferExpirationTime(), + transferData.getExtendedRegistrationYears()); + // Create speculative entities in anticipation of an automatic server approval. + ImmutableSet serverApproveEntities = + createTransferServerApproveEntities( + transferData.getPendingTransferExpirationTime(), + domain.getRegistrationExpirationTime() + .plusYears(transferData.getExtendedRegistrationYears()), + historyEntry, + domain, + historyEntry.getTrid(), + transferData.getGainingClientId(), + transferCost, + transferData.getExtendedRegistrationYears(), + transferData.getTransferRequestTime()); + transferData = + createPendingTransferData(transferData.asBuilder(), serverApproveEntities); + // Create a poll message to notify the losing registrar that a transfer was requested. + PollMessage requestPollMessage = createLosingTransferPollMessage(domain.getRepoId(), + transferData, transferData.getPendingTransferExpirationTime(), historyEntry) + .asBuilder().setEventTime(transferData.getTransferRequestTime()).build(); + domain = domain.asBuilder().setTransferData(transferData).build(); + autorenewBillingEvent = autorenewBillingEvent.asBuilder() + .setRecurrenceEndTime(transferData.getPendingTransferExpirationTime()).build(); + autorenewPollMessage = autorenewPollMessage.asBuilder() + .setAutorenewEndTime(transferData.getPendingTransferExpirationTime()).build(); + extraEntitiesToSave = new ImmutableSet.Builder<>() + .add(requestPollMessage) + .addAll(extraEntitiesToSave) + .addAll(serverApproveEntities).build(); + } // End pending transfer check + ofy().save() + .entities(new ImmutableSet.Builder<>() + .add(domain, historyEntry, autorenewBillingEvent, autorenewPollMessage) + .addAll(extraEntitiesToSave) + .build()) + .now(); } }); // Record the number of domains imported diff --git a/java/google/registry/rde/imports/RdeHostImportAction.java b/java/google/registry/rde/imports/RdeHostImportAction.java index dfe1db13c..bc81bb18d 100644 --- a/java/google/registry/rde/imports/RdeHostImportAction.java +++ b/java/google/registry/rde/imports/RdeHostImportAction.java @@ -125,7 +125,7 @@ public class RdeHostImportAction implements Runnable { @Override public void vrun() { HostResource host = XjcToHostResourceConverter.convert(xjcHost); - getImportUtils().importHost(host); + getImportUtils().importEppResource(host); } }); // Record number of hosts imported diff --git a/java/google/registry/rde/imports/RdeImportUtils.java b/java/google/registry/rde/imports/RdeImportUtils.java index 32a7385ed..1e3a1c0f7 100644 --- a/java/google/registry/rde/imports/RdeImportUtils.java +++ b/java/google/registry/rde/imports/RdeImportUtils.java @@ -16,34 +16,47 @@ package google.registry.rde.imports; import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Preconditions.checkState; +import static google.registry.util.DateTimeUtils.END_OF_TIME; import static google.registry.util.DateTimeUtils.START_OF_TIME; +import static java.nio.charset.StandardCharsets.UTF_8; import com.google.appengine.tools.cloudstorage.GcsFilename; +import com.google.common.collect.ImmutableSet; import com.google.common.io.BaseEncoding; import com.googlecode.objectify.Key; import google.registry.config.RegistryConfig.Config; import google.registry.gcs.GcsUtils; import google.registry.model.EppResource; -import google.registry.model.contact.ContactResource; +import google.registry.model.EppResource.ForeignKeyedEppResource; +import google.registry.model.billing.BillingEvent; +import google.registry.model.billing.BillingEvent.Flag; +import google.registry.model.billing.BillingEvent.Reason; import google.registry.model.domain.DomainResource; import google.registry.model.eppcommon.Trid; -import google.registry.model.host.HostResource; import google.registry.model.index.EppResourceIndex; import google.registry.model.index.ForeignKeyIndex; import google.registry.model.ofy.Ofy; +import google.registry.model.poll.PollMessage; import google.registry.model.registrar.Registrar; import google.registry.model.registry.Registry; import google.registry.model.registry.Registry.RegistryNotFoundException; import google.registry.model.registry.Registry.TldState; +import google.registry.model.reporting.HistoryEntry; import google.registry.util.Clock; import google.registry.util.FormattingLogger; +import google.registry.xjc.XjcXmlTransformer; +import google.registry.xjc.rdedomain.XjcRdeDomain; +import google.registry.xjc.rdedomain.XjcRdeDomainElement; import google.registry.xjc.rderegistrar.XjcRdeRegistrar; +import google.registry.xml.XmlException; +import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; import java.util.UUID; import javax.inject.Inject; import javax.xml.bind.JAXBException; import javax.xml.stream.XMLStreamException; +import org.joda.time.DateTime; /** * Utility functions for escrow file import. @@ -66,10 +79,11 @@ public class RdeImportUtils { this.escrowBucketName = escrowBucketName; } - private void importEppResource(final T resource, final String type) { + public ImmutableSet + createIndexesForEppResource(T resource) { @SuppressWarnings("unchecked") Class resourceClass = (Class) resource.getClass(); - EppResource existing = ofy.load().key(Key.create(resource)).now(); + Object existing = ofy.load().key(Key.create(resource)).now(); if (existing != null) { // This will roll back the transaction and prevent duplicate history entries from being saved. throw new ResourceExistsException(); @@ -80,51 +94,30 @@ public class RdeImportUtils { checkState( existingForeignKeyIndex == null, "New %s resource has existing foreign key index; foreignKey=%s, repoId=%s", - type, + resource.getClass().getCanonicalName(), resource.getForeignKey(), resource.getRepoId()); - ofy.save().entity(resource); - ofy.save().entity(ForeignKeyIndex.create(resource, resource.getDeletionTime())); - ofy.save().entity(EppResourceIndex.create(Key.create(resource))); - logger.infofmt( - "Imported %s resource - ROID=%s, id=%s", - type, resource.getRepoId(), resource.getForeignKey()); + return ImmutableSet.of(ForeignKeyIndex.create(resource, resource.getDeletionTime()), + EppResourceIndex.create(Key.create(resource))); } /** - * Imports a host from an escrow file. + * Imports a resource from an escrow file. * *

The host will only be imported if it has not been previously imported. * *

If the host is imported, {@link ForeignKeyIndex} and {@link EppResourceIndex} are also * created. */ - public void importHost(final HostResource resource) { - importEppResource(resource, "host"); - } - - /** - * Imports a contact from an escrow file. - * - *

The contact will only be imported if it has not been previously imported. - * - *

If the contact is imported, {@link ForeignKeyIndex} and {@link EppResourceIndex} are also - * created. - */ - public void importContact(final ContactResource resource) { - importEppResource(resource, "contact"); - } - - /** - * Imports a domain from an escrow file. - * - *

The domain will only be imported if it has not been previously imported. - * - *

If the domain is imported, {@link ForeignKeyIndex} and {@link EppResourceIndex} are also - * created. - */ - public void importDomain(final DomainResource resource) { - importEppResource(resource, "domain"); + public void + importEppResource(final T resource) { + ofy.save().entities(new ImmutableSet.Builder<>() + .add(resource) + .addAll(createIndexesForEppResource(resource)) + .build()); + logger.infofmt( + "Imported %s resource - ROID=%s, id=%s", + resource.getClass().getCanonicalName(), resource.getRepoId(), resource.getForeignKey()); } /** @@ -184,4 +177,59 @@ public class RdeImportUtils { return Trid.create( "Import_" + BaseEncoding.base64().encode(UUID.randomUUID().toString().getBytes())); } + + public static BillingEvent.Recurring createAutoRenewBillingEventForDomainImport( + XjcRdeDomain domain, HistoryEntry historyEntry) { + final BillingEvent.Recurring billingEvent = + new BillingEvent.Recurring.Builder() + .setReason(Reason.RENEW) + .setFlags(ImmutableSet.of(Flag.AUTO_RENEW)) + .setTargetId(domain.getRoid()) + .setClientId(domain.getClID()) + .setEventTime(domain.getExDate()) + .setRecurrenceEndTime(END_OF_TIME) + .setParent(historyEntry) + .build(); + return billingEvent; + } + + public static PollMessage.Autorenew createAutoRenewPollMessageForDomainImport( + XjcRdeDomain domain, HistoryEntry historyEntry) { + final PollMessage.Autorenew pollMessage = + new PollMessage.Autorenew.Builder() + .setTargetId(domain.getRoid()) + .setClientId(domain.getClID()) + .setEventTime(domain.getExDate()) + .setMsg("Domain was auto-renewed.") + .setParent(historyEntry) + .build(); + return pollMessage; + } + + public static HistoryEntry createHistoryEntryForDomainImport(XjcRdeDomain domain) { + XjcRdeDomainElement element = new XjcRdeDomainElement(domain); + final HistoryEntry historyEntry = + new HistoryEntry.Builder() + .setType(HistoryEntry.Type.RDE_IMPORT) + .setClientId(domain.getClID()) + .setTrid(generateTridForImport()) + .setModificationTime(DateTime.now()) + .setXmlBytes(getObjectXml(element)) + .setBySuperuser(true) + .setReason("RDE Import") + .setRequestedByRegistrar(false) + .setParent(Key.create(null, DomainResource.class, domain.getRoid())) + .build(); + return historyEntry; + } + + public static byte[] getObjectXml(Object jaxbElement) { + try { + ByteArrayOutputStream bout = new ByteArrayOutputStream(); + XjcXmlTransformer.marshalLenient(jaxbElement, bout, UTF_8); + return bout.toByteArray(); + } catch (XmlException e) { + throw new RuntimeException(e); + } + } } diff --git a/java/google/registry/rde/imports/XjcToDomainResourceConverter.java b/java/google/registry/rde/imports/XjcToDomainResourceConverter.java index fd3a55888..a8e4056f8 100644 --- a/java/google/registry/rde/imports/XjcToDomainResourceConverter.java +++ b/java/google/registry/rde/imports/XjcToDomainResourceConverter.java @@ -17,9 +17,6 @@ package google.registry.rde.imports; import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Preconditions.checkState; import static com.google.common.collect.Iterables.transform; -import static google.registry.model.ofy.ObjectifyService.ofy; -import static google.registry.rde.imports.RdeImportUtils.generateTridForImport; -import static google.registry.util.DateTimeUtils.END_OF_TIME; import static google.registry.util.DomainNameUtils.canonicalizeDomainName; import static google.registry.util.PreconditionsUtils.checkArgumentNotNull; @@ -29,10 +26,7 @@ import com.google.common.collect.ImmutableSet; import com.google.common.collect.Iterables; import com.google.common.net.InternetDomainName; import com.googlecode.objectify.Key; -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.ContactResource; import google.registry.model.domain.DesignatedContact; import google.registry.model.domain.DomainAuthInfo; @@ -47,7 +41,6 @@ import google.registry.model.index.ForeignKeyIndex; import google.registry.model.poll.PollMessage; import google.registry.model.registry.Registries; import google.registry.model.registry.Registry; -import google.registry.model.reporting.HistoryEntry; import google.registry.model.transfer.TransferData; import google.registry.model.transfer.TransferStatus; import google.registry.util.NonFinalForTesting; @@ -67,6 +60,7 @@ import java.security.ProviderException; import java.security.SecureRandom; import java.util.Random; import org.joda.time.DateTime; +import org.joda.time.Years; /** Utility class that converts an {@link XjcRdeDomainElement} into a {@link DomainResource}. */ final class XjcToDomainResourceConverter extends XjcToEppResourceConverter { @@ -184,14 +178,10 @@ final class XjcToDomainResourceConverter extends XjcToEppResourceConverter { } /** Converts {@link XjcRdeDomain} to {@link DomainResource}. */ - static DomainResource convertDomain(XjcRdeDomain domain) { - // First create history entry and autorenew billing event/poll message - // Autorenew billing event is required for creating AUTO_RENEW grace period - HistoryEntry historyEntry = createHistoryEntry(domain); - BillingEvent.Recurring autoRenewBillingEvent = - createAutoRenewBillingEvent(domain, historyEntry); - PollMessage.Autorenew pollMessage = createAutoRenewPollMessage(domain, historyEntry); - ofy().save().entities(historyEntry, autoRenewBillingEvent, pollMessage); + static DomainResource convertDomain( + XjcRdeDomain domain, + BillingEvent.Recurring autoRenewBillingEvent, + PollMessage.Autorenew autoRenewPollMessage) { GracePeriodConverter gracePeriodConverter = new GracePeriodConverter(domain, Key.create(autoRenewBillingEvent)); DomainResource.Builder builder = @@ -202,7 +192,7 @@ final class XjcToDomainResourceConverter extends XjcToEppResourceConverter { .setCurrentSponsorClientId(domain.getClID()) .setCreationClientId(domain.getCrRr().getValue()) .setCreationTime(domain.getCrDate()) - .setAutorenewPollMessage(Key.create(pollMessage)) + .setAutorenewPollMessage(Key.create(autoRenewPollMessage)) .setAutorenewBillingEvent(Key.create(autoRenewBillingEvent)) .setRegistrationExpirationTime(domain.getExDate()) .setLastEppUpdateTime(domain.getUpDate()) @@ -218,7 +208,7 @@ final class XjcToDomainResourceConverter extends XjcToEppResourceConverter { ? ImmutableSet.of() : ImmutableSet.copyOf( transform(domain.getSecDNS().getDsDatas(), SECDNS_CONVERTER))) - .setTransferData(convertDomainTransferData(domain.getTrnData())) + .setTransferData(convertDomainTransferData(domain.getTrnData(), domain.getExDate())) // authInfo pw must be a token between 6 and 16 characters in length // generate a token of 16 characters as the default authInfo pw .setAuthInfo(DomainAuthInfo @@ -229,51 +219,6 @@ final class XjcToDomainResourceConverter extends XjcToEppResourceConverter { return builder.build(); } - private static BillingEvent.Recurring createAutoRenewBillingEvent( - XjcRdeDomain domain, HistoryEntry historyEntry) { - final BillingEvent.Recurring billingEvent = - new BillingEvent.Recurring.Builder() - .setReason(Reason.RENEW) - .setFlags(ImmutableSet.of(Flag.AUTO_RENEW)) - .setTargetId(domain.getRoid()) - .setClientId(domain.getClID()) - .setEventTime(domain.getExDate()) - .setRecurrenceEndTime(END_OF_TIME) - .setParent(historyEntry) - .build(); - return billingEvent; - } - - private static PollMessage.Autorenew createAutoRenewPollMessage( - XjcRdeDomain domain, HistoryEntry historyEntry) { - final PollMessage.Autorenew pollMessage = - new PollMessage.Autorenew.Builder() - .setTargetId(domain.getRoid()) - .setClientId(domain.getClID()) - .setEventTime(domain.getExDate()) - .setMsg("Domain was auto-renewed.") - .setParent(historyEntry) - .build(); - return pollMessage; - } - - private static HistoryEntry createHistoryEntry(XjcRdeDomain domain) { - XjcRdeDomainElement element = new XjcRdeDomainElement(domain); - final HistoryEntry historyEntry = - new HistoryEntry.Builder() - .setType(HistoryEntry.Type.RDE_IMPORT) - .setClientId(domain.getClID()) - .setTrid(generateTridForImport()) - .setModificationTime(DateTime.now()) - .setXmlBytes(getObjectXml(element)) - .setBySuperuser(true) - .setReason("RDE Import") - .setRequestedByRegistrar(false) - .setParent(Key.create(null, DomainResource.class, domain.getRoid())) - .build(); - return historyEntry; - } - /** Returns {@link Key} for registrant from foreign key */ private static Key convertRegistrant(String contactId) { Key key = @@ -296,16 +241,34 @@ final class XjcToDomainResourceConverter extends XjcToEppResourceConverter { } /** Converts {@link XjcRdeDomainTransferDataType} to {@link TransferData}. */ - private static TransferData convertDomainTransferData(XjcRdeDomainTransferDataType data) { + private static TransferData convertDomainTransferData(XjcRdeDomainTransferDataType data, + DateTime domainExpiration) { if (data == null) { return TransferData.EMPTY; } + // If the transfer is pending, calculate the number of years to add to the domain expiration + // on approval of the transfer. + TransferStatus transferStatus = TRANSFER_STATUS_MAPPER.xmlToEnum(data.getTrStatus().value()); + // Get new expiration date + DateTime newExpirationTime = domainExpiration; + if (transferStatus == TransferStatus.PENDING) { + // Default to domain expiration time plus one year if no expiration is specified + if (data.getExDate() == null) { + newExpirationTime = newExpirationTime.plusYears(1); + } else { + newExpirationTime = data.getExDate(); + } + } return new TransferData.Builder() - .setTransferStatus(TRANSFER_STATUS_MAPPER.xmlToEnum(data.getTrStatus().value())) + .setTransferStatus(transferStatus) .setGainingClientId(data.getReRr().getValue()) .setLosingClientId(data.getAcRr().getValue()) .setTransferRequestTime(data.getReDate()) .setPendingTransferExpirationTime(data.getAcDate()) + // This will be wrong for domains that are not in pending transfer, + // but there isn't a reliable way to calculate it. + .setExtendedRegistrationYears( + Years.yearsBetween(domainExpiration, newExpirationTime).getYears()) .build(); } diff --git a/javatests/google/registry/rde/imports/BUILD b/javatests/google/registry/rde/imports/BUILD index 541535b00..1f3c84df1 100644 --- a/javatests/google/registry/rde/imports/BUILD +++ b/javatests/google/registry/rde/imports/BUILD @@ -31,6 +31,7 @@ java_library( "@com_google_truth", "@joda_time", "@junit", + "@org_joda_money", "@org_mockito_all", ], ) diff --git a/javatests/google/registry/rde/imports/RdeDomainImportActionTest.java b/javatests/google/registry/rde/imports/RdeDomainImportActionTest.java index 30f96ca5a..bc247c015 100644 --- a/javatests/google/registry/rde/imports/RdeDomainImportActionTest.java +++ b/javatests/google/registry/rde/imports/RdeDomainImportActionTest.java @@ -18,11 +18,15 @@ import static com.google.common.truth.Truth.assertThat; import static google.registry.model.ofy.ObjectifyService.ofy; import static google.registry.testing.DatastoreHelper.createTld; import static google.registry.testing.DatastoreHelper.getHistoryEntries; +import static google.registry.testing.DatastoreHelper.getPollMessages; import static google.registry.testing.DatastoreHelper.newDomainResource; import static google.registry.testing.DatastoreHelper.persistActiveContact; import static google.registry.testing.DatastoreHelper.persistResource; import static google.registry.testing.DatastoreHelper.persistSimpleResource; import static google.registry.testing.TaskQueueHelper.assertDnsTasksEnqueued; +import static google.registry.util.DateTimeUtils.END_OF_TIME; +import static org.joda.money.CurrencyUnit.USD; +import static org.junit.Assert.fail; import com.google.appengine.tools.cloudstorage.GcsFilename; import com.google.appengine.tools.cloudstorage.GcsService; @@ -35,9 +39,13 @@ import com.googlecode.objectify.Key; import google.registry.config.RegistryConfig.ConfigModule; import google.registry.gcs.GcsUtils; import google.registry.mapreduce.MapreduceRunner; +import google.registry.model.billing.BillingEvent; import google.registry.model.domain.DomainResource; +import google.registry.model.domain.rgp.GracePeriodStatus; import google.registry.model.eppcommon.Trid; +import google.registry.model.poll.PollMessage; import google.registry.model.reporting.HistoryEntry; +import google.registry.model.transfer.TransferStatus; import google.registry.request.Response; import google.registry.testing.FakeResponse; import google.registry.testing.mapreduce.MapreduceTestCase; @@ -45,7 +53,10 @@ import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.util.List; +import javax.annotation.Nullable; +import org.joda.money.Money; import org.joda.time.DateTime; +import org.joda.time.Seconds; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @@ -56,6 +67,8 @@ import org.mockito.runners.MockitoJUnitRunner; public class RdeDomainImportActionTest extends MapreduceTestCase { private static final ByteSource DEPOSIT_1_DOMAIN = RdeImportsTestData.get("deposit_1_domain.xml"); + private static final ByteSource DEPOSIT_1_DOMAIN_PENDING_TRANSFER = + RdeImportsTestData.get("deposit_1_domain_pending_transfer.xml"); private static final String IMPORT_BUCKET_NAME = "import-bucket"; private static final String IMPORT_FILE_NAME = "escrow-file.xml"; @@ -82,7 +95,7 @@ public class RdeDomainImportActionTest extends MapreduceTestCase domains = ofy().load().type(DomainResource.class).list(); @@ -91,7 +104,7 @@ public class RdeDomainImportActionTest extends MapreduceTestCase domains = ofy().load().type(DomainResource.class).list(); @@ -104,7 +117,7 @@ public class RdeDomainImportActionTest extends MapreduceTestCase domains = ofy().load().type(DomainResource.class).list(); + assertThat(domains).hasSize(1); + checkDomain(domains.get(0)); + // implicit server approval happens at 2015-01-08T22:00:00.0Z + DateTime serverApprovalTime = DateTime.parse("2015-01-08T22:00:00.0Z"); + // Domain should be assigned to RegistrarX before server approval + DomainResource beforeApproval = + domains.get(0).cloneProjectedAtTime(serverApprovalTime.minus(Seconds.ONE)); + assertThat(beforeApproval.getCurrentSponsorClientId()).isEqualTo("RegistrarX"); + assertThat(loadAutorenewBillingEventForDomain(beforeApproval).getClientId()) + .isEqualTo("RegistrarX"); + assertThat(loadAutorenewPollMessageForDomain(beforeApproval).getClientId()) + .isEqualTo("RegistrarX"); + // Current expiration is 2015-04-03T22:00:00.0Z + assertThat(beforeApproval.getRegistrationExpirationTime()) + .isEqualTo(DateTime.parse("2015-04-03T22:00:00.0Z")); + // Domain is not yet in transfer grace period + assertThat(beforeApproval.getGracePeriodStatuses()).doesNotContain(GracePeriodStatus.TRANSFER); + // Check autorenew events - recurrence end is set to transfer approval time, + // and client id is set to losing registrar. This is important in case the + // transfer is cancelled or rejected. + // Event time is set to domain expiration date. + checkAutorenewBillingEvent( + beforeApproval, + "RegistrarX", + DateTime.parse("2015-04-03T22:00:00.0Z"), + DateTime.parse("2015-01-08T22:00:00.0Z")); + checkAutorenewPollMessage( + beforeApproval, + "RegistrarX", + DateTime.parse("2015-04-03T22:00:00.0Z"), + DateTime.parse("2015-01-08T22:00:00.0Z")); + + // Domain should be assigned to RegistrarY after server approval + DomainResource afterApproval = + domains.get(0).cloneProjectedAtTime(serverApprovalTime.plus(Seconds.ONE)); + assertThat(afterApproval.getCurrentSponsorClientId()).isEqualTo("RegistrarY"); + assertThat(loadAutorenewBillingEventForDomain(afterApproval).getClientId()) + .isEqualTo("RegistrarY"); + assertThat(loadAutorenewPollMessageForDomain(afterApproval).getClientId()) + .isEqualTo("RegistrarY"); + // New expiration should be incremented by 1 year + assertThat(afterApproval.getRegistrationExpirationTime()) + .isEqualTo(DateTime.parse("2016-04-03T22:00:00.0Z")); + // Domain should now be in transfer grace period + assertThat(afterApproval.getGracePeriodStatuses()).contains(GracePeriodStatus.TRANSFER); + // Check autorenew events - recurrence end is set to END_OF_TIME, + // and client id is set to gaining registrar. This represents the new state of the domain, + // unless the transfer is cancelled during the grace period, at which time it will + // revert to the previous state. + // Event time is set to domain expiration date. + checkAutorenewBillingEvent( + afterApproval, "RegistrarY", DateTime.parse("2016-04-03T22:00:00.0Z"), END_OF_TIME); + checkAutorenewPollMessage( + afterApproval, "RegistrarY", DateTime.parse("2016-04-03T22:00:00.0Z"), END_OF_TIME); + } + + @Test + public void testMapreducePendingTransferEvents() throws Exception { + pushToGcs(DEPOSIT_1_DOMAIN_PENDING_TRANSFER); + runMapreduce(); + List domains = ofy().load().type(DomainResource.class).list(); + assertThat(domains).hasSize(1); + checkDomain(domains.get(0)); + checkTransferRequestPollMessage(domains.get(0), "RegistrarX", + DateTime.parse("2015-01-03T22:00:00.0Z")); + checkTransferServerApprovalPollMessage(domains.get(0), "RegistrarX", + DateTime.parse("2015-01-08T22:00:00.0Z")); + checkTransferServerApprovalPollMessage(domains.get(0), "RegistrarY", + DateTime.parse("2015-01-08T22:00:00.0Z")); + // Billing event is set to the end of the transfer grace period, 5 days after server approval + checkTransferBillingEvent(domains.get(0), DateTime.parse("2015-01-13T22:00:00.0Z")); + } + + private static void checkTransferBillingEvent( + DomainResource domain, DateTime automaticTransferTime) { + for (BillingEvent.OneTime event : + ofy().load().type(BillingEvent.OneTime.class).ancestor(domain).list()) { + if (event.getReason() == BillingEvent.Reason.TRANSFER) { + assertThat(event.getCost()).isEqualTo(Money.of(USD, 11)); + assertThat(event.getClientId()).isEqualTo("RegistrarY"); + assertThat(event.getBillingTime()).isEqualTo(automaticTransferTime); + } + } + } + + /** Verifies the existence of a transfer request poll message */ + private static void checkTransferRequestPollMessage( + DomainResource domain, String clientId, DateTime expectedAt) { + for (PollMessage message : getPollMessages(domain)) { + if (TransferStatus.PENDING.getMessage().equals(message.getMsg()) + && clientId.equals(message.getClientId()) + && expectedAt.equals(message.getEventTime())) { + return; + } + } + fail("Expected transfer request poll message"); + } + + /** Verifies the existence of a transfer request poll message */ + private static void checkTransferServerApprovalPollMessage( + DomainResource domain, String clientId, DateTime expectedAt) { + for (PollMessage message : getPollMessages(domain)) { + if (TransferStatus.SERVER_APPROVED.getMessage().equals(message.getMsg()) + && clientId.equals(message.getClientId()) + && expectedAt.equals(message.getEventTime())) { + return; + } + } + fail("Expected transfer request poll message"); + } + + /** Verifies autorenew {@link PollMessage} is correct */ + private static void checkAutorenewPollMessage( + DomainResource domain, String clientId, DateTime expectedAt, DateTime recurrenceEndTime) { + PollMessage.Autorenew autorenewPollMessage = loadAutorenewPollMessageForDomain(domain); + assertThat(autorenewPollMessage).isNotNull(); + assertThat(autorenewPollMessage.getClientId()).isEqualTo(clientId); + assertThat(autorenewPollMessage.getEventTime()).isEqualTo(expectedAt); + assertThat(autorenewPollMessage.getAutorenewEndTime()).isEqualTo(recurrenceEndTime); + } + + /** Verifies autorenew {@link BillingEvent} is correct */ + private static void checkAutorenewBillingEvent( + DomainResource domain, String clientId, DateTime expectedAt, DateTime recurrenceEndTime) { + BillingEvent.Recurring autorenewBillingEvent = loadAutorenewBillingEventForDomain(domain); + assertThat(autorenewBillingEvent).isNotNull(); + assertThat(autorenewBillingEvent.getClientId()).isEqualTo(clientId); + assertThat(autorenewBillingEvent.getEventTime()).isEqualTo(expectedAt); + assertThat(autorenewBillingEvent.getRecurrenceEndTime()).isEqualTo(recurrenceEndTime); + } + /** Verify history entry fields are correct */ - private void checkHistoryEntry(HistoryEntry entry, DomainResource parent) { + private static void checkHistoryEntry(HistoryEntry entry, DomainResource parent) { assertThat(entry.getType()).isEqualTo(HistoryEntry.Type.RDE_IMPORT); assertThat(entry.getClientId()).isEqualTo(parent.getCurrentSponsorClientId()); assertThat(entry.getXmlBytes().length).isGreaterThan(0); @@ -146,8 +298,8 @@ public class RdeDomainImportActionTest extends MapreduceTestCase() { - @Override - public DomainResource run() { - return XjcToDomainResourceConverter.convertDomain(xjcDomain); - }}); + final HistoryEntry historyEntry = createHistoryEntryForDomainImport(xjcDomain); + final BillingEvent.Recurring autorenewBillingEvent = + createAutoRenewBillingEventForDomainImport(xjcDomain, historyEntry); + final PollMessage.Autorenew autorenewPollMessage = + createAutoRenewPollMessageForDomainImport(xjcDomain, historyEntry); + return ofy() + .transact( + new Work() { + @SuppressWarnings("unchecked") + @Override + public DomainResource run() { + ofy().save().entities(historyEntry, autorenewBillingEvent, autorenewPollMessage); + return XjcToDomainResourceConverter.convertDomain( + xjcDomain, autorenewBillingEvent, autorenewPollMessage); + } + }); } private XjcRdeDomain loadDomainFromRdeXml(String filename) { diff --git a/javatests/google/registry/rde/imports/testdata/deposit_1_domain_pending_transfer.xml b/javatests/google/registry/rde/imports/testdata/deposit_1_domain_pending_transfer.xml new file mode 100644 index 000000000..66a898013 --- /dev/null +++ b/javatests/google/registry/rde/imports/testdata/deposit_1_domain_pending_transfer.xml @@ -0,0 +1,249 @@ + + + + 2010-10-17T00:00:00Z + + 1.0 + urn:ietf:params:xml:ns:rdeHeader-1.0 + urn:ietf:params:xml:ns:rdeContact-1.0 + urn:ietf:params:xml:ns:rdeHost-1.0 + urn:ietf:params:xml:ns:rdeDomain-1.0 + urn:ietf:params:xml:ns:rdeRegistrar-1.0 + urn:ietf:params:xml:ns:rdeIDN-1.0 + urn:ietf:params:xml:ns:rdeNNDN-1.0 + urn:ietf:params:xml:ns:rdeEppParams-1.0 + + + + + + + test + 2 + + 1 + + 1 + + 1 + + 1 + + 1 + + 1 + + + + + + example1.test + Dexample1-TEST + + jd1234 + sh8013 + sh8013 + RegistrarX + RegistrarX + 1999-04-03T22:00:00.0Z + 2015-04-03T22:00:00.0Z + RegistrarY + 2015-01-03T22:00:00.0Z + + pending + RegistrarY + 2015-01-03T22:00:00.0Z + RegistrarX + 2015-01-08T22:00:00.0Z + + + + + + ns1.example.com + Hns1_example_com-TEST + + + 192.0.2.2 + 192.0.2.29 + 1080:0:0:0:8:800:200C:417A + + RegistrarX + RegistrarX + 1999-05-08T12:10:00.0Z + RegistrarX + 2009-10-03T09:34:00.0Z + + + + + ns1.example1.test + Hns1_example1_test-TEST + + + 192.0.2.2 + 192.0.2.29 + 1080:0:0:0:8:800:200C:417A + + RegistrarX + RegistrarX + 1999-05-08T12:10:00.0Z + RegistrarX + 2009-10-03T09:34:00.0Z + + + + + sh8013 + Csh8013-TEST + + + + John Doe + Example Inc. + + 123 Example Dr. + Suite 100 + Dulles + VA + 20166-6503 + US + + + +1.7035555555 + + +1.7035555556 + + jdoe@example.test + + RegistrarX + RegistrarX + + 2009-09-13T08:01:00.0Z + RegistrarX + + 2009-11-26T09:10:00.0Z + 2009-12-03T09:05:00.0Z + + + + + + + + + RegistrarX + Registrar X + 123 + ok + + + 123 Example Dr. + + Suite 100 + + Dulles + VA + 20166-6503 + US + + + +1.7035555555 + + +1.7035555556 + + jdoe@example.test + + http://www.example.test + + + whois.example.test + + http://whois.example.test + + + 2005-04-23T11:49:00.0Z + 2009-02-17T17:51:00.0Z + + + + + +http://www.iana.org/domains/idn-tables/tables/br_pt-br_1.0.html + + + http://registro.br/dominio/regras.html + + + + + + xn--exampl-gva.test + pt-BR + example1.test + withheld + 2005-04-23T11:49:00.0Z + + + + + 1.0 + en + + urn:ietf:params:xml:ns:domain-1.0 + + + urn:ietf:params:xml:ns:contact-1.0 + + + urn:ietf:params:xml:ns:host-1.0 + + + urn:ietf:params:xml:ns:rgp-1.0 + + urn:ietf:params:xml:ns:secDNS-1.1 + + + + + + + + + + + + + + + + + + + + + + diff --git a/javatests/google/registry/rde/imports/testdata/domain_fragment_pending_transfer.xml b/javatests/google/registry/rde/imports/testdata/domain_fragment_pending_transfer.xml new file mode 100644 index 000000000..c19c3ac94 --- /dev/null +++ b/javatests/google/registry/rde/imports/testdata/domain_fragment_pending_transfer.xml @@ -0,0 +1,23 @@ + + example1.example + Dexample1-TEST + + jd1234 + sh8013 + sh8013 + RegistrarX + RegistrarX + 1999-04-03T22:00:00.0Z + 2015-04-03T22:00:00.0Z + RegistrarY + 2015-01-03T22:00:00.0Z + + pending + RegistrarY + 2015-01-03T22:00:00.0Z + RegistrarX + 2015-01-08T22:00:00.0Z + + diff --git a/javatests/google/registry/rde/imports/testdata/domain_fragment_pending_transfer_1yr.xml b/javatests/google/registry/rde/imports/testdata/domain_fragment_pending_transfer_1yr.xml new file mode 100644 index 000000000..f2bce9a96 --- /dev/null +++ b/javatests/google/registry/rde/imports/testdata/domain_fragment_pending_transfer_1yr.xml @@ -0,0 +1,24 @@ + + example1.example + Dexample1-TEST + + jd1234 + sh8013 + sh8013 + RegistrarX + RegistrarX + 1999-04-03T22:00:00.0Z + 2015-04-03T22:00:00.0Z + RegistrarY + 2015-01-03T22:00:00.0Z + + pending + RegistrarY + 2015-01-03T22:00:00.0Z + RegistrarX + 2015-01-08T22:00:00.0Z + 2016-04-03T22:00:00.0Z + + diff --git a/javatests/google/registry/rde/imports/testdata/domain_fragment_pending_transfer_2yr.xml b/javatests/google/registry/rde/imports/testdata/domain_fragment_pending_transfer_2yr.xml new file mode 100644 index 000000000..722329284 --- /dev/null +++ b/javatests/google/registry/rde/imports/testdata/domain_fragment_pending_transfer_2yr.xml @@ -0,0 +1,24 @@ + + example1.example + Dexample1-TEST + + jd1234 + sh8013 + sh8013 + RegistrarX + RegistrarX + 1999-04-03T22:00:00.0Z + 2015-04-03T22:00:00.0Z + RegistrarY + 2015-01-03T22:00:00.0Z + + pending + RegistrarY + 2015-01-03T22:00:00.0Z + RegistrarX + 2015-01-08T22:00:00.0Z + 2017-04-03T22:00:00.0Z + +