diff --git a/java/google/registry/billing/BUILD b/java/google/registry/billing/BUILD new file mode 100644 index 000000000..0eaab8be3 --- /dev/null +++ b/java/google/registry/billing/BUILD @@ -0,0 +1,31 @@ +package( + default_visibility = ["//java/google/registry:registry_project"], +) + +licenses(["notice"]) # Apache 2.0 + + +java_library( + name = "billing", + srcs = glob(["*.java"]), + deps = [ + "//java/com/google/common/base", + "//java/com/google/common/collect", + "//java/com/google/common/net", + "//third_party/java/appengine:appengine-api", + "//third_party/java/appengine_mapreduce2:appengine_mapreduce", + "//third_party/java/dagger", + "//third_party/java/joda_money", + "//third_party/java/joda_time", + "//third_party/java/jsr305_annotations", + "//third_party/java/jsr330_inject", + "//third_party/java/objectify:objectify-v4_1", + "//third_party/java/servlet/servlet_api", + "//java/google/registry/mapreduce", + "//java/google/registry/mapreduce/inputs", + "//java/google/registry/model", + "//java/google/registry/pricing", + "//java/google/registry/request", + "//java/google/registry/util", + ], +) diff --git a/java/google/registry/billing/ExpandRecurringBillingEventsAction.java b/java/google/registry/billing/ExpandRecurringBillingEventsAction.java new file mode 100644 index 000000000..5b565d417 --- /dev/null +++ b/java/google/registry/billing/ExpandRecurringBillingEventsAction.java @@ -0,0 +1,302 @@ +// 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.billing; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.collect.Sets.difference; +import static google.registry.mapreduce.MapreduceRunner.PARAM_DRY_RUN; +import static google.registry.mapreduce.inputs.EppResourceInputs.createChildEntityInput; +import static google.registry.model.EppResourceUtils.loadByUniqueId; +import static google.registry.model.common.Cursor.CursorType.RECURRING_BILLING; +import static google.registry.model.ofy.ObjectifyService.ofy; +import static google.registry.pricing.PricingEngineProxy.getDomainRenewCost; +import static google.registry.util.CollectionUtils.union; +import static google.registry.util.DateTimeUtils.START_OF_TIME; +import static google.registry.util.DateTimeUtils.earliestOf; +import static google.registry.util.DomainNameUtils.getTldFromDomainName; +import static google.registry.util.PipelineUtils.createJobPath; + +import com.google.appengine.tools.mapreduce.Mapper; +import com.google.appengine.tools.mapreduce.Reducer; +import com.google.appengine.tools.mapreduce.ReducerInput; +import com.google.common.base.Function; +import com.google.common.base.Optional; +import com.google.common.base.Predicate; +import com.google.common.collect.FluentIterable; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSet; +import com.google.common.collect.Range; + +import com.googlecode.objectify.Key; +import com.googlecode.objectify.VoidWork; +import com.googlecode.objectify.Work; + +import google.registry.mapreduce.MapreduceRunner; +import google.registry.mapreduce.inputs.NullInput; +import google.registry.model.billing.BillingEvent; +import google.registry.model.billing.BillingEvent.Flag; +import google.registry.model.billing.BillingEvent.OneTime; +import google.registry.model.billing.BillingEvent.Recurring; +import google.registry.model.common.Cursor; +import google.registry.model.domain.DomainResource; +import google.registry.model.registry.Registry; +import google.registry.request.Action; +import google.registry.request.Parameter; +import google.registry.request.Response; +import google.registry.util.Clock; +import google.registry.util.FormattingLogger; + +import org.joda.money.Money; +import org.joda.time.DateTime; + +import java.util.Set; + +import javax.inject.Inject; + +/** + * A mapreduce that expands {@link Recurring} billing events into synthetic {@link OneTime} events. + * + *

The cursor used throughout this mapreduce (overridden if necessary using the parameter + * {@code cursorTime}) represents the inclusive lower bound on the range of billing times that will + * be expanded as a result of the job (the exclusive upper bound being the execution time of the + * job). + */ +@Action(path = "/_dr/task/expandRecurringBillingEvents") +public class ExpandRecurringBillingEventsAction implements Runnable { + + public static final String PARAM_CURSOR_TIME = "cursorTime"; + private static final String ERROR_COUNTER = "errors"; + private static final FormattingLogger logger = FormattingLogger.getLoggerForCallerClass(); + + @Inject Clock clock; + @Inject MapreduceRunner mrRunner; + @Inject @Parameter(PARAM_DRY_RUN) boolean isDryRun; + @Inject @Parameter(PARAM_CURSOR_TIME) Optional cursorTimeParam; + @Inject Response response; + @Inject ExpandRecurringBillingEventsAction() {} + + @Override + public void run() { + Cursor cursor = ofy().load().key(Cursor.createGlobalKey(RECURRING_BILLING)).now(); + DateTime executeTime = clock.nowUtc(); + DateTime persistedCursorTime = (cursor == null ? START_OF_TIME : cursor.getCursorTime()); + DateTime cursorTime = cursorTimeParam.or(persistedCursorTime); + checkArgument( + cursorTime.isBefore(executeTime), + "Cursor time must be earlier than execution time."); + logger.infofmt( + "Running Recurring billing event expansion for billing time range [%s, %s).", + cursorTime, + executeTime); + response.sendJavaScriptRedirect(createJobPath(mrRunner + .setJobName("Expand Recurring billing events into synthetic OneTime events.") + .setModuleName("backend") + .runMapreduce( + new ExpandRecurringBillingEventsMapper(isDryRun, cursorTime, clock.nowUtc()), + new ExpandRecurringBillingEventsReducer(isDryRun, persistedCursorTime), + // Add an extra shard that maps over a null recurring event (see the mapper for why). + ImmutableList.of( + new NullInput(), + createChildEntityInput( + ImmutableSet.>of(DomainResource.class), + ImmutableSet.>of(Recurring.class)))))); + } + + /** Mapper to expand {@link Recurring} billing events into synthetic {@link OneTime} events. */ + public static class ExpandRecurringBillingEventsMapper + extends Mapper { + + private static final long serialVersionUID = 8376442755556228455L; + + private final boolean isDryRun; + private final DateTime cursorTime; + private final DateTime executeTime; + + public ExpandRecurringBillingEventsMapper( + boolean isDryRun, DateTime cursorTime, DateTime executeTime) { + this.isDryRun = isDryRun; + this.cursorTime = cursorTime; + this.executeTime = executeTime; + } + + @Override + public final void map(final Recurring recurring) { + // This single emit forces the reducer to run at the end of the map job, so that a mapper + // that runs without error will advance the cursor at the end of processing (unless this was + // a dry run, in which case the cursor should not be advanced). + if (recurring == null) { + emit(cursorTime, executeTime); + return; + } + getContext().incrementCounter("Recurring billing events encountered"); + int billingEventsSaved = 0; + try { + billingEventsSaved = ofy().transactNew(new Work() { + @Override + public Integer run() { + ImmutableSet.Builder syntheticOneTimesBuilder = + ImmutableSet.builder(); + final Registry tld = Registry.get(getTldFromDomainName(recurring.getTargetId())); + + // Determine the complete set of times at which this recurring event should occur + // (up to and including the runtime of the mapreduce). + Iterable eventTimes = + recurring.getRecurrenceTimeOfYear().getInstancesInRange(Range.closed( + recurring.getEventTime(), + earliestOf(recurring.getRecurrenceEndTime(), executeTime))); + + // Convert these event times to billing times + final ImmutableSet billingTimes = + getBillingTimesInScope(eventTimes, cursorTime, executeTime, tld); + + Iterable oneTimesForDomain = ofy().load() + .type(OneTime.class) + .ancestor(loadByUniqueId( + DomainResource.class, recurring.getTargetId(), executeTime)); + + // Determine the billing times that already have OneTime events persisted. + ImmutableSet existingBillingTimes = + getExistingBillingTimes(oneTimesForDomain, recurring); + + // Create synthetic OneTime events for all billing times that do not yet have an event + // persisted. + for (DateTime billingTime : difference(billingTimes, existingBillingTimes)) { + DateTime eventTime = billingTime.minus(tld.getAutoRenewGracePeriodLength()); + // Determine the cost for a one-year renewal. + Money renewCost = getDomainRenewCost(recurring.getTargetId(), eventTime, 1); + syntheticOneTimesBuilder.add(new BillingEvent.OneTime.Builder() + .setBillingTime(billingTime) + .setClientId(recurring.getClientId()) + .setCost(renewCost) + .setEventTime(eventTime) + .setFlags(union(recurring.getFlags(), Flag.SYNTHETIC)) + .setParent(recurring.getParentKey()) + .setPeriodYears(1) + .setReason(recurring.getReason()) + .setSyntheticCreationTime(executeTime) + .setCancellationMatchingBillingEvent(Key.create(recurring)) + .setTargetId(recurring.getTargetId()) + .build()); + } + Set syntheticOneTimes = syntheticOneTimesBuilder.build(); + if (!isDryRun) { + ofy().save().entities(syntheticOneTimes).now(); + } + return syntheticOneTimes.size(); + } + }); + } catch (Throwable t) { + logger.severefmt( + t, "Error while expanding Recurring billing events for %s", recurring.getId()); + getContext().incrementCounter("error: " + t.getClass().getSimpleName()); + getContext().incrementCounter(ERROR_COUNTER); + } + getContext().incrementCounter("Saved OneTime billing events", billingEventsSaved); + } + + /** + * Filters a set of {@link DateTime}s down to event times that are in scope for a particular + * mapreduce run, given the cursor time and the mapreduce execution time. + */ + private ImmutableSet getBillingTimesInScope( + Iterable eventTimes, + DateTime cursorTime, + DateTime executeTime, + final Registry tld) { + return FluentIterable.from(eventTimes) + .transform(new Function() { + @Override + public DateTime apply(DateTime eventTime) { + return eventTime.plus(tld.getAutoRenewGracePeriodLength()); + }}) + .filter(Range.closedOpen(cursorTime, executeTime)) + .toSet(); + } + + /** + * Determines an {@link ImmutableSet} of {@link DateTime}s that have already been persisted + * for a given recurring billing event. + */ + private ImmutableSet getExistingBillingTimes( + Iterable oneTimesForDomain, + final BillingEvent.Recurring recurringEvent) { + return FluentIterable.from(oneTimesForDomain) + .filter(new Predicate() { + @Override + public boolean apply(OneTime billingEvent) { + return billingEvent.getCancellationMatchingBillingEvent().equals( + Key.create(recurringEvent)); + }}) + .transform(new Function() { + @Override + public DateTime apply(OneTime billingEvent) { + return billingEvent.getBillingTime(); + }}) + .toSet(); + } + } + + + /** + * "Reducer" to advance the cursor after all map jobs have been completed. The NullInput into the + * mapper will cause the mapper to emit one timestamp pair (current cursor and execution time), + * and the cursor will be advanced (and the timestamps logged) at the end of a successful + * mapreduce. + */ + public static class ExpandRecurringBillingEventsReducer + extends Reducer { + + private final boolean isDryRun; + private final DateTime expectedPersistedCursorTime; + + public ExpandRecurringBillingEventsReducer( + boolean isDryRun, DateTime expectedPersistedCursorTime) { + this.isDryRun = isDryRun; + this.expectedPersistedCursorTime = expectedPersistedCursorTime; + } + + @Override + public void reduce(final DateTime cursorTime, final ReducerInput executionTimeInput) { + if (getContext().getCounter(ERROR_COUNTER).getValue() > 0) { + logger.severefmt("One or more errors logged during recurring event expansion. Cursor will" + + " not be advanced."); + return; + } + final DateTime executionTime = executionTimeInput.next(); + logger.infofmt( + "Recurring event expansion %s complete for billing event range [%s, %s).", + isDryRun ? "(dry run) " : "", + cursorTime, + executionTime); + ofy().transact(new VoidWork() { + @Override + public void vrun() { + Cursor cursor = ofy().load().key(Cursor.createGlobalKey(RECURRING_BILLING)).now(); + DateTime currentCursorTime = (cursor == null ? START_OF_TIME : cursor.getCursorTime()); + if (!currentCursorTime.equals(expectedPersistedCursorTime)) { + logger.severefmt( + "Current cursor position %s does not match expected cursor position %s.", + currentCursorTime, + expectedPersistedCursorTime); + return; + } + if (!isDryRun) { + ofy().save().entity(Cursor.createGlobal(RECURRING_BILLING, executionTime)); + } + } + }); + } + } +} diff --git a/java/google/registry/loadtest/LoadTestAction.java b/java/google/registry/loadtest/LoadTestAction.java index 8ad58678e..5fcb835cf 100644 --- a/java/google/registry/loadtest/LoadTestAction.java +++ b/java/google/registry/loadtest/LoadTestAction.java @@ -286,7 +286,7 @@ public class LoadTestAction implements Runnable { tasks.add(TaskOptions.Builder.withUrl("/_dr/epptool") .etaMillis(start.getMillis() + offsetMillis) .payload( - Joiner.on('&').withKeyValueSeparator('=').join( + Joiner.on('&').withKeyValueSeparator("=").join( ImmutableMap.of( "clientIdentifier", clientId, "superuser", false, diff --git a/java/google/registry/tools/EppToolCommand.java b/java/google/registry/tools/EppToolCommand.java index 981f2c604..156d09247 100644 --- a/java/google/registry/tools/EppToolCommand.java +++ b/java/google/registry/tools/EppToolCommand.java @@ -147,7 +147,7 @@ abstract class EppToolCommand extends ConfirmingCommand implements ServerSideCom params.put("clientIdentifier", command.clientId); params.put("superuser", superuser); params.put("xml", URLEncoder.encode(command.xml, UTF_8.toString())); - String requestBody = Joiner.on('&').withKeyValueSeparator('=') + String requestBody = Joiner.on('&').withKeyValueSeparator("=") .join(filterValues(params, notNull())); responses.add(nullToEmpty(connection.send( "/_dr/epptool", diff --git a/javatests/google/registry/billing/BUILD b/javatests/google/registry/billing/BUILD new file mode 100644 index 000000000..6f337924b --- /dev/null +++ b/javatests/google/registry/billing/BUILD @@ -0,0 +1,40 @@ +package( + default_testonly = 1, + default_visibility = ["//java/google/registry:registry_project"], +) + +licenses(["notice"]) # Apache 2.0 + +load("//java/com/google/testing/builddefs:GenTestRules.bzl", "GenTestRules") + + +java_library( + name = "billing", + srcs = glob(["*.java"]), + deps = [ + "//java/com/google/common/base", + "//java/com/google/common/collect", + "//third_party/java/appengine:appengine-api-testonly", + "//third_party/java/appengine_mapreduce2:appengine_mapreduce", + "//third_party/java/joda_money", + "//third_party/java/joda_time", + "//third_party/java/junit", + "//third_party/java/objectify:objectify-v4_1", + "//third_party/java/truth", + "//java/google/registry/billing", + "//java/google/registry/mapreduce", + "//java/google/registry/model", + "//java/google/registry/util", + "//javatests/google/registry/model", + "//javatests/google/registry/testing", + "//javatests/google/registry/testing/mapreduce", + ], +) + +GenTestRules( + name = "GeneratedTestRules", + default_test_size = "medium", + shard_count = 3, + test_files = glob(["*Test.java"]), + deps = [":billing"], +) diff --git a/javatests/google/registry/billing/ExpandRecurringBillingEventsActionTest.java b/javatests/google/registry/billing/ExpandRecurringBillingEventsActionTest.java new file mode 100644 index 000000000..539780d92 --- /dev/null +++ b/javatests/google/registry/billing/ExpandRecurringBillingEventsActionTest.java @@ -0,0 +1,672 @@ +// 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.billing; + +import static com.google.common.truth.Truth.assertThat; +import static google.registry.model.common.Cursor.CursorType.RECURRING_BILLING; +import static google.registry.model.ofy.ObjectifyService.ofy; +import static google.registry.testing.DatastoreHelper.assertBillingEventsForResource; +import static google.registry.testing.DatastoreHelper.createTld; +import static google.registry.testing.DatastoreHelper.persistActiveDomain; +import static google.registry.testing.DatastoreHelper.persistPremiumList; +import static google.registry.testing.DatastoreHelper.persistResource; +import static google.registry.util.DateTimeUtils.END_OF_TIME; +import static google.registry.util.DateTimeUtils.START_OF_TIME; +import static org.joda.money.CurrencyUnit.USD; + +import com.google.common.base.Optional; +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.VoidWork; + +import google.registry.mapreduce.MapreduceRunner; +import google.registry.model.billing.BillingEvent; +import google.registry.model.billing.BillingEvent.Flag; +import google.registry.model.billing.BillingEvent.Reason; +import google.registry.model.common.Cursor; +import google.registry.model.domain.DomainResource; +import google.registry.model.registry.Registry; +import google.registry.model.reporting.HistoryEntry; +import google.registry.testing.ExceptionRule; +import google.registry.testing.FakeClock; +import google.registry.testing.FakeResponse; +import google.registry.testing.mapreduce.MapreduceTestCase; + +import org.joda.money.Money; +import org.joda.time.DateTime; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +import java.util.ArrayList; +import java.util.List; + +/** Unit tests for {@link ExpandRecurringBillingEventsAction}. */ +@RunWith(JUnit4.class) +public class ExpandRecurringBillingEventsActionTest + extends MapreduceTestCase { + + @Rule + public final ExceptionRule thrown = new ExceptionRule(); + + final FakeClock clock = new FakeClock(DateTime.parse("2000-10-02T00:00:00Z")); + + DomainResource domain; + HistoryEntry historyEntry; + BillingEvent.Recurring recurring; + + @Before + public void init() { + action = new ExpandRecurringBillingEventsAction(); + action.mrRunner = new MapreduceRunner(Optional.absent(), Optional.absent()); + action.clock = clock; + action.cursorTimeParam = Optional.absent(); + createTld("tld"); + domain = persistActiveDomain("example.tld"); + historyEntry = persistResource(new HistoryEntry.Builder().setParent(domain).build()); + recurring = new BillingEvent.Recurring.Builder() + .setParent(historyEntry) + .setClientId(domain.getCreationClientId()) + .setEventTime(DateTime.parse("2000-01-05T00:00:00Z")) + .setFlags(ImmutableSet.of(Flag.AUTO_RENEW)) + .setId(2L) + .setReason(Reason.RENEW) + .setRecurrenceEndTime(END_OF_TIME) + .setTargetId(domain.getFullyQualifiedDomainName()) + .build(); + } + + void saveCursor(final DateTime cursorTime) throws Exception { + ofy().transact(new VoidWork() { + @Override + public void vrun() { + ofy().save().entity(Cursor.createGlobal(RECURRING_BILLING, cursorTime)); + }}); + } + + void runMapreduce() throws Exception { + action.response = new FakeResponse(); + action.run(); + executeTasksUntilEmpty("mapreduce"); + ofy().clearSessionCache(); + } + + void assertCursorAt(DateTime expectedCursorTime) throws Exception { + Cursor cursor = ofy().load().key(Cursor.createGlobalKey(RECURRING_BILLING)).now(); + assertThat(cursor).isNotNull(); + assertThat(cursor.getCursorTime()).isEqualTo(expectedCursorTime); + } + + @Test + public void testSuccess_expandSingleEvent() throws Exception { + persistResource(recurring); + action.cursorTimeParam = Optional.of(START_OF_TIME); + runMapreduce(); + BillingEvent.OneTime expected = new BillingEvent.OneTime.Builder() + // Default renew grace period of 45 days. + .setBillingTime(DateTime.parse("2000-02-19T00:00:00Z")) + .setClientId("TheRegistrar") + .setCost(Money.of(USD, 11)) + .setEventTime(DateTime.parse("2000-01-05T00:00:00Z")) + .setFlags(ImmutableSet.of(Flag.AUTO_RENEW, Flag.SYNTHETIC)) + .setParent(historyEntry) + .setPeriodYears(1) + .setReason(Reason.RENEW) + .setSyntheticCreationTime(clock.nowUtc()) + .setCancellationMatchingBillingEvent(Key.create(recurring)) + .setTargetId(domain.getFullyQualifiedDomainName()) + .build(); + assertBillingEventsForResource(domain, expected, recurring); + assertCursorAt(clock.nowUtc()); + } + + @Test + public void testSuccess_expandSingleEvent_idempotentForDuplicateRuns() throws Exception { + persistResource(recurring); + action.cursorTimeParam = Optional.of(START_OF_TIME); + runMapreduce(); + BillingEvent.OneTime expected = new BillingEvent.OneTime.Builder() + // Default renew grace period of 45 days. + .setBillingTime(DateTime.parse("2000-02-19T00:00:00Z")) + .setClientId("TheRegistrar") + .setCost(Money.of(USD, 11)) + .setEventTime(DateTime.parse("2000-01-05T00:00:00Z")) + .setFlags(ImmutableSet.of(Flag.AUTO_RENEW, Flag.SYNTHETIC)) + .setParent(historyEntry) + .setPeriodYears(1) + .setReason(Reason.RENEW) + .setSyntheticCreationTime(clock.nowUtc()) + .setCancellationMatchingBillingEvent(Key.create(recurring)) + .setTargetId(domain.getFullyQualifiedDomainName()) + .build(); + assertCursorAt(clock.nowUtc()); + action.response = new FakeResponse(); + runMapreduce(); + assertCursorAt(clock.nowUtc()); + assertBillingEventsForResource(domain, expected, recurring); + } + + @Test + public void testSuccess_expandSingleEvent_idempotentForExistingOneTime() throws Exception { + persistResource(recurring); + BillingEvent.OneTime persisted = persistResource(new BillingEvent.OneTime.Builder() + // Default renew grace period of 45 days. + .setBillingTime(DateTime.parse("2000-02-19T00:00:00Z")) + .setClientId("TheRegistrar") + .setCost(Money.of(USD, 11)) + .setEventTime(DateTime.parse("2000-01-05T00:00:00Z")) + .setFlags(ImmutableSet.of(Flag.AUTO_RENEW, Flag.SYNTHETIC)) + .setParent(historyEntry) + .setPeriodYears(1) + .setReason(Reason.RENEW) + .setSyntheticCreationTime(clock.nowUtc()) + .setCancellationMatchingBillingEvent(Key.create(recurring)) + .setTargetId(domain.getFullyQualifiedDomainName()) + .build()); + action.cursorTimeParam = Optional.of(START_OF_TIME); + runMapreduce(); + assertCursorAt(clock.nowUtc()); + assertBillingEventsForResource(domain, persisted, recurring); // no additional billing events + } + + @Test + public void testSuccess_expandSingleEvent_notIdempotentForDifferentBillingTime() + throws Exception { + persistResource(recurring); + BillingEvent.OneTime expected = new BillingEvent.OneTime.Builder() + .setBillingTime(DateTime.parse("2000-02-19T00:00:00Z")) + .setClientId("TheRegistrar") + .setCost(Money.of(USD, 11)) + .setEventTime(DateTime.parse("2000-01-05T00:00:00Z")) + .setFlags(ImmutableSet.of(Flag.AUTO_RENEW, Flag.SYNTHETIC)) + .setParent(historyEntry) + .setPeriodYears(1) + .setReason(Reason.RENEW) + .setSyntheticCreationTime(clock.nowUtc()) + .setCancellationMatchingBillingEvent(Key.create(recurring)) + .setTargetId(domain.getFullyQualifiedDomainName()) + .build(); + // Persist an otherwise identical billing event that differs only in billing time. + BillingEvent.OneTime persisted = persistResource(expected.asBuilder() + .setBillingTime(DateTime.parse("1999-02-19T00:00:00Z")) + .setEventTime(DateTime.parse("1999-01-05T00:00:00Z")) + .build()); + action.cursorTimeParam = Optional.of(START_OF_TIME); + runMapreduce(); + assertCursorAt(clock.nowUtc()); + assertBillingEventsForResource(domain, persisted, expected, recurring); + } + + @Test + public void testSuccess_expandSingleEvent_notIdempotentForDifferentRecurring() + throws Exception { + persistResource(recurring); + BillingEvent.Recurring recurring2 = persistResource(recurring.asBuilder() + .setId(3L) + .build()); + BillingEvent.OneTime expected = new BillingEvent.OneTime.Builder() + .setBillingTime(DateTime.parse("2000-02-19T00:00:00Z")) + .setClientId("TheRegistrar") + .setCost(Money.of(USD, 11)) + .setEventTime(DateTime.parse("2000-01-05T00:00:00Z")) + .setFlags(ImmutableSet.of(Flag.AUTO_RENEW, Flag.SYNTHETIC)) + .setParent(historyEntry) + .setPeriodYears(1) + .setReason(Reason.RENEW) + .setSyntheticCreationTime(clock.nowUtc()) + .setCancellationMatchingBillingEvent(Key.create(recurring)) + .setTargetId(domain.getFullyQualifiedDomainName()) + .build(); + // Persist an otherwise identical billing event that differs only in recurring event key. + BillingEvent.OneTime persisted = persistResource(expected.asBuilder() + .setCancellationMatchingBillingEvent(Key.create(recurring2)) + .build()); + action.cursorTimeParam = Optional.of(START_OF_TIME); + runMapreduce(); + assertCursorAt(clock.nowUtc()); + assertBillingEventsForResource(domain, persisted, expected, recurring, recurring2); + } + + @Test + public void testSuccess_ignoreRecurringBeforeWindow() throws Exception { + recurring = persistResource(recurring.asBuilder() + .setEventTime(DateTime.parse("1997-01-05T00:00:00Z")) + .setRecurrenceEndTime(DateTime.parse("1999-10-05T00:00:00Z")) + .build()); + action.cursorTimeParam = Optional.of(DateTime.parse("2000-01-01T00:00:00Z")); + runMapreduce(); + assertBillingEventsForResource(domain, recurring); + assertCursorAt(clock.nowUtc()); + } + + @Test + public void testSuccess_ignoreRecurringAfterWindow() throws Exception { + recurring = persistResource(recurring.asBuilder() + .setEventTime(clock.nowUtc().plusYears(2)) + .build()); + action.cursorTimeParam = Optional.of(START_OF_TIME); + runMapreduce(); + assertBillingEventsForResource(domain, recurring); + assertCursorAt(clock.nowUtc()); + } + + @Test + public void testSuccess_expandSingleEvent_billingTimeAtCursorTime() throws Exception { + persistResource(recurring); + action.cursorTimeParam = Optional.of(DateTime.parse("2000-02-19T00:00:00Z")); + runMapreduce(); + BillingEvent.OneTime expected = new BillingEvent.OneTime.Builder() + // Default renew grace period of 45 days. + .setBillingTime(DateTime.parse("2000-02-19T00:00:00Z")) + .setClientId("TheRegistrar") + .setCost(Money.of(USD, 11)) + .setEventTime(DateTime.parse("2000-01-05T00:00:00Z")) + .setFlags(ImmutableSet.of(Flag.AUTO_RENEW, Flag.SYNTHETIC)) + .setParent(historyEntry) + .setPeriodYears(1) + .setReason(Reason.RENEW) + .setSyntheticCreationTime(clock.nowUtc()) + .setCancellationMatchingBillingEvent(Key.create(recurring)) + .setTargetId(domain.getFullyQualifiedDomainName()) + .build(); + assertBillingEventsForResource(domain, expected, recurring); + assertCursorAt(clock.nowUtc()); + } + + @Test + public void testSuccess_expandSingleEvent_cursorTimeBetweenEventAndBillingTime() + throws Exception { + persistResource(recurring); + action.cursorTimeParam = Optional.of(DateTime.parse("2000-01-12T00:00:00Z")); + runMapreduce(); + BillingEvent.OneTime expected = new BillingEvent.OneTime.Builder() + // Default renew grace period of 45 days. + .setBillingTime(DateTime.parse("2000-02-19T00:00:00Z")) + .setClientId("TheRegistrar") + .setCost(Money.of(USD, 11)) + .setEventTime(DateTime.parse("2000-01-05T00:00:00Z")) + .setFlags(ImmutableSet.of(Flag.AUTO_RENEW, Flag.SYNTHETIC)) + .setParent(historyEntry) + .setPeriodYears(1) + .setReason(Reason.RENEW) + .setSyntheticCreationTime(clock.nowUtc()) + .setCancellationMatchingBillingEvent(Key.create(recurring)) + .setTargetId(domain.getFullyQualifiedDomainName()) + .build(); + assertBillingEventsForResource(domain, expected, recurring); + assertCursorAt(clock.nowUtc()); + } + + @Test + public void testSuccess_expandSingleEvent_billingTimeAtExecutionTime() throws Exception { + persistResource(recurring); + action.cursorTimeParam = Optional.of(START_OF_TIME); + // Clock is advanced one milli in runMapreduce() + clock.setTo(DateTime.parse("2000-02-19T00:00:00Z").minusMillis(1)); + runMapreduce(); + // A candidate billing event is set to be billed exactly on 2/19/00 @ 00:00, + // but these should not be generated as the interval is closed on cursorTime, open on + // executeTime. + assertBillingEventsForResource(domain, recurring); + assertCursorAt(clock.nowUtc()); + } + + @Test + public void testSuccess_expandSingleEvent_multipleYearCreate() throws Exception { + action.cursorTimeParam = Optional.of(recurring.getEventTime()); + recurring = persistResource(recurring.asBuilder() + .setEventTime(recurring.getEventTime().plusYears(2)) + .build()); + clock.setTo(clock.nowUtc().plusYears(2)); + runMapreduce(); + BillingEvent.OneTime expected = new BillingEvent.OneTime.Builder() + // Default renew grace period of 45 days. + .setBillingTime(DateTime.parse("2002-02-19T00:00:00Z")) + .setClientId("TheRegistrar") + .setCost(Money.of(USD, 11)) + .setEventTime(DateTime.parse("2002-01-05T00:00:00Z")) + .setFlags(ImmutableSet.of(Flag.AUTO_RENEW, Flag.SYNTHETIC)) + .setParent(historyEntry) + .setPeriodYears(1) + .setReason(Reason.RENEW) + .setSyntheticCreationTime(clock.nowUtc()) + .setCancellationMatchingBillingEvent(Key.create(recurring)) + .setTargetId(domain.getFullyQualifiedDomainName()) + .build(); + assertBillingEventsForResource(domain, expected, recurring); + assertCursorAt(clock.nowUtc()); + } + + @Test + public void testSuccess_expandSingleEvent_withCursor() throws Exception { + persistResource(recurring); + saveCursor(START_OF_TIME); + runMapreduce(); + BillingEvent.OneTime expected = new BillingEvent.OneTime.Builder() + // Default renew grace period of 45 days. + .setBillingTime(DateTime.parse("2000-02-19T00:00:00Z")) + .setClientId("TheRegistrar") + .setCost(Money.of(USD, 11)) + .setEventTime(DateTime.parse("2000-01-05T00:00:00Z")) + .setFlags(ImmutableSet.of(Flag.AUTO_RENEW, Flag.SYNTHETIC)) + .setParent(historyEntry) + .setPeriodYears(1) + .setReason(Reason.RENEW) + .setSyntheticCreationTime(clock.nowUtc()) + .setCancellationMatchingBillingEvent(Key.create(recurring)) + .setTargetId(domain.getFullyQualifiedDomainName()) + .build(); + assertBillingEventsForResource(domain, expected, recurring); + assertCursorAt(clock.nowUtc()); + } + + @Test + public void testSuccess_expandSingleEvent_withCursorPastExpected() throws Exception { + persistResource(recurring); + // Simulate a quick second run of the mapreduce (this should be a no-op). + saveCursor(clock.nowUtc().minusSeconds(1)); + runMapreduce(); + assertBillingEventsForResource(domain, recurring); + assertCursorAt(clock.nowUtc()); + } + + @Test + public void testSuccess_expandSingleEvent_dryRun() throws Exception { + persistResource(recurring); + action.isDryRun = true; + saveCursor(START_OF_TIME); // Need a saved cursor to verify that it didn't move. + runMapreduce(); + assertBillingEventsForResource(domain, recurring); + assertCursorAt(START_OF_TIME); // Cursor doesn't move on a dry run. + } + + @Test + public void testSuccess_expandSingleEvent_multipleYears() throws Exception { + clock.setTo(clock.nowUtc().plusYears(5)); + List expectedEvents = new ArrayList<>(); + expectedEvents.add(persistResource(recurring)); + action.cursorTimeParam = Optional.of(START_OF_TIME); + runMapreduce(); + DateTime eventDate = DateTime.parse("2000-01-05T00:00:00Z"); + // Default renew grace period of 45 days. + DateTime billingDate = DateTime.parse("2000-02-19T00:00:00Z"); + // Expecting events for '00, '01, '02, '03, '04, '05. + for (int year = 0; year < 6; year++) { + expectedEvents.add(new BillingEvent.OneTime.Builder() + .setBillingTime(billingDate.plusYears(year)) + .setClientId("TheRegistrar") + .setCost(Money.of(USD, 11)) + .setEventTime(eventDate.plusYears(year)) + .setFlags(ImmutableSet.of(Flag.AUTO_RENEW, Flag.SYNTHETIC)) + .setParent(historyEntry) + .setPeriodYears(1) + .setReason(Reason.RENEW) + .setSyntheticCreationTime(clock.nowUtc()) + .setCancellationMatchingBillingEvent(Key.create(recurring)) + .setTargetId(domain.getFullyQualifiedDomainName()) + .build()); + } + assertBillingEventsForResource(domain, Iterables.toArray(expectedEvents, BillingEvent.class)); + assertCursorAt(clock.nowUtc()); + } + + @Test + public void testSuccess_expandSingleEvent_multipleYears_cursorInBetweenYears() throws Exception { + clock.setTo(clock.nowUtc().plusYears(5)); + List expectedEvents = new ArrayList<>(); + expectedEvents.add(persistResource(recurring)); + saveCursor(DateTime.parse("2003-10-02T00:00:00Z")); + runMapreduce(); + DateTime eventDate = DateTime.parse("2004-01-05T00:00:00Z"); + // Default renew grace period of 45 days. + DateTime billingDate = DateTime.parse("2004-02-19T00:00:00Z"); + // Only expect the last two years' worth of billing events. + for (int year = 0; year < 2; year++) { + expectedEvents.add(new BillingEvent.OneTime.Builder() + .setBillingTime(billingDate.plusYears(year)) + .setClientId("TheRegistrar") + .setCost(Money.of(USD, 11)) + .setEventTime(eventDate.plusYears(year)) + .setFlags(ImmutableSet.of(Flag.AUTO_RENEW, Flag.SYNTHETIC)) + .setParent(historyEntry) + .setPeriodYears(1) + .setReason(Reason.RENEW) + .setSyntheticCreationTime(clock.nowUtc()) + .setCancellationMatchingBillingEvent(Key.create(recurring)) + .setTargetId(domain.getFullyQualifiedDomainName()) + .build()); + } + assertBillingEventsForResource(domain, Iterables.toArray(expectedEvents, BillingEvent.class)); + assertCursorAt(clock.nowUtc()); + } + + @Test + public void testSuccess_singleEvent_beforeRenewal() throws Exception { + clock.setTo(DateTime.parse("2000-01-04T00:00:00Z")); + persistResource(recurring); + action.cursorTimeParam = Optional.of(START_OF_TIME); + runMapreduce(); + assertBillingEventsForResource(domain, recurring); + assertCursorAt(clock.nowUtc()); + } + + @Test + public void testSuccess_singleEvent_afterRecurrenceEnd() throws Exception { + clock.setTo(clock.nowUtc().plusYears(2)); + recurring = persistResource(recurring.asBuilder() + // Set between event time and billing time (i.e. before the grace period expires) for 2000. + // We should still expect a billing event. + .setRecurrenceEndTime(DateTime.parse("2000-01-29T00:00:00Z")) + .build()); + action.cursorTimeParam = Optional.of(START_OF_TIME); + runMapreduce(); + BillingEvent.OneTime expected = new BillingEvent.OneTime.Builder() + .setBillingTime(DateTime.parse("2000-02-19T00:00:00Z")) + .setClientId("TheRegistrar") + .setCost(Money.of(USD, 11)) + .setEventTime(DateTime.parse("2000-01-05T00:00:00Z")) + .setFlags(ImmutableSet.of(Flag.AUTO_RENEW, Flag.SYNTHETIC)) + .setParent(historyEntry) + .setPeriodYears(1) + .setReason(Reason.RENEW) + .setSyntheticCreationTime(clock.nowUtc()) + .setCancellationMatchingBillingEvent(Key.create(recurring)) + .setTargetId(domain.getFullyQualifiedDomainName()) + .build(); + assertBillingEventsForResource(domain, recurring, expected); + assertCursorAt(clock.nowUtc()); + } + + @Test + public void testSuccess_expandSingleEvent_billingTimeOnLeapYear() throws Exception { + recurring = persistResource(recurring.asBuilder() + .setEventTime(DateTime.parse("2000-01-15T00:00:00Z")) + .build()); + action.cursorTimeParam = Optional.of(START_OF_TIME); + runMapreduce(); + BillingEvent.OneTime expected = new BillingEvent.OneTime.Builder() + // Default renew grace period of 45 days. + .setBillingTime(DateTime.parse("2000-02-29T00:00:00Z")) + .setClientId("TheRegistrar") + .setCost(Money.of(USD, 11)) + .setEventTime(DateTime.parse("2000-01-15T00:00:00Z")) + .setFlags(ImmutableSet.of(Flag.AUTO_RENEW, Flag.SYNTHETIC)) + .setParent(historyEntry) + .setPeriodYears(1) + .setReason(Reason.RENEW) + .setSyntheticCreationTime(clock.nowUtc()) + .setCancellationMatchingBillingEvent(Key.create(recurring)) + .setTargetId(domain.getFullyQualifiedDomainName()) + .build(); + assertBillingEventsForResource(domain, expected, recurring); + assertCursorAt(clock.nowUtc()); + } + + @Test + public void testSuccess_expandSingleEvent_billingTimeNotOnLeapYear() throws Exception { + recurring = persistResource(recurring.asBuilder() + .setEventTime(DateTime.parse("1999-01-15T00:00:00Z")) + .build()); + action.cursorTimeParam = Optional.of(START_OF_TIME); + clock.setTo(DateTime.parse("1999-12-01T00:00:00Z")); + runMapreduce(); + BillingEvent.OneTime expected = new BillingEvent.OneTime.Builder() + // Default renew grace period of 45 days. + .setBillingTime(DateTime.parse("1999-03-01T00:00:00Z")) + .setClientId("TheRegistrar") + .setCost(Money.of(USD, 11)) + .setEventTime(DateTime.parse("1999-01-15T00:00:00Z")) + .setFlags(ImmutableSet.of(Flag.AUTO_RENEW, Flag.SYNTHETIC)) + .setParent(historyEntry) + .setPeriodYears(1) + .setReason(Reason.RENEW) + .setSyntheticCreationTime(clock.nowUtc()) + .setCancellationMatchingBillingEvent(Key.create(recurring)) + .setTargetId(domain.getFullyQualifiedDomainName()) + .build(); + assertBillingEventsForResource(domain, expected, recurring); + assertCursorAt(clock.nowUtc()); + } + + @Test + public void testSuccess_expandMultipleEvents() throws Exception { + persistResource(recurring); + BillingEvent.Recurring recurring2 = persistResource(recurring.asBuilder() + .setEventTime(recurring.getEventTime().plusMonths(3)) + .setId(3L) + .build()); + action.cursorTimeParam = Optional.of(START_OF_TIME); + runMapreduce(); + BillingEvent.OneTime expected = new BillingEvent.OneTime.Builder() + // Default renew grace period of 45 days. + .setBillingTime(DateTime.parse("2000-02-19T00:00:00Z")) + .setClientId("TheRegistrar") + .setCost(Money.of(USD, 11)) + .setEventTime(DateTime.parse("2000-01-05T00:00:00Z")) + .setFlags(ImmutableSet.of(Flag.AUTO_RENEW, Flag.SYNTHETIC)) + .setParent(historyEntry) + .setPeriodYears(1) + .setReason(Reason.RENEW) + .setSyntheticCreationTime(clock.nowUtc()) + .setCancellationMatchingBillingEvent(Key.create(recurring)) + .setTargetId(domain.getFullyQualifiedDomainName()) + .build(); + BillingEvent.OneTime expected2 = new BillingEvent.OneTime.Builder() + // Default renew grace period of 45 days. + .setBillingTime(DateTime.parse("2000-05-20T00:00:00Z")) + .setClientId("TheRegistrar") + .setCost(Money.of(USD, 11)) + .setEventTime(DateTime.parse("2000-04-05T00:00:00Z")) + .setFlags(ImmutableSet.of(Flag.AUTO_RENEW, Flag.SYNTHETIC)) + .setParent(historyEntry) + .setPeriodYears(1) + .setReason(Reason.RENEW) + .setSyntheticCreationTime(clock.nowUtc()) + .setCancellationMatchingBillingEvent(Key.create(recurring2)) + .setTargetId(domain.getFullyQualifiedDomainName()) + .build(); + assertBillingEventsForResource(domain, expected, expected2, recurring, recurring2); + assertCursorAt(clock.nowUtc()); + } + + @Test + public void testSuccess_premiumDomain() throws Exception { + persistResource( + Registry.get("tld") + .asBuilder() + .setPremiumList(persistPremiumList("tld2", "example,USD 100")) + .build()); + persistResource(recurring); + action.cursorTimeParam = Optional.of(START_OF_TIME); + runMapreduce(); + BillingEvent.OneTime expected = new BillingEvent.OneTime.Builder() + // Default renew grace period of 45 days. + .setBillingTime(DateTime.parse("2000-02-19T00:00:00Z")) + .setClientId("TheRegistrar") + .setCost(Money.of(USD, 100)) + .setEventTime(DateTime.parse("2000-01-05T00:00:00Z")) + .setFlags(ImmutableSet.of(Flag.AUTO_RENEW, Flag.SYNTHETIC)) + .setParent(historyEntry) + .setPeriodYears(1) + .setReason(Reason.RENEW) + .setSyntheticCreationTime(clock.nowUtc()) + .setCancellationMatchingBillingEvent(Key.create(recurring)) + .setTargetId(domain.getFullyQualifiedDomainName()) + .build(); + assertBillingEventsForResource(domain, expected, recurring); + assertCursorAt(clock.nowUtc()); + } + + @Test + public void testSuccess_varyingRenewPrices() throws Exception { + persistResource( + Registry.get("tld") + .asBuilder() + .setRenewBillingCostTransitions( + ImmutableSortedMap.of( + START_OF_TIME, Money.of(USD, 8), + DateTime.parse("2000-06-01T00:00:00Z"), Money.of(USD, 10))) + .build()); + clock.setTo(clock.nowUtc().plusYears(1)); + persistResource(recurring); + action.cursorTimeParam = Optional.of(START_OF_TIME); + runMapreduce(); + DateTime eventDate = DateTime.parse("2000-01-05T00:00:00Z"); + // Default renew grace period of 45 days. + DateTime billingDate = DateTime.parse("2000-02-19T00:00:00Z"); + BillingEvent.OneTime cheaper = new BillingEvent.OneTime.Builder() + .setBillingTime(billingDate) + .setClientId("TheRegistrar") + .setCost(Money.of(USD, 8)) + .setEventTime(eventDate) + .setFlags(ImmutableSet.of(Flag.AUTO_RENEW, Flag.SYNTHETIC)) + .setParent(historyEntry) + .setPeriodYears(1) + .setReason(Reason.RENEW) + .setSyntheticCreationTime(clock.nowUtc()) + .setCancellationMatchingBillingEvent(Key.create(recurring)) + .setTargetId(domain.getFullyQualifiedDomainName()) + .build(); + BillingEvent.OneTime expensive = cheaper.asBuilder() + .setCost(Money.of(USD, 10)) + .setBillingTime(billingDate.plusYears(1)) + .setEventTime(eventDate.plusYears(1)) + .build(); + assertBillingEventsForResource(domain, recurring, cheaper, expensive); + assertCursorAt(clock.nowUtc()); + } + + @Test + public void testFailure_cursorAfterExecutionTime() throws Exception { + action.cursorTimeParam = Optional.of(clock.nowUtc().plusYears(1)); + thrown.expect( + IllegalArgumentException.class, "Cursor time must be earlier than execution time."); + runMapreduce(); + } + + @Test + public void testFailure_cursorAtExecutionTime() throws Exception { + // The clock advances one milli on runMapreduce. + action.cursorTimeParam = Optional.of(clock.nowUtc().plusMillis(1)); + thrown.expect( + IllegalArgumentException.class, "Cursor time must be earlier than execution time."); + runMapreduce(); + } +} diff --git a/javatests/google/registry/flows/session/LoginFlowViaTlsTest.java b/javatests/google/registry/flows/session/LoginFlowViaTlsTest.java index 2ab04238b..ec421d906 100644 --- a/javatests/google/registry/flows/session/LoginFlowViaTlsTest.java +++ b/javatests/google/registry/flows/session/LoginFlowViaTlsTest.java @@ -121,7 +121,7 @@ public class LoginFlowViaTlsTest extends LoginFlowTestCase { CidrAddressBlock.create(InetAddresses.forString("2001:db8::1"), 128))) .build()); sessionMetadata.setTransportCredentials( - new TlsCredentials(GOOD_CERT, Optional.empty(), "goo.example")); + new TlsCredentials(GOOD_CERT, Optional.absent(), "goo.example")); doFailingTest("login_valid.xml", BadRegistrarIpAddressException.class); } diff --git a/javatests/google/registry/model/SchemaVersionTest.java b/javatests/google/registry/model/SchemaVersionTest.java index 8cfc073a6..051492dca 100644 --- a/javatests/google/registry/model/SchemaVersionTest.java +++ b/javatests/google/registry/model/SchemaVersionTest.java @@ -37,7 +37,7 @@ public class SchemaVersionTest { private static final String GOLDEN_SCHEMA_FILE = "schema.txt"; private static final String UPDATE_COMMAND = - "google.registry.tools.RegistryTool -e localhost " + "google.registry.tools.RegistryTool -e localhost get_schema " + ">javatests/google/registry/model/schema.txt"; private static final String UPDATE_INSTRUCTIONS = Joiner.on('\n').join( diff --git a/javatests/google/registry/model/schema.txt b/javatests/google/registry/model/schema.txt index dd1ffa074..a9f76df10 100644 --- a/javatests/google/registry/model/schema.txt +++ b/javatests/google/registry/model/schema.txt @@ -43,9 +43,9 @@ class google.registry.model.billing.BillingEvent$Modification { class google.registry.model.billing.BillingEvent$OneTime { @Id long id; @Parent com.googlecode.objectify.Key parent; + com.googlecode.objectify.Key cancellationMatchingBillingEvent; google.registry.model.billing.BillingEvent$Reason reason; java.lang.Integer periodYears; - java.lang.Long cancellationTargetId; java.lang.String clientId; java.lang.String targetId; java.util.Set flags; @@ -672,6 +672,7 @@ class google.registry.model.registry.Registry { com.googlecode.objectify.Key premiumList; google.registry.model.CreateAutoTimestamp creationTime; google.registry.model.common.TimedTransitionProperty tldStateTransitions; + google.registry.model.common.TimedTransitionProperty eapFeeSchedule; google.registry.model.common.TimedTransitionProperty renewBillingCostTransitions; google.registry.model.registry.Registry$TldType tldType; java.lang.String driveFolderId; diff --git a/javatests/google/registry/tools/EppToolVerifier.java b/javatests/google/registry/tools/EppToolVerifier.java index 5c37e1277..843fe9d3e 100644 --- a/javatests/google/registry/tools/EppToolVerifier.java +++ b/javatests/google/registry/tools/EppToolVerifier.java @@ -14,17 +14,14 @@ package google.registry.tools; -import static com.google.common.collect.Iterables.pairUp; import static com.google.common.truth.Truth.assertThat; import static google.registry.util.ResourceUtils.readResourceUtf8; import static google.registry.xml.XmlTestUtils.assertXmlEquals; import static java.nio.charset.StandardCharsets.UTF_8; -import static java.util.Arrays.asList; import static org.mockito.Matchers.eq; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; -import com.google.common.base.Pair; import com.google.common.base.Splitter; import com.google.common.collect.ImmutableMap; import com.google.common.net.MediaType; @@ -82,12 +79,16 @@ public class EppToolVerifier { params.capture()); List capturedParams = params.getAllValues(); assertThat(capturedParams).hasSize(xmlToMatch.length); - for (Pair xmlAndParams : pairUp(asList(xmlToMatch), capturedParams)) { - Map map = Splitter.on('&').withKeyValueSeparator('=') - .split(new String(xmlAndParams.getSecond(), UTF_8)); + for (int i = 0; i < xmlToMatch.length; i++) { + String xml = xmlToMatch[i]; + byte[] capturedParam = capturedParams.get(i); + Map map = + Splitter.on('&') + .withKeyValueSeparator('=') + .split(new String(capturedParam, UTF_8)); assertThat(map).hasSize(4); assertXmlEquals( - readResourceUtf8(getClass(), "testdata/" + xmlAndParams.getFirst()), + readResourceUtf8(getClass(), "testdata/" + xml), URLDecoder.decode(map.get("xml"), UTF_8.toString())); assertThat(map).containsEntry("dryRun", Boolean.toString(dryRun)); assertThat(map).containsEntry("clientIdentifier", clientIdentifier);