mirror of
https://github.com/google/nomulus
synced 2026-09-09 09:36:12 +00:00
Refactor to be more in line with a standard Gradle project structure
This commit is contained in:
@@ -0,0 +1,39 @@
|
||||
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 = "backup",
|
||||
srcs = glob(["*.java"]),
|
||||
resources = glob(["testdata/*"]),
|
||||
deps = [
|
||||
"//java/google/registry/backup",
|
||||
"//java/google/registry/model",
|
||||
"//java/google/registry/util",
|
||||
"//javatests/google/registry/testing",
|
||||
"//javatests/google/registry/testing/mapreduce",
|
||||
"//third_party/objectify:objectify-v4_1",
|
||||
"@com_google_appengine_api_1_0_sdk",
|
||||
"@com_google_appengine_tools_appengine_gcs_client",
|
||||
"@com_google_flogger",
|
||||
"@com_google_flogger_system_backend",
|
||||
"@com_google_guava",
|
||||
"@com_google_guava_testlib",
|
||||
"@com_google_truth",
|
||||
"@com_google_truth_extensions_truth_java8_extension",
|
||||
"@joda_time",
|
||||
"@junit",
|
||||
"@org_mockito_core",
|
||||
],
|
||||
)
|
||||
|
||||
GenTestRules(
|
||||
name = "GeneratedTestRules",
|
||||
test_files = glob(["*Test.java"]),
|
||||
deps = [":backup"],
|
||||
)
|
||||
@@ -0,0 +1,104 @@
|
||||
// Copyright 2017 The Nomulus Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package google.registry.backup;
|
||||
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
import static google.registry.model.ofy.CommitLogCheckpointRoot.loadRoot;
|
||||
import static google.registry.testing.DatastoreHelper.persistResource;
|
||||
import static google.registry.testing.TaskQueueHelper.assertNoTasksEnqueued;
|
||||
import static google.registry.testing.TaskQueueHelper.assertTasksEnqueued;
|
||||
import static google.registry.util.DateTimeUtils.START_OF_TIME;
|
||||
import static org.joda.time.DateTimeZone.UTC;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import com.google.common.collect.ImmutableMap;
|
||||
import google.registry.model.ofy.CommitLogCheckpoint;
|
||||
import google.registry.model.ofy.CommitLogCheckpointRoot;
|
||||
import google.registry.testing.AppEngineRule;
|
||||
import google.registry.testing.FakeClock;
|
||||
import google.registry.testing.TaskQueueHelper.TaskMatcher;
|
||||
import google.registry.util.Retrier;
|
||||
import google.registry.util.TaskQueueUtils;
|
||||
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;
|
||||
|
||||
/** Unit tests for {@link CommitLogCheckpointAction}. */
|
||||
@RunWith(JUnit4.class)
|
||||
public class CommitLogCheckpointActionTest {
|
||||
|
||||
private static final String QUEUE_NAME = "export-commits";
|
||||
|
||||
@Rule
|
||||
public final AppEngineRule appEngine = AppEngineRule.builder()
|
||||
.withDatastore()
|
||||
.withTaskQueue()
|
||||
.build();
|
||||
|
||||
CommitLogCheckpointStrategy strategy = mock(CommitLogCheckpointStrategy.class);
|
||||
|
||||
DateTime now = DateTime.now(UTC);
|
||||
CommitLogCheckpointAction task = new CommitLogCheckpointAction();
|
||||
|
||||
@Before
|
||||
public void before() {
|
||||
task.clock = new FakeClock(now);
|
||||
task.strategy = strategy;
|
||||
task.taskQueueUtils = new TaskQueueUtils(new Retrier(null, 1));
|
||||
when(strategy.computeCheckpoint())
|
||||
.thenReturn(
|
||||
CommitLogCheckpoint.create(
|
||||
now, ImmutableMap.of(1, START_OF_TIME, 2, START_OF_TIME, 3, START_OF_TIME)));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRun_noCheckpointEverWritten_writesCheckpointAndEnqueuesTask() {
|
||||
task.run();
|
||||
assertTasksEnqueued(
|
||||
QUEUE_NAME,
|
||||
new TaskMatcher()
|
||||
.url(ExportCommitLogDiffAction.PATH)
|
||||
.param(ExportCommitLogDiffAction.LOWER_CHECKPOINT_TIME_PARAM, START_OF_TIME.toString())
|
||||
.param(ExportCommitLogDiffAction.UPPER_CHECKPOINT_TIME_PARAM, now.toString()));
|
||||
assertThat(loadRoot().getLastWrittenTime()).isEqualTo(now);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRun_checkpointWrittenBeforeNow_writesCheckpointAndEnqueuesTask() {
|
||||
DateTime oneMinuteAgo = now.minusMinutes(1);
|
||||
persistResource(CommitLogCheckpointRoot.create(oneMinuteAgo));
|
||||
task.run();
|
||||
assertTasksEnqueued(
|
||||
QUEUE_NAME,
|
||||
new TaskMatcher()
|
||||
.url(ExportCommitLogDiffAction.PATH)
|
||||
.param(ExportCommitLogDiffAction.LOWER_CHECKPOINT_TIME_PARAM, oneMinuteAgo.toString())
|
||||
.param(ExportCommitLogDiffAction.UPPER_CHECKPOINT_TIME_PARAM, now.toString()));
|
||||
assertThat(loadRoot().getLastWrittenTime()).isEqualTo(now);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRun_checkpointWrittenAfterNow_doesntOverwrite_orEnqueueTask() {
|
||||
DateTime oneMinuteFromNow = now.plusMinutes(1);
|
||||
persistResource(CommitLogCheckpointRoot.create(oneMinuteFromNow));
|
||||
task.run();
|
||||
assertNoTasksEnqueued(QUEUE_NAME);
|
||||
assertThat(loadRoot().getLastWrittenTime()).isEqualTo(oneMinuteFromNow);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,311 @@
|
||||
// Copyright 2017 The Nomulus Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package google.registry.backup;
|
||||
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
import static google.registry.model.common.Cursor.CursorType.RDE_REPORT;
|
||||
import static google.registry.model.ofy.CommitLogBucket.getBucketKey;
|
||||
import static google.registry.model.ofy.ObjectifyService.ofy;
|
||||
import static google.registry.testing.DatastoreHelper.createTld;
|
||||
import static google.registry.util.DateTimeUtils.END_OF_TIME;
|
||||
import static google.registry.util.DateTimeUtils.START_OF_TIME;
|
||||
|
||||
import com.google.common.base.Supplier;
|
||||
import com.google.common.collect.ImmutableMap;
|
||||
import google.registry.model.common.Cursor;
|
||||
import google.registry.model.ofy.CommitLogBucket;
|
||||
import google.registry.model.ofy.CommitLogCheckpoint;
|
||||
import google.registry.model.ofy.Ofy;
|
||||
import google.registry.model.registry.Registry;
|
||||
import google.registry.testing.AppEngineRule;
|
||||
import google.registry.testing.FakeClock;
|
||||
import google.registry.testing.InjectRule;
|
||||
import org.joda.time.DateTime;
|
||||
import org.joda.time.Duration;
|
||||
import org.junit.Before;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.junit.runners.JUnit4;
|
||||
|
||||
/** Unit tests for {@link CommitLogCheckpointStrategy}. */
|
||||
@RunWith(JUnit4.class)
|
||||
public class CommitLogCheckpointStrategyTest {
|
||||
|
||||
@Rule
|
||||
public final AppEngineRule appEngine = AppEngineRule.builder()
|
||||
.withDatastore()
|
||||
.build();
|
||||
|
||||
@Rule
|
||||
public final InjectRule inject = new InjectRule();
|
||||
|
||||
final FakeClock clock = new FakeClock(DateTime.parse("2000-01-01TZ"));
|
||||
final Ofy ofy = new Ofy(clock);
|
||||
final CommitLogCheckpointStrategy strategy = new CommitLogCheckpointStrategy();
|
||||
|
||||
/**
|
||||
* Supplier to inject into CommitLogBucket for doling out predictable bucket IDs.
|
||||
*
|
||||
* <p>If not overridden, the supplier returns 1 so that other saves won't hit an NPE (since even
|
||||
* if they use saveWithoutBackup() the transaction still selects a bucket key early).
|
||||
*/
|
||||
final FakeSupplier<Integer> fakeBucketIdSupplier = new FakeSupplier<>(1);
|
||||
|
||||
/** Gross but necessary supplier that can be modified to return the desired value. */
|
||||
private static class FakeSupplier<T> implements Supplier<T> {
|
||||
/** Default value to return if 'value' is not set. */
|
||||
final T defaultValue;
|
||||
|
||||
/** Set this value field to make the supplier return this value. */
|
||||
T value = null;
|
||||
|
||||
public FakeSupplier(T defaultValue) {
|
||||
this.defaultValue = defaultValue;
|
||||
}
|
||||
|
||||
@Override
|
||||
public T get() {
|
||||
return value == null ? defaultValue : value;
|
||||
}
|
||||
}
|
||||
|
||||
@Before
|
||||
public void before() {
|
||||
strategy.clock = clock;
|
||||
strategy.ofy = ofy;
|
||||
|
||||
// Need to inject clock into Ofy so that createTld() below will get the right time.
|
||||
inject.setStaticField(Ofy.class, "clock", clock);
|
||||
// Inject a fake bucket ID supplier so we can dole out specific bucket IDs to commit logs.
|
||||
inject.setStaticField(CommitLogBucket.class, "bucketIdSupplier", fakeBucketIdSupplier);
|
||||
|
||||
// Create some fake TLDs to parent RegistryCursor test objects under.
|
||||
createTld("tld1");
|
||||
createTld("tld2");
|
||||
createTld("tld3");
|
||||
clock.advanceOneMilli();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test_readBucketTimestamps_noCommitLogs() {
|
||||
assertThat(strategy.readBucketTimestamps())
|
||||
.containsExactly(1, START_OF_TIME, 2, START_OF_TIME, 3, START_OF_TIME);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test_readBucketTimestamps_withSomeCommitLogs() {
|
||||
DateTime startTime = clock.nowUtc();
|
||||
writeCommitLogToBucket(1);
|
||||
clock.advanceOneMilli();
|
||||
writeCommitLogToBucket(2);
|
||||
assertThat(strategy.readBucketTimestamps())
|
||||
.containsExactly(1, startTime, 2, startTime.plusMillis(1), 3, START_OF_TIME);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test_readBucketTimestamps_againAfterUpdate_reflectsUpdate() {
|
||||
DateTime firstTime = clock.nowUtc();
|
||||
writeCommitLogToBucket(1);
|
||||
writeCommitLogToBucket(2);
|
||||
writeCommitLogToBucket(3);
|
||||
assertThat(strategy.readBucketTimestamps().values())
|
||||
.containsExactly(firstTime, firstTime, firstTime);
|
||||
clock.advanceOneMilli();
|
||||
writeCommitLogToBucket(1);
|
||||
DateTime secondTime = clock.nowUtc();
|
||||
assertThat(strategy.readBucketTimestamps())
|
||||
.containsExactly(1, secondTime, 2, firstTime, 3, firstTime);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test_readNewCommitLogsAndFindThreshold_noCommitsAtAll_returnsEndOfTime() {
|
||||
ImmutableMap<Integer, DateTime> bucketTimes =
|
||||
ImmutableMap.of(1, START_OF_TIME, 2, START_OF_TIME, 3, START_OF_TIME);
|
||||
assertThat(strategy.readNewCommitLogsAndFindThreshold(bucketTimes)).isEqualTo(END_OF_TIME);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test_readNewCommitLogsAndFindThreshold_noNewCommits_returnsEndOfTime() {
|
||||
DateTime now = clock.nowUtc();
|
||||
writeCommitLogToBucket(1);
|
||||
clock.advanceOneMilli();
|
||||
writeCommitLogToBucket(2);
|
||||
clock.advanceOneMilli();
|
||||
writeCommitLogToBucket(3);
|
||||
ImmutableMap<Integer, DateTime> bucketTimes =
|
||||
ImmutableMap.of(1, now, 2, now.plusMillis(1), 3, now.plusMillis(2));
|
||||
assertThat(strategy.readNewCommitLogsAndFindThreshold(bucketTimes)).isEqualTo(END_OF_TIME);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test_readNewCommitLogsAndFindThreshold_tiedNewCommits_returnsCommitTimeMinusOne() {
|
||||
DateTime now = clock.nowUtc();
|
||||
writeCommitLogToBucket(1);
|
||||
writeCommitLogToBucket(2);
|
||||
writeCommitLogToBucket(3);
|
||||
assertThat(strategy.readNewCommitLogsAndFindThreshold(
|
||||
ImmutableMap.of(1, START_OF_TIME, 2, START_OF_TIME, 3, START_OF_TIME)))
|
||||
.isEqualTo(now.minusMillis(1));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test_readNewCommitLogsAndFindThreshold_someNewCommits_returnsEarliestTimeMinusOne() {
|
||||
DateTime now = clock.nowUtc();
|
||||
writeCommitLogToBucket(1); // 1A
|
||||
writeCommitLogToBucket(2); // 2A
|
||||
writeCommitLogToBucket(3); // 3A
|
||||
clock.advanceBy(Duration.millis(5));
|
||||
writeCommitLogToBucket(1); // 1B
|
||||
writeCommitLogToBucket(2); // 2B
|
||||
writeCommitLogToBucket(3); // 3B
|
||||
clock.advanceBy(Duration.millis(5));
|
||||
writeCommitLogToBucket(1); // 1C
|
||||
writeCommitLogToBucket(2); // 2C
|
||||
writeCommitLogToBucket(3); // 3C
|
||||
// First pass times: 1 at T0, 2 at T+5, 3 at T+10.
|
||||
// Commits 1A, 2B, 3C are the commits seen in the first pass.
|
||||
// Commits 2A, 3A, 3B are all old prior commits that should be ignored.
|
||||
// Commit 1B is the first new commit for bucket 1, at T+5.
|
||||
// Commit 1C is the second new commit for bucket 1, at T+10, and should be ignored.
|
||||
// Commit 2C is the first new commit for bucket 2, at T+10.
|
||||
// Since 1B as a new commit is older than 1C, T+5 is the oldest new commit time.
|
||||
// Therefore, expect T+4 as the threshold time.
|
||||
assertThat(strategy.readNewCommitLogsAndFindThreshold(
|
||||
ImmutableMap.of(1, now, 2, now.plusMillis(5), 3, now.plusMillis(10))))
|
||||
.isEqualTo(now.plusMillis(4));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test_readNewCommitLogsAndFindThreshold_commitsAtBucketTimes() {
|
||||
DateTime now = clock.nowUtc();
|
||||
ImmutableMap<Integer, DateTime> bucketTimes =
|
||||
ImmutableMap.of(1, now.minusMillis(1), 2, now, 3, now.plusMillis(1));
|
||||
assertThat(strategy.readNewCommitLogsAndFindThreshold(bucketTimes)).isEqualTo(END_OF_TIME);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test_computeBucketCheckpointTimes_earlyThreshold_setsEverythingToThreshold() {
|
||||
DateTime now = clock.nowUtc();
|
||||
ImmutableMap<Integer, DateTime> bucketTimes =
|
||||
ImmutableMap.of(1, now.minusMillis(1), 2, now, 3, now.plusMillis(1));
|
||||
assertThat(strategy.computeBucketCheckpointTimes(bucketTimes, now.minusMillis(2)).values())
|
||||
.containsExactly(now.minusMillis(2), now.minusMillis(2), now.minusMillis(2));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test_computeBucketCheckpointTimes_middleThreshold_clampsToThreshold() {
|
||||
DateTime now = clock.nowUtc();
|
||||
ImmutableMap<Integer, DateTime> bucketTimes =
|
||||
ImmutableMap.of(1, now.minusMillis(1), 2, now, 3, now.plusMillis(1));
|
||||
assertThat(strategy.computeBucketCheckpointTimes(bucketTimes, now))
|
||||
.containsExactly(1, now.minusMillis(1), 2, now, 3, now);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test_computeBucketCheckpointTimes_lateThreshold_leavesBucketTimesAsIs() {
|
||||
DateTime now = clock.nowUtc();
|
||||
ImmutableMap<Integer, DateTime> bucketTimes =
|
||||
ImmutableMap.of(1, now.minusMillis(1), 2, now, 3, now.plusMillis(1));
|
||||
assertThat(strategy.computeBucketCheckpointTimes(bucketTimes, now.plusMillis(2)))
|
||||
.isEqualTo(bucketTimes);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test_computeCheckpoint_noCommitsAtAll_bucketCheckpointTimesAreStartOfTime() {
|
||||
assertThat(strategy.computeCheckpoint())
|
||||
.isEqualTo(CommitLogCheckpoint.create(
|
||||
clock.nowUtc(),
|
||||
ImmutableMap.of(1, START_OF_TIME, 2, START_OF_TIME, 3, START_OF_TIME)));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test_computeCheckpoint_noNewCommitLogs_bucketCheckpointTimesAreBucketTimes() {
|
||||
DateTime now = clock.nowUtc();
|
||||
writeCommitLogToBucket(1);
|
||||
clock.advanceOneMilli();
|
||||
writeCommitLogToBucket(2);
|
||||
clock.advanceOneMilli();
|
||||
writeCommitLogToBucket(3);
|
||||
clock.advanceOneMilli();
|
||||
DateTime checkpointTime = clock.nowUtc();
|
||||
assertThat(strategy.computeCheckpoint())
|
||||
.isEqualTo(CommitLogCheckpoint.create(
|
||||
checkpointTime,
|
||||
ImmutableMap.of(1, now, 2, now.plusMillis(1), 3, now.plusMillis(2))));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test_computeCheckpoint_someNewCommits_bucketCheckpointTimesAreClampedToThreshold() {
|
||||
DateTime now = clock.nowUtc();
|
||||
writeCommitLogToBucket(1); // 1A
|
||||
writeCommitLogToBucket(2); // 2A
|
||||
writeCommitLogToBucket(3); // 3A
|
||||
clock.advanceBy(Duration.millis(5));
|
||||
writeCommitLogToBucket(1); // 1B
|
||||
writeCommitLogToBucket(2); // 2B
|
||||
writeCommitLogToBucket(3); // 3B
|
||||
clock.advanceBy(Duration.millis(5));
|
||||
writeCommitLogToBucket(1); // 1C
|
||||
writeCommitLogToBucket(2); // 2C
|
||||
writeCommitLogToBucket(3); // 3C
|
||||
|
||||
// Set first pass times: 1 at T0, 2 at T+5, 3 at T+10.
|
||||
saveBucketWithLastWrittenTime(1, now);
|
||||
saveBucketWithLastWrittenTime(2, now.plusMillis(5));
|
||||
saveBucketWithLastWrittenTime(3, now.plusMillis(10));
|
||||
|
||||
// Commits 1A, 2B, 3C are the commits seen in the first pass.
|
||||
// Commits 2A, 3A, 3B are all old prior commits that should be ignored.
|
||||
// Commit 1B is the first new commit for bucket 1, at T+5.
|
||||
// Commit 1C is the second new commit for bucket 1, at T+10, and should be ignored.
|
||||
// Commit 2C is the first new commit for bucket 2, at T+10.
|
||||
// Since 1B as a new commit is older than 1C, T+5 is the oldest new commit time.
|
||||
// Therefore, expect T+4 as the threshold time.
|
||||
DateTime threshold = now.plusMillis(4);
|
||||
|
||||
// Advance clock before taking checkpoint.
|
||||
clock.advanceBy(Duration.millis(10));
|
||||
DateTime checkpointTime = clock.nowUtc();
|
||||
|
||||
// Bucket checkpoint times should be clamped as expected.
|
||||
assertThat(strategy.computeCheckpoint())
|
||||
.isEqualTo(CommitLogCheckpoint.create(
|
||||
checkpointTime,
|
||||
ImmutableMap.of(1, now, 2, threshold, 3, threshold)));
|
||||
}
|
||||
|
||||
private void writeCommitLogToBucket(final int bucketId) {
|
||||
fakeBucketIdSupplier.value = bucketId;
|
||||
ofy.transact(
|
||||
() -> {
|
||||
Cursor cursor =
|
||||
Cursor.create(RDE_REPORT, ofy.getTransactionTime(), Registry.get("tld" + bucketId));
|
||||
ofy().save().entity(cursor);
|
||||
});
|
||||
fakeBucketIdSupplier.value = null;
|
||||
}
|
||||
|
||||
private void saveBucketWithLastWrittenTime(final int bucketId, final DateTime lastWrittenTime) {
|
||||
ofy.transact(
|
||||
() ->
|
||||
ofy.saveWithoutBackup()
|
||||
.entity(
|
||||
CommitLogBucket.loadBucket(getBucketKey(bucketId))
|
||||
.asBuilder()
|
||||
.setLastWrittenTime(lastWrittenTime)
|
||||
.build()));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,138 @@
|
||||
// Copyright 2017 The Nomulus Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package google.registry.backup;
|
||||
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
import static google.registry.model.ofy.ObjectifyService.ofy;
|
||||
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import google.registry.model.contact.ContactResource;
|
||||
import google.registry.model.ofy.CommitLogManifest;
|
||||
import google.registry.model.ofy.CommitLogMutation;
|
||||
import google.registry.model.ofy.Ofy;
|
||||
import google.registry.testing.DatastoreHelper;
|
||||
import google.registry.testing.FakeClock;
|
||||
import google.registry.testing.FakeResponse;
|
||||
import google.registry.testing.InjectRule;
|
||||
import google.registry.testing.mapreduce.MapreduceTestCase;
|
||||
import org.joda.time.DateTime;
|
||||
import org.joda.time.Duration;
|
||||
import org.junit.Before;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.junit.runners.JUnit4;
|
||||
|
||||
/** Unit tests for {@link DeleteOldCommitLogsAction}. */
|
||||
@RunWith(JUnit4.class)
|
||||
public class DeleteOldCommitLogsActionTest
|
||||
extends MapreduceTestCase<DeleteOldCommitLogsAction> {
|
||||
|
||||
private final FakeClock clock = new FakeClock(DateTime.parse("2000-01-01TZ"));
|
||||
private final FakeResponse response = new FakeResponse();
|
||||
private ContactResource contact;
|
||||
|
||||
@Rule
|
||||
public final InjectRule inject = new InjectRule();
|
||||
|
||||
@Before
|
||||
public void setup() {
|
||||
inject.setStaticField(Ofy.class, "clock", clock);
|
||||
action = new DeleteOldCommitLogsAction();
|
||||
action.mrRunner = makeDefaultRunner();
|
||||
action.response = response;
|
||||
action.clock = clock;
|
||||
action.maxAge = Duration.standardDays(30);
|
||||
|
||||
ContactResource contact = DatastoreHelper.persistActiveContact("TheRegistrar");
|
||||
clock.advanceBy(Duration.standardDays(1));
|
||||
DatastoreHelper.persistResourceWithCommitLog(contact);
|
||||
|
||||
prepareData();
|
||||
}
|
||||
|
||||
private void runMapreduce(Duration maxAge) throws Exception {
|
||||
action.maxAge = maxAge;
|
||||
action.run();
|
||||
executeTasksUntilEmpty("mapreduce");
|
||||
ofy().clearSessionCache();
|
||||
}
|
||||
|
||||
private void mutateContact(String email) {
|
||||
ofy().clearSessionCache();
|
||||
ContactResource contact = ofy().load()
|
||||
.type(ContactResource.class)
|
||||
.first()
|
||||
.now()
|
||||
.asBuilder()
|
||||
.setEmailAddress(email)
|
||||
.build();
|
||||
DatastoreHelper.persistResourceWithCommitLog(contact);
|
||||
}
|
||||
|
||||
private void prepareData() {
|
||||
|
||||
for (int i = 0; i < 10; i++) {
|
||||
clock.advanceBy(Duration.standardDays(7));
|
||||
String email = String.format("pumpkin_%d@cat.test", i);
|
||||
mutateContact(email);
|
||||
}
|
||||
ofy().clearSessionCache();
|
||||
|
||||
contact = ofy().load().type(ContactResource.class).first().now();
|
||||
|
||||
// The following value might change if {@link CommitLogRevisionsTranslatorFactory} changes.
|
||||
assertThat(contact.getRevisions().size()).isEqualTo(6);
|
||||
|
||||
// Before deleting the unneeded manifests - we have 11 of them (one for the first
|
||||
// creation, and 10 more for the mutateContacts)
|
||||
assertThat(ofy().load().type(CommitLogManifest.class).count()).isEqualTo(11);
|
||||
// And each DatastoreHelper.persistResourceWithCommitLog creates 3 mutations
|
||||
assertThat(ofy().load().type(CommitLogMutation.class).count()).isEqualTo(33);
|
||||
}
|
||||
|
||||
private <T> ImmutableList<T> ofyLoadType(Class<T> clazz) {
|
||||
return ImmutableList.copyOf(ofy().load().type(clazz).iterable());
|
||||
}
|
||||
|
||||
/**
|
||||
* Check that with very short maxAge, only the referenced elements remain.
|
||||
*/
|
||||
@Test
|
||||
public void test_shortMaxAge() throws Exception {
|
||||
runMapreduce(Duration.millis(1));
|
||||
|
||||
assertThat(ImmutableList.copyOf(ofy().load().type(CommitLogManifest.class).keys().iterable()))
|
||||
.containsExactlyElementsIn(contact.getRevisions().values());
|
||||
|
||||
// And each DatastoreHelper.persistResourceWithCommitLog creates 3 mutations
|
||||
assertThat(ofyLoadType(CommitLogMutation.class)).hasSize(contact.getRevisions().size() * 3);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check that with very long maxAge, all the elements remain.
|
||||
*/
|
||||
@Test
|
||||
public void test_longMaxAge() throws Exception {
|
||||
|
||||
ImmutableList<CommitLogManifest> initialManifests = ofyLoadType(CommitLogManifest.class);
|
||||
ImmutableList<CommitLogMutation> initialMutations = ofyLoadType(CommitLogMutation.class);
|
||||
|
||||
runMapreduce(Duration.standardDays(1000));
|
||||
|
||||
assertThat(ofyLoadType(CommitLogManifest.class)).containsExactlyElementsIn(initialManifests);
|
||||
assertThat(ofyLoadType(CommitLogMutation.class)).containsExactlyElementsIn(initialMutations);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,463 @@
|
||||
// Copyright 2017 The Nomulus Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package google.registry.backup;
|
||||
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
import static com.google.common.truth.Truth.assertWithMessage;
|
||||
import static google.registry.backup.BackupUtils.GcsMetadataKeys.LOWER_BOUND_CHECKPOINT;
|
||||
import static google.registry.backup.BackupUtils.GcsMetadataKeys.NUM_TRANSACTIONS;
|
||||
import static google.registry.backup.BackupUtils.GcsMetadataKeys.UPPER_BOUND_CHECKPOINT;
|
||||
import static google.registry.backup.BackupUtils.deserializeEntities;
|
||||
import static google.registry.testing.DatastoreHelper.persistResource;
|
||||
import static google.registry.util.DateTimeUtils.START_OF_TIME;
|
||||
import static org.joda.time.DateTimeZone.UTC;
|
||||
|
||||
import com.google.appengine.tools.cloudstorage.GcsFilename;
|
||||
import com.google.appengine.tools.cloudstorage.GcsService;
|
||||
import com.google.appengine.tools.cloudstorage.GcsServiceFactory;
|
||||
import com.google.common.collect.ImmutableMap;
|
||||
import com.googlecode.objectify.Key;
|
||||
import google.registry.model.ImmutableObject;
|
||||
import google.registry.model.ofy.CommitLogBucket;
|
||||
import google.registry.model.ofy.CommitLogCheckpoint;
|
||||
import google.registry.model.ofy.CommitLogManifest;
|
||||
import google.registry.model.ofy.CommitLogMutation;
|
||||
import google.registry.testing.AppEngineRule;
|
||||
import google.registry.testing.GcsTestingUtils;
|
||||
import google.registry.testing.TestObject;
|
||||
import java.util.List;
|
||||
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;
|
||||
|
||||
/** Unit tests for {@link ExportCommitLogDiffAction}. */
|
||||
@RunWith(JUnit4.class)
|
||||
public class ExportCommitLogDiffActionTest {
|
||||
|
||||
@Rule
|
||||
public final AppEngineRule appEngine = AppEngineRule.builder()
|
||||
.withDatastore()
|
||||
.build();
|
||||
|
||||
/** Local GCS service available for testing. */
|
||||
private final GcsService gcsService = GcsServiceFactory.createGcsService();
|
||||
|
||||
private final DateTime now = DateTime.now(UTC);
|
||||
private final DateTime oneMinuteAgo = now.minusMinutes(1);
|
||||
|
||||
private final ExportCommitLogDiffAction task = new ExportCommitLogDiffAction();
|
||||
|
||||
@Before
|
||||
public void before() {
|
||||
task.gcsService = gcsService;
|
||||
task.gcsBucket = "gcs bucket";
|
||||
task.batchSize = 5;
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRun_noCommitHistory_onlyUpperCheckpointExported() throws Exception {
|
||||
task.lowerCheckpointTime = oneMinuteAgo;
|
||||
task.upperCheckpointTime = now;
|
||||
|
||||
persistResource(CommitLogCheckpoint.create(
|
||||
oneMinuteAgo,
|
||||
ImmutableMap.of(1, oneMinuteAgo, 2, oneMinuteAgo, 3, oneMinuteAgo)));
|
||||
CommitLogCheckpoint upperCheckpoint = persistResource(CommitLogCheckpoint.create(
|
||||
now,
|
||||
ImmutableMap.of(1, now, 2, now, 3, now)));
|
||||
|
||||
// Don't persist any manifests or mutations.
|
||||
|
||||
task.run();
|
||||
|
||||
GcsFilename expectedFilename = new GcsFilename("gcs bucket", "commit_diff_until_" + now);
|
||||
assertWithMessage("GCS file not found: " + expectedFilename)
|
||||
.that(gcsService.getMetadata(expectedFilename)).isNotNull();
|
||||
assertThat(gcsService.getMetadata(expectedFilename).getOptions().getUserMetadata())
|
||||
.containsExactly(
|
||||
LOWER_BOUND_CHECKPOINT,
|
||||
oneMinuteAgo.toString(),
|
||||
UPPER_BOUND_CHECKPOINT,
|
||||
now.toString(),
|
||||
NUM_TRANSACTIONS,
|
||||
"0");
|
||||
List<ImmutableObject> exported =
|
||||
deserializeEntities(GcsTestingUtils.readGcsFile(gcsService, expectedFilename));
|
||||
assertThat(exported).containsExactly(upperCheckpoint);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRun_regularCommitHistory_exportsCorrectCheckpointDiff() throws Exception {
|
||||
task.lowerCheckpointTime = oneMinuteAgo;
|
||||
task.upperCheckpointTime = now;
|
||||
|
||||
// Persist the lower and upper checkpoints, with 3 buckets each and staggered times. We respect
|
||||
// the real invariant that the time for bucket n in the lower checkpoint is <= the time for
|
||||
// that bucket in the upper.
|
||||
persistResource(CommitLogCheckpoint.create(
|
||||
oneMinuteAgo,
|
||||
ImmutableMap.of(
|
||||
1, oneMinuteAgo,
|
||||
2, oneMinuteAgo.minusDays(1),
|
||||
3, oneMinuteAgo.minusDays(2))));
|
||||
CommitLogCheckpoint upperCheckpoint = persistResource(CommitLogCheckpoint.create(
|
||||
now,
|
||||
ImmutableMap.of(
|
||||
1, now,
|
||||
2, now.minusDays(1),
|
||||
3, oneMinuteAgo.minusDays(2)))); // Note that this matches the lower bound.
|
||||
|
||||
// Persist some fake commit log manifests.
|
||||
// These shouldn't be in the diff because the lower bound is exclusive.
|
||||
persistManifestAndMutation(1, oneMinuteAgo);
|
||||
persistManifestAndMutation(2, oneMinuteAgo.minusDays(1));
|
||||
persistManifestAndMutation(3, oneMinuteAgo.minusDays(2)); // Even though it's == upper bound.
|
||||
// These shouldn't be in the diff because they are above the upper bound.
|
||||
persistManifestAndMutation(1, now.plusMillis(1));
|
||||
persistManifestAndMutation(2, now.minusDays(1).plusMillis(1));
|
||||
persistManifestAndMutation(3, oneMinuteAgo.minusDays(2).plusMillis(1));
|
||||
// These should be in the diff because they are between the bounds. (Not possible for bucket 3.)
|
||||
persistManifestAndMutation(1, now.minusMillis(1));
|
||||
persistManifestAndMutation(2, now.minusDays(1).minusMillis(1));
|
||||
// These should be in the diff because they are at the upper bound. (Not possible for bucket 3.)
|
||||
persistManifestAndMutation(1, now);
|
||||
persistManifestAndMutation(2, now.minusDays(1));
|
||||
|
||||
task.run();
|
||||
|
||||
GcsFilename expectedFilename = new GcsFilename("gcs bucket", "commit_diff_until_" + now);
|
||||
assertWithMessage("GCS file not found: " + expectedFilename)
|
||||
.that(gcsService.getMetadata(expectedFilename)).isNotNull();
|
||||
assertThat(gcsService.getMetadata(expectedFilename).getOptions().getUserMetadata())
|
||||
.containsExactly(
|
||||
LOWER_BOUND_CHECKPOINT,
|
||||
oneMinuteAgo.toString(),
|
||||
UPPER_BOUND_CHECKPOINT,
|
||||
now.toString(),
|
||||
NUM_TRANSACTIONS,
|
||||
"4");
|
||||
List<ImmutableObject> exported =
|
||||
deserializeEntities(GcsTestingUtils.readGcsFile(gcsService, expectedFilename));
|
||||
assertThat(exported.get(0)).isEqualTo(upperCheckpoint);
|
||||
// We expect these manifests, in time order, with matching mutations.
|
||||
CommitLogManifest manifest1 = createManifest(2, now.minusDays(1).minusMillis(1));
|
||||
CommitLogManifest manifest2 = createManifest(2, now.minusDays(1));
|
||||
CommitLogManifest manifest3 = createManifest(1, now.minusMillis(1));
|
||||
CommitLogManifest manifest4 = createManifest(1, now);
|
||||
assertThat(exported).containsExactly(
|
||||
upperCheckpoint,
|
||||
manifest1,
|
||||
createMutation(manifest1),
|
||||
manifest2,
|
||||
createMutation(manifest2),
|
||||
manifest3,
|
||||
createMutation(manifest3),
|
||||
manifest4,
|
||||
createMutation(manifest4))
|
||||
.inOrder();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRun_simultaneousTransactions_bothExported() throws Exception {
|
||||
task.lowerCheckpointTime = oneMinuteAgo;
|
||||
task.upperCheckpointTime = now;
|
||||
|
||||
persistResource(CommitLogCheckpoint.create(
|
||||
oneMinuteAgo,
|
||||
ImmutableMap.of(1, START_OF_TIME, 2, START_OF_TIME, 3, START_OF_TIME)));
|
||||
CommitLogCheckpoint upperCheckpoint = persistResource(CommitLogCheckpoint.create(
|
||||
now,
|
||||
ImmutableMap.of(1, now, 2, now, 3, now)));
|
||||
|
||||
// Persist some fake commit log manifests that are at the same time but in different buckets.
|
||||
persistManifestAndMutation(1, oneMinuteAgo);
|
||||
persistManifestAndMutation(2, oneMinuteAgo);
|
||||
persistManifestAndMutation(1, now);
|
||||
persistManifestAndMutation(2, now);
|
||||
|
||||
task.run();
|
||||
|
||||
GcsFilename expectedFilename = new GcsFilename("gcs bucket", "commit_diff_until_" + now);
|
||||
assertWithMessage("GCS file not found: " + expectedFilename)
|
||||
.that(gcsService.getMetadata(expectedFilename)).isNotNull();
|
||||
assertThat(gcsService.getMetadata(expectedFilename).getOptions().getUserMetadata())
|
||||
.containsExactly(
|
||||
LOWER_BOUND_CHECKPOINT,
|
||||
oneMinuteAgo.toString(),
|
||||
UPPER_BOUND_CHECKPOINT,
|
||||
now.toString(),
|
||||
NUM_TRANSACTIONS,
|
||||
"4");
|
||||
List<ImmutableObject> exported =
|
||||
deserializeEntities(GcsTestingUtils.readGcsFile(gcsService, expectedFilename));
|
||||
assertThat(exported.get(0)).isEqualTo(upperCheckpoint);
|
||||
// We expect these manifests, in the order below, with matching mutations.
|
||||
CommitLogManifest manifest1 = createManifest(1, oneMinuteAgo);
|
||||
CommitLogManifest manifest2 = createManifest(2, oneMinuteAgo);
|
||||
CommitLogManifest manifest3 = createManifest(1, now);
|
||||
CommitLogManifest manifest4 = createManifest(2, now);
|
||||
assertThat(exported).containsExactly(
|
||||
upperCheckpoint,
|
||||
manifest1,
|
||||
createMutation(manifest1),
|
||||
manifest2,
|
||||
createMutation(manifest2),
|
||||
manifest3,
|
||||
createMutation(manifest3),
|
||||
manifest4,
|
||||
createMutation(manifest4))
|
||||
.inOrder();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRun_exportsAcrossMultipleBatches() throws Exception {
|
||||
task.batchSize = 2;
|
||||
task.lowerCheckpointTime = oneMinuteAgo;
|
||||
task.upperCheckpointTime = now;
|
||||
|
||||
persistResource(CommitLogCheckpoint.create(
|
||||
oneMinuteAgo,
|
||||
ImmutableMap.of(1, START_OF_TIME, 2, START_OF_TIME, 3, START_OF_TIME)));
|
||||
CommitLogCheckpoint upperCheckpoint = persistResource(CommitLogCheckpoint.create(
|
||||
now,
|
||||
ImmutableMap.of(1, now, 2, now, 3, now)));
|
||||
|
||||
// Persist some fake commit log manifests.
|
||||
persistManifestAndMutation(1, oneMinuteAgo);
|
||||
persistManifestAndMutation(2, oneMinuteAgo);
|
||||
persistManifestAndMutation(3, oneMinuteAgo);
|
||||
persistManifestAndMutation(1, now);
|
||||
persistManifestAndMutation(2, now);
|
||||
persistManifestAndMutation(3, now);
|
||||
|
||||
task.run();
|
||||
|
||||
GcsFilename expectedFilename = new GcsFilename("gcs bucket", "commit_diff_until_" + now);
|
||||
assertWithMessage("GCS file not found: " + expectedFilename)
|
||||
.that(gcsService.getMetadata(expectedFilename)).isNotNull();
|
||||
assertThat(gcsService.getMetadata(expectedFilename).getOptions().getUserMetadata())
|
||||
.containsExactly(
|
||||
LOWER_BOUND_CHECKPOINT,
|
||||
oneMinuteAgo.toString(),
|
||||
UPPER_BOUND_CHECKPOINT,
|
||||
now.toString(),
|
||||
NUM_TRANSACTIONS,
|
||||
"6");
|
||||
List<ImmutableObject> exported =
|
||||
deserializeEntities(GcsTestingUtils.readGcsFile(gcsService, expectedFilename));
|
||||
assertThat(exported.get(0)).isEqualTo(upperCheckpoint);
|
||||
// We expect these manifests, in the order below, with matching mutations.
|
||||
CommitLogManifest manifest1 = createManifest(1, oneMinuteAgo);
|
||||
CommitLogManifest manifest2 = createManifest(2, oneMinuteAgo);
|
||||
CommitLogManifest manifest3 = createManifest(3, oneMinuteAgo);
|
||||
CommitLogManifest manifest4 = createManifest(1, now);
|
||||
CommitLogManifest manifest5 = createManifest(2, now);
|
||||
CommitLogManifest manifest6 = createManifest(3, now);
|
||||
assertThat(exported).containsExactly(
|
||||
upperCheckpoint,
|
||||
manifest1,
|
||||
createMutation(manifest1),
|
||||
manifest2,
|
||||
createMutation(manifest2),
|
||||
manifest3,
|
||||
createMutation(manifest3),
|
||||
manifest4,
|
||||
createMutation(manifest4),
|
||||
manifest5,
|
||||
createMutation(manifest5),
|
||||
manifest6,
|
||||
createMutation(manifest6))
|
||||
.inOrder();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRun_checkpointDiffWithNeverTouchedBuckets_exportsCorrectly() throws Exception {
|
||||
task.lowerCheckpointTime = oneMinuteAgo;
|
||||
task.upperCheckpointTime = now;
|
||||
|
||||
persistResource(CommitLogCheckpoint.create(
|
||||
oneMinuteAgo,
|
||||
ImmutableMap.of(1, START_OF_TIME, 2, START_OF_TIME, 3, START_OF_TIME)));
|
||||
CommitLogCheckpoint upperCheckpoint = persistResource(CommitLogCheckpoint.create(
|
||||
now,
|
||||
ImmutableMap.of(1, START_OF_TIME, 2, START_OF_TIME, 3, START_OF_TIME)));
|
||||
|
||||
// Don't persist any commit log manifests; we're just checking that the task runs correctly
|
||||
// even if the upper timestamp contains START_OF_TIME values.
|
||||
|
||||
task.run();
|
||||
|
||||
GcsFilename expectedFilename = new GcsFilename("gcs bucket", "commit_diff_until_" + now);
|
||||
assertWithMessage("GCS file not found: " + expectedFilename)
|
||||
.that(gcsService.getMetadata(expectedFilename)).isNotNull();
|
||||
assertThat(gcsService.getMetadata(expectedFilename).getOptions().getUserMetadata())
|
||||
.containsExactly(
|
||||
LOWER_BOUND_CHECKPOINT,
|
||||
oneMinuteAgo.toString(),
|
||||
UPPER_BOUND_CHECKPOINT,
|
||||
now.toString(),
|
||||
NUM_TRANSACTIONS,
|
||||
"0");
|
||||
List<ImmutableObject> exported =
|
||||
deserializeEntities(GcsTestingUtils.readGcsFile(gcsService, expectedFilename));
|
||||
// We expect no manifests or mutations, only the upper checkpoint.
|
||||
assertThat(exported).containsExactly(upperCheckpoint);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRun_checkpointDiffWithNonExistentBucketTimestamps_exportsCorrectly()
|
||||
throws Exception {
|
||||
// Non-existent bucket timestamps can exist when the commit log bucket count was increased
|
||||
// recently.
|
||||
|
||||
task.lowerCheckpointTime = oneMinuteAgo;
|
||||
task.upperCheckpointTime = now;
|
||||
|
||||
// No lower checkpoint times are persisted for buckets 2 and 3 (simulating a recent increase in
|
||||
// the number of commit log buckets from 1 to 3), so all mutations on buckets 2 and 3, even
|
||||
// those older than the lower checkpoint, will be exported.
|
||||
persistResource(
|
||||
CommitLogCheckpoint.createForTest(oneMinuteAgo, ImmutableMap.of(1, oneMinuteAgo)));
|
||||
CommitLogCheckpoint upperCheckpoint =
|
||||
persistResource(
|
||||
CommitLogCheckpoint.create(
|
||||
now,
|
||||
ImmutableMap.of(
|
||||
1, now,
|
||||
2, now.minusDays(1),
|
||||
3, oneMinuteAgo.minusDays(2))));
|
||||
|
||||
// These shouldn't be in the diff because the lower bound is exclusive.
|
||||
persistManifestAndMutation(1, oneMinuteAgo);
|
||||
// These shouldn't be in the diff because they are above the upper bound.
|
||||
persistManifestAndMutation(1, now.plusMillis(1));
|
||||
persistManifestAndMutation(2, now.minusDays(1).plusMillis(1));
|
||||
persistManifestAndMutation(3, oneMinuteAgo.minusDays(2).plusMillis(1));
|
||||
// These should be in the diff because they happened after START_OF_TIME on buckets with
|
||||
// non-existent timestamps.
|
||||
persistManifestAndMutation(2, oneMinuteAgo.minusDays(1));
|
||||
persistManifestAndMutation(3, oneMinuteAgo.minusDays(2));
|
||||
// These should be in the diff because they are between the bounds.
|
||||
persistManifestAndMutation(1, now.minusMillis(1));
|
||||
persistManifestAndMutation(2, now.minusDays(1).minusMillis(1));
|
||||
// These should be in the diff because they are at the upper bound.
|
||||
persistManifestAndMutation(1, now);
|
||||
persistManifestAndMutation(2, now.minusDays(1));
|
||||
|
||||
task.run();
|
||||
|
||||
GcsFilename expectedFilename = new GcsFilename("gcs bucket", "commit_diff_until_" + now);
|
||||
assertWithMessage("GCS file not found: " + expectedFilename)
|
||||
.that(gcsService.getMetadata(expectedFilename))
|
||||
.isNotNull();
|
||||
assertThat(gcsService.getMetadata(expectedFilename).getOptions().getUserMetadata())
|
||||
.containsExactly(
|
||||
LOWER_BOUND_CHECKPOINT,
|
||||
oneMinuteAgo.toString(),
|
||||
UPPER_BOUND_CHECKPOINT,
|
||||
now.toString(),
|
||||
NUM_TRANSACTIONS,
|
||||
"6");
|
||||
List<ImmutableObject> exported =
|
||||
deserializeEntities(GcsTestingUtils.readGcsFile(gcsService, expectedFilename));
|
||||
assertThat(exported.get(0)).isEqualTo(upperCheckpoint);
|
||||
// We expect these manifests, in time order, with matching mutations.
|
||||
CommitLogManifest manifest1 = createManifest(3, oneMinuteAgo.minusDays(2));
|
||||
CommitLogManifest manifest2 = createManifest(2, oneMinuteAgo.minusDays(1));
|
||||
CommitLogManifest manifest3 = createManifest(2, now.minusDays(1).minusMillis(1));
|
||||
CommitLogManifest manifest4 = createManifest(2, now.minusDays(1));
|
||||
CommitLogManifest manifest5 = createManifest(1, now.minusMillis(1));
|
||||
CommitLogManifest manifest6 = createManifest(1, now);
|
||||
assertThat(exported)
|
||||
.containsExactly(
|
||||
upperCheckpoint,
|
||||
manifest1,
|
||||
createMutation(manifest1),
|
||||
manifest2,
|
||||
createMutation(manifest2),
|
||||
manifest3,
|
||||
createMutation(manifest3),
|
||||
manifest4,
|
||||
createMutation(manifest4),
|
||||
manifest5,
|
||||
createMutation(manifest5),
|
||||
manifest6,
|
||||
createMutation(manifest6))
|
||||
.inOrder();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRun_exportingFromStartOfTime_exportsAllCommits() throws Exception {
|
||||
task.lowerCheckpointTime = START_OF_TIME;
|
||||
task.upperCheckpointTime = now;
|
||||
|
||||
CommitLogCheckpoint upperCheckpoint = persistResource(CommitLogCheckpoint.create(
|
||||
now,
|
||||
ImmutableMap.of(1, now, 2, now, 3, now)));
|
||||
|
||||
// Persist some fake commit log manifests.
|
||||
persistManifestAndMutation(1, START_OF_TIME.plusMillis(1)); // Oldest possible manifest time.
|
||||
persistManifestAndMutation(2, oneMinuteAgo);
|
||||
persistManifestAndMutation(3, now);
|
||||
|
||||
task.run();
|
||||
|
||||
GcsFilename expectedFilename = new GcsFilename("gcs bucket", "commit_diff_until_" + now);
|
||||
assertWithMessage("GCS file not found: " + expectedFilename)
|
||||
.that(gcsService.getMetadata(expectedFilename)).isNotNull();
|
||||
assertThat(gcsService.getMetadata(expectedFilename).getOptions().getUserMetadata())
|
||||
.containsExactly(
|
||||
LOWER_BOUND_CHECKPOINT,
|
||||
START_OF_TIME.toString(),
|
||||
UPPER_BOUND_CHECKPOINT,
|
||||
now.toString(),
|
||||
NUM_TRANSACTIONS,
|
||||
"3");
|
||||
List<ImmutableObject> exported =
|
||||
deserializeEntities(GcsTestingUtils.readGcsFile(gcsService, expectedFilename));
|
||||
assertThat(exported.get(0)).isEqualTo(upperCheckpoint);
|
||||
// We expect these manifests, in the order below, with matching mutations.
|
||||
CommitLogManifest manifest1 = createManifest(1, START_OF_TIME.plusMillis(1));
|
||||
CommitLogManifest manifest2 = createManifest(2, oneMinuteAgo);
|
||||
CommitLogManifest manifest3 = createManifest(3, now);
|
||||
assertThat(exported).containsExactly(
|
||||
upperCheckpoint,
|
||||
manifest1,
|
||||
createMutation(manifest1),
|
||||
manifest2,
|
||||
createMutation(manifest2),
|
||||
manifest3,
|
||||
createMutation(manifest3))
|
||||
.inOrder();
|
||||
}
|
||||
|
||||
private CommitLogManifest createManifest(int bucketNum, DateTime commitTime) {
|
||||
return CommitLogManifest.create(CommitLogBucket.getBucketKey(bucketNum), commitTime, null);
|
||||
}
|
||||
|
||||
private CommitLogMutation createMutation(CommitLogManifest manifest) {
|
||||
return CommitLogMutation.create(
|
||||
Key.create(manifest),
|
||||
TestObject.create(manifest.getCommitTime().toString()));
|
||||
}
|
||||
|
||||
private void persistManifestAndMutation(int bucketNum, DateTime commitTime) {
|
||||
persistResource(
|
||||
createMutation(persistResource(createManifest(bucketNum, commitTime))));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,242 @@
|
||||
// Copyright 2017 The Nomulus Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package google.registry.backup;
|
||||
|
||||
import static com.google.common.collect.Iterables.transform;
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
import static com.google.common.util.concurrent.MoreExecutors.newDirectExecutorService;
|
||||
import static google.registry.backup.BackupUtils.GcsMetadataKeys.LOWER_BOUND_CHECKPOINT;
|
||||
import static google.registry.backup.ExportCommitLogDiffAction.DIFF_FILE_PREFIX;
|
||||
import static google.registry.testing.JUnitBackports.assertThrows;
|
||||
import static java.lang.reflect.Proxy.newProxyInstance;
|
||||
import static org.joda.time.DateTimeZone.UTC;
|
||||
import static org.junit.Assert.fail;
|
||||
|
||||
import com.google.appengine.tools.cloudstorage.GcsFileMetadata;
|
||||
import com.google.appengine.tools.cloudstorage.GcsFileOptions;
|
||||
import com.google.appengine.tools.cloudstorage.GcsFilename;
|
||||
import com.google.appengine.tools.cloudstorage.GcsService;
|
||||
import com.google.appengine.tools.cloudstorage.GcsServiceFactory;
|
||||
import com.google.appengine.tools.cloudstorage.ListItem;
|
||||
import com.google.appengine.tools.cloudstorage.ListResult;
|
||||
import com.google.common.collect.Iterators;
|
||||
import com.google.common.flogger.LoggerConfig;
|
||||
import com.google.common.testing.TestLogHandler;
|
||||
import google.registry.testing.AppEngineRule;
|
||||
import java.io.IOException;
|
||||
import java.lang.reflect.InvocationHandler;
|
||||
import java.lang.reflect.Method;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.Callable;
|
||||
import java.util.logging.LogRecord;
|
||||
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;
|
||||
|
||||
/** Unit tests for {@link GcsDiffFileLister}. */
|
||||
@RunWith(JUnit4.class)
|
||||
public class GcsDiffFileListerTest {
|
||||
|
||||
static final String GCS_BUCKET = "gcs bucket";
|
||||
|
||||
final DateTime now = DateTime.now(UTC);
|
||||
final GcsDiffFileLister diffLister = new GcsDiffFileLister();
|
||||
final GcsService gcsService = GcsServiceFactory.createGcsService();
|
||||
private final TestLogHandler logHandler = new TestLogHandler();
|
||||
|
||||
@Rule
|
||||
public final AppEngineRule appEngine = AppEngineRule.builder()
|
||||
.withDatastore()
|
||||
.build();
|
||||
|
||||
@Before
|
||||
public void before() throws Exception {
|
||||
diffLister.gcsService = gcsService;
|
||||
diffLister.gcsBucket = GCS_BUCKET;
|
||||
diffLister.executor = newDirectExecutorService();
|
||||
for (int i = 0; i < 5; i++) {
|
||||
gcsService.createOrReplace(
|
||||
new GcsFilename(GCS_BUCKET, DIFF_FILE_PREFIX + now.minusMinutes(i)),
|
||||
new GcsFileOptions.Builder()
|
||||
.addUserMetadata(LOWER_BOUND_CHECKPOINT, now.minusMinutes(i + 1).toString())
|
||||
.build(),
|
||||
ByteBuffer.wrap(new byte[]{1, 2, 3}));
|
||||
}
|
||||
LoggerConfig.getConfig(GcsDiffFileLister.class).addHandler(logHandler);
|
||||
}
|
||||
|
||||
private Iterable<DateTime> extractTimesFromDiffFiles(List<GcsFileMetadata> diffFiles) {
|
||||
return transform(
|
||||
diffFiles,
|
||||
metadata ->
|
||||
DateTime.parse(
|
||||
metadata.getFilename().getObjectName().substring(DIFF_FILE_PREFIX.length())));
|
||||
}
|
||||
|
||||
private Iterable<DateTime> listDiffFiles(DateTime fromTime, DateTime toTime) {
|
||||
return extractTimesFromDiffFiles(diffLister.listDiffFiles(fromTime, toTime));
|
||||
}
|
||||
|
||||
private void addGcsFile(int fileAge, int prevAge) throws IOException {
|
||||
gcsService.createOrReplace(
|
||||
new GcsFilename(GCS_BUCKET, DIFF_FILE_PREFIX + now.minusMinutes(fileAge)),
|
||||
new GcsFileOptions.Builder()
|
||||
.addUserMetadata(LOWER_BOUND_CHECKPOINT, now.minusMinutes(prevAge).toString())
|
||||
.build(),
|
||||
ByteBuffer.wrap(new byte[]{1, 2, 3}));
|
||||
}
|
||||
|
||||
private void assertLogContains(String message) {
|
||||
for (LogRecord entry : logHandler.getStoredLogRecords()) {
|
||||
if (entry.getMessage().contains(message)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
fail("No log entry contains " + message);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testList_noFilesFound() {
|
||||
DateTime fromTime = now.plusMillis(1);
|
||||
assertThat(listDiffFiles(fromTime, null)).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testList_patchesHoles() {
|
||||
// Fake out the GCS list() method to return only the first and last file.
|
||||
// We can't use Mockito.spy() because GcsService's impl is final.
|
||||
diffLister.gcsService = (GcsService) newProxyInstance(
|
||||
GcsService.class.getClassLoader(),
|
||||
new Class<?>[] {GcsService.class},
|
||||
new InvocationHandler() {
|
||||
@Override
|
||||
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
|
||||
if (method.getName().equals("list")) {
|
||||
// ListResult is an incredibly annoying thing to construct. It needs to be fed from a
|
||||
// Callable that returns Iterators, each representing a batch of results.
|
||||
return new ListResult(new Callable<Iterator<ListItem>>() {
|
||||
boolean called = false;
|
||||
|
||||
@Override
|
||||
public Iterator<ListItem> call() {
|
||||
try {
|
||||
return called ? null : Iterators.forArray(
|
||||
new ListItem.Builder()
|
||||
.setName(DIFF_FILE_PREFIX + now)
|
||||
.build(),
|
||||
new ListItem.Builder()
|
||||
.setName(DIFF_FILE_PREFIX + now.minusMinutes(4))
|
||||
.build());
|
||||
} finally {
|
||||
called = true;
|
||||
}
|
||||
}});
|
||||
}
|
||||
return method.invoke(gcsService, args);
|
||||
}});
|
||||
DateTime fromTime = now.minusMinutes(4).minusSeconds(1);
|
||||
// Request all files with checkpoint > fromTime.
|
||||
assertThat(listDiffFiles(fromTime, null))
|
||||
.containsExactly(
|
||||
now.minusMinutes(4),
|
||||
now.minusMinutes(3),
|
||||
now.minusMinutes(2),
|
||||
now.minusMinutes(1),
|
||||
now)
|
||||
.inOrder();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testList_failsOnFork() throws Exception {
|
||||
// We currently have files for now-4m ... now, construct the following sequence:
|
||||
// now-8m <- now-7m <- now-6m now-5m <- now-4m ... now
|
||||
// ^___________________________|
|
||||
addGcsFile(5, 8);
|
||||
for (int i = 6; i < 9; ++i) {
|
||||
addGcsFile(i, i + 1);
|
||||
}
|
||||
|
||||
assertThrows(IllegalStateException.class, () -> listDiffFiles(now.minusMinutes(9), null));
|
||||
assertLogContains(String.format(
|
||||
"Found sequence from %s to %s", now.minusMinutes(9), now));
|
||||
assertLogContains(String.format(
|
||||
"Found sequence from %s to %s", now.minusMinutes(9), now.minusMinutes(6)));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testList_boundaries() {
|
||||
assertThat(listDiffFiles(now.minusMinutes(4), now))
|
||||
.containsExactly(
|
||||
now.minusMinutes(4),
|
||||
now.minusMinutes(3),
|
||||
now.minusMinutes(2),
|
||||
now.minusMinutes(1),
|
||||
now)
|
||||
.inOrder();
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testList_failsOnGaps() throws Exception {
|
||||
// We currently have files for now-4m ... now, construct the following sequence:
|
||||
// now-8m <- now-7m <- now-6m {missing} <- now-4m ... now
|
||||
for (int i = 6; i < 9; ++i) {
|
||||
addGcsFile(i, i + 1);
|
||||
}
|
||||
|
||||
assertThrows(IllegalStateException.class, () -> listDiffFiles(now.minusMinutes(9), null));
|
||||
assertLogContains(String.format(
|
||||
"Gap discovered in sequence terminating at %s, missing file: commit_diff_until_%s",
|
||||
now, now.minusMinutes(5)));
|
||||
assertLogContains(String.format(
|
||||
"Found sequence from %s to %s", now.minusMinutes(9), now.minusMinutes(6)));
|
||||
assertLogContains(String.format(
|
||||
"Found sequence from %s to %s", now.minusMinutes(5), now));
|
||||
|
||||
// Verify that we can work around the gap.
|
||||
DateTime fromTime = now.minusMinutes(4).minusSeconds(1);
|
||||
assertThat(listDiffFiles(fromTime, null))
|
||||
.containsExactly(
|
||||
now.minusMinutes(4),
|
||||
now.minusMinutes(3),
|
||||
now.minusMinutes(2),
|
||||
now.minusMinutes(1),
|
||||
now)
|
||||
.inOrder();
|
||||
assertThat(listDiffFiles(
|
||||
now.minusMinutes(8).minusSeconds(1), now.minusMinutes(6).plusSeconds(1)))
|
||||
.containsExactly(
|
||||
now.minusMinutes(8),
|
||||
now.minusMinutes(7),
|
||||
now.minusMinutes(6))
|
||||
.inOrder();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testList_toTimeSpecified() {
|
||||
assertThat(listDiffFiles(
|
||||
now.minusMinutes(4).minusSeconds(1), now.minusMinutes(2).plusSeconds(1)))
|
||||
.containsExactly(
|
||||
now.minusMinutes(4),
|
||||
now.minusMinutes(3),
|
||||
now.minusMinutes(2))
|
||||
.inOrder();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,300 @@
|
||||
// Copyright 2017 The Nomulus Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package google.registry.backup;
|
||||
|
||||
import static com.google.appengine.tools.cloudstorage.GcsServiceFactory.createGcsService;
|
||||
import static com.google.common.collect.Iterables.transform;
|
||||
import static com.google.common.collect.Maps.toMap;
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
import static com.google.common.util.concurrent.MoreExecutors.newDirectExecutorService;
|
||||
import static google.registry.backup.BackupUtils.GcsMetadataKeys.LOWER_BOUND_CHECKPOINT;
|
||||
import static google.registry.backup.BackupUtils.serializeEntity;
|
||||
import static google.registry.backup.ExportCommitLogDiffAction.DIFF_FILE_PREFIX;
|
||||
import static google.registry.model.ofy.CommitLogBucket.getBucketIds;
|
||||
import static google.registry.model.ofy.CommitLogBucket.getBucketKey;
|
||||
import static google.registry.model.ofy.ObjectifyService.ofy;
|
||||
import static org.joda.time.DateTimeZone.UTC;
|
||||
|
||||
import com.google.appengine.api.datastore.DatastoreServiceFactory;
|
||||
import com.google.appengine.tools.cloudstorage.GcsFileOptions;
|
||||
import com.google.appengine.tools.cloudstorage.GcsFilename;
|
||||
import com.google.appengine.tools.cloudstorage.GcsService;
|
||||
import com.google.common.collect.ImmutableMap;
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
import com.google.common.collect.Lists;
|
||||
import com.google.common.primitives.Longs;
|
||||
import com.googlecode.objectify.Key;
|
||||
import google.registry.model.ImmutableObject;
|
||||
import google.registry.model.ofy.CommitLogBucket;
|
||||
import google.registry.model.ofy.CommitLogCheckpoint;
|
||||
import google.registry.model.ofy.CommitLogCheckpointRoot;
|
||||
import google.registry.model.ofy.CommitLogManifest;
|
||||
import google.registry.model.ofy.CommitLogMutation;
|
||||
import google.registry.testing.AppEngineRule;
|
||||
import google.registry.testing.FakeClock;
|
||||
import google.registry.testing.FakeSleeper;
|
||||
import google.registry.testing.TestObject;
|
||||
import google.registry.util.Retrier;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Map.Entry;
|
||||
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;
|
||||
|
||||
/** Unit tests for {@link RestoreCommitLogsAction}. */
|
||||
@RunWith(JUnit4.class)
|
||||
public class RestoreCommitLogsActionTest {
|
||||
|
||||
static final String GCS_BUCKET = "gcs bucket";
|
||||
|
||||
final DateTime now = DateTime.now(UTC);
|
||||
final RestoreCommitLogsAction action = new RestoreCommitLogsAction();
|
||||
final GcsService gcsService = createGcsService();
|
||||
|
||||
@Rule
|
||||
public final AppEngineRule appEngine = AppEngineRule.builder()
|
||||
.withDatastore()
|
||||
.build();
|
||||
|
||||
@Before
|
||||
public void init() {
|
||||
action.gcsService = gcsService;
|
||||
action.dryRun = false;
|
||||
action.datastoreService = DatastoreServiceFactory.getDatastoreService();
|
||||
action.fromTime = now.minusMillis(1);
|
||||
action.retrier = new Retrier(new FakeSleeper(new FakeClock()), 1);
|
||||
action.diffLister = new GcsDiffFileLister();
|
||||
action.diffLister.gcsService = gcsService;
|
||||
action.diffLister.gcsBucket = GCS_BUCKET;
|
||||
action.diffLister.executor = newDirectExecutorService();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRestore_multipleDiffFiles() throws Exception {
|
||||
ofy().saveWithoutBackup().entities(
|
||||
TestObject.create("previous to keep"),
|
||||
TestObject.create("previous to delete")).now();
|
||||
// Create 3 transactions, across two diff files.
|
||||
// Before: {"previous to keep", "previous to delete"}
|
||||
// 1a: Add {"a", "b"}, Delete {"previous to delete"}
|
||||
// 1b: Add {"c", "d"}, Delete {"a"}
|
||||
// 2: Add {"e", "f"}, Delete {"c"}
|
||||
// After: {"previous to keep", "b", "d", "e", "f"}
|
||||
Key<CommitLogManifest> manifest1aKey =
|
||||
CommitLogManifest.createKey(getBucketKey(1), now.minusMinutes(3));
|
||||
Key<CommitLogManifest> manifest1bKey =
|
||||
CommitLogManifest.createKey(getBucketKey(2), now.minusMinutes(2));
|
||||
Key<CommitLogManifest> manifest2Key =
|
||||
CommitLogManifest.createKey(getBucketKey(1), now.minusMinutes(1));
|
||||
saveDiffFileNotToRestore(now.minusMinutes(2));
|
||||
Iterable<ImmutableObject> file1CommitLogs = saveDiffFile(
|
||||
createCheckpoint(now.minusMinutes(1)),
|
||||
CommitLogManifest.create(
|
||||
getBucketKey(1),
|
||||
now.minusMinutes(3),
|
||||
ImmutableSet.of(Key.create(TestObject.create("previous to delete")))),
|
||||
CommitLogMutation.create(manifest1aKey, TestObject.create("a")),
|
||||
CommitLogMutation.create(manifest1aKey, TestObject.create("b")),
|
||||
CommitLogManifest.create(
|
||||
getBucketKey(2),
|
||||
now.minusMinutes(2),
|
||||
ImmutableSet.of(Key.create(TestObject.create("a")))),
|
||||
CommitLogMutation.create(manifest1bKey, TestObject.create("c")),
|
||||
CommitLogMutation.create(manifest1bKey, TestObject.create("d")));
|
||||
Iterable<ImmutableObject> file2CommitLogs = saveDiffFile(
|
||||
createCheckpoint(now),
|
||||
CommitLogManifest.create(
|
||||
getBucketKey(1),
|
||||
now.minusMinutes(1),
|
||||
ImmutableSet.of(Key.create(TestObject.create("c")))),
|
||||
CommitLogMutation.create(manifest2Key, TestObject.create("e")),
|
||||
CommitLogMutation.create(manifest2Key, TestObject.create("f")));
|
||||
action.fromTime = now.minusMinutes(1).minusMillis(1);
|
||||
action.run();
|
||||
ofy().clearSessionCache();
|
||||
assertExpectedIds("previous to keep", "b", "d", "e", "f");
|
||||
assertInDatastore(file1CommitLogs);
|
||||
assertInDatastore(file2CommitLogs);
|
||||
assertInDatastore(CommitLogCheckpointRoot.create(now));
|
||||
assertCommitLogBuckets(ImmutableMap.of(1, now.minusMinutes(1), 2, now.minusMinutes(2)));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRestore_noManifests() throws Exception {
|
||||
ofy().saveWithoutBackup().entity(
|
||||
TestObject.create("previous to keep")).now();
|
||||
saveDiffFileNotToRestore(now.minusMinutes(1));
|
||||
Iterable<ImmutableObject> commitLogs = saveDiffFile(createCheckpoint(now));
|
||||
action.run();
|
||||
ofy().clearSessionCache();
|
||||
assertExpectedIds("previous to keep");
|
||||
assertInDatastore(commitLogs);
|
||||
assertInDatastore(CommitLogCheckpointRoot.create(now));
|
||||
assertCommitLogBuckets(ImmutableMap.of());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRestore_manifestWithNoDeletions() throws Exception {
|
||||
ofy().saveWithoutBackup().entity(TestObject.create("previous to keep")).now();
|
||||
Key<CommitLogBucket> bucketKey = getBucketKey(1);
|
||||
Key<CommitLogManifest> manifestKey = CommitLogManifest.createKey(bucketKey, now);
|
||||
saveDiffFileNotToRestore(now.minusMinutes(1));
|
||||
Iterable<ImmutableObject> commitLogs = saveDiffFile(
|
||||
createCheckpoint(now),
|
||||
CommitLogManifest.create(bucketKey, now, null),
|
||||
CommitLogMutation.create(manifestKey, TestObject.create("a")),
|
||||
CommitLogMutation.create(manifestKey, TestObject.create("b")));
|
||||
action.run();
|
||||
ofy().clearSessionCache();
|
||||
assertExpectedIds("previous to keep", "a", "b");
|
||||
assertInDatastore(commitLogs);
|
||||
assertInDatastore(CommitLogCheckpointRoot.create(now));
|
||||
assertCommitLogBuckets(ImmutableMap.of(1, now));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRestore_manifestWithNoMutations() throws Exception {
|
||||
ofy().saveWithoutBackup().entities(
|
||||
TestObject.create("previous to keep"),
|
||||
TestObject.create("previous to delete")).now();
|
||||
saveDiffFileNotToRestore(now.minusMinutes(1));
|
||||
Iterable<ImmutableObject> commitLogs = saveDiffFile(
|
||||
createCheckpoint(now),
|
||||
CommitLogManifest.create(
|
||||
getBucketKey(1),
|
||||
now,
|
||||
ImmutableSet.of(Key.create(TestObject.create("previous to delete")))));
|
||||
action.run();
|
||||
ofy().clearSessionCache();
|
||||
assertExpectedIds("previous to keep");
|
||||
assertInDatastore(commitLogs);
|
||||
assertInDatastore(CommitLogCheckpointRoot.create(now));
|
||||
assertCommitLogBuckets(ImmutableMap.of(1, now));
|
||||
}
|
||||
|
||||
// This is a pathological case that shouldn't be possible, but we should be robust to it.
|
||||
@Test
|
||||
public void testRestore_manifestWithNoMutationsOrDeletions() throws Exception {
|
||||
ofy().saveWithoutBackup().entities(
|
||||
TestObject.create("previous to keep")).now();
|
||||
saveDiffFileNotToRestore(now.minusMinutes(1));
|
||||
Iterable<ImmutableObject> commitLogs = saveDiffFile(
|
||||
createCheckpoint(now),
|
||||
CommitLogManifest.create(getBucketKey(1), now, null));
|
||||
action.run();
|
||||
ofy().clearSessionCache();
|
||||
assertExpectedIds("previous to keep");
|
||||
assertInDatastore(commitLogs);
|
||||
assertInDatastore(CommitLogCheckpointRoot.create(now));
|
||||
assertCommitLogBuckets(ImmutableMap.of(1, now));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRestore_mutateExistingEntity() throws Exception {
|
||||
ofy().saveWithoutBackup().entity(TestObject.create("existing", "a")).now();
|
||||
Key<CommitLogManifest> manifestKey = CommitLogManifest.createKey(getBucketKey(1), now);
|
||||
saveDiffFileNotToRestore(now.minusMinutes(1));
|
||||
Iterable<ImmutableObject> commitLogs = saveDiffFile(
|
||||
createCheckpoint(now),
|
||||
CommitLogManifest.create(getBucketKey(1), now, null),
|
||||
CommitLogMutation.create(manifestKey, TestObject.create("existing", "b")));
|
||||
action.run();
|
||||
ofy().clearSessionCache();
|
||||
assertThat(ofy().load().entity(TestObject.create("existing")).now().getField()).isEqualTo("b");
|
||||
assertInDatastore(commitLogs);
|
||||
assertInDatastore(CommitLogCheckpointRoot.create(now));
|
||||
assertCommitLogBuckets(ImmutableMap.of(1, now));
|
||||
}
|
||||
|
||||
// This should be harmless; deletes are idempotent.
|
||||
@Test
|
||||
public void testRestore_deleteMissingEntity() throws Exception {
|
||||
ofy().saveWithoutBackup().entity(TestObject.create("previous to keep", "a")).now();
|
||||
saveDiffFileNotToRestore(now.minusMinutes(1));
|
||||
Iterable<ImmutableObject> commitLogs = saveDiffFile(
|
||||
createCheckpoint(now),
|
||||
CommitLogManifest.create(
|
||||
getBucketKey(1),
|
||||
now,
|
||||
ImmutableSet.of(Key.create(TestObject.create("previous to delete")))));
|
||||
action.run();
|
||||
ofy().clearSessionCache();
|
||||
assertExpectedIds("previous to keep");
|
||||
assertInDatastore(commitLogs);
|
||||
assertCommitLogBuckets(ImmutableMap.of(1, now));
|
||||
assertInDatastore(CommitLogCheckpointRoot.create(now));
|
||||
}
|
||||
|
||||
private CommitLogCheckpoint createCheckpoint(DateTime now) {
|
||||
return CommitLogCheckpoint.create(now, toMap(getBucketIds(), x -> now));
|
||||
}
|
||||
|
||||
private Iterable<ImmutableObject> saveDiffFile(
|
||||
CommitLogCheckpoint checkpoint, ImmutableObject... entities) throws IOException {
|
||||
DateTime now = checkpoint.getCheckpointTime();
|
||||
List<ImmutableObject> allEntities = Lists.asList(checkpoint, entities);
|
||||
ByteArrayOutputStream output = new ByteArrayOutputStream();
|
||||
for (ImmutableObject entity : allEntities) {
|
||||
serializeEntity(entity, output);
|
||||
}
|
||||
gcsService.createOrReplace(
|
||||
new GcsFilename(GCS_BUCKET, DIFF_FILE_PREFIX + now),
|
||||
new GcsFileOptions.Builder()
|
||||
.addUserMetadata(LOWER_BOUND_CHECKPOINT, now.minusMinutes(1).toString())
|
||||
.build(),
|
||||
ByteBuffer.wrap(output.toByteArray()));
|
||||
return allEntities;
|
||||
}
|
||||
|
||||
private void saveDiffFileNotToRestore(DateTime now) throws Exception {
|
||||
saveDiffFile(
|
||||
createCheckpoint(now),
|
||||
CommitLogManifest.create(getBucketKey(1), now, null),
|
||||
CommitLogMutation.create(
|
||||
CommitLogManifest.createKey(getBucketKey(1), now),
|
||||
TestObject.create("should not be restored")));
|
||||
}
|
||||
|
||||
private void assertExpectedIds(String... ids) {
|
||||
assertThat(transform(ofy().load().type(TestObject.class), TestObject::getId))
|
||||
.containsExactly((Object[]) ids);
|
||||
}
|
||||
|
||||
private void assertInDatastore(ImmutableObject entity) {
|
||||
assertThat(ofy().load().entity(entity).now()).isEqualTo(entity);
|
||||
}
|
||||
|
||||
private void assertInDatastore(Iterable<? extends ImmutableObject> entities) {
|
||||
assertThat(ofy().load().entities(entities).values()).containsExactlyElementsIn(entities);
|
||||
}
|
||||
|
||||
private void assertCommitLogBuckets(Map<Integer, DateTime> bucketIdsAndTimestamps) {
|
||||
Map<Long, CommitLogBucket> buckets = ofy().load()
|
||||
.type(CommitLogBucket.class)
|
||||
.ids(Longs.asList(Longs.toArray(CommitLogBucket.getBucketIds())));
|
||||
assertThat(buckets).hasSize(bucketIdsAndTimestamps.size());
|
||||
for (Entry<Integer, DateTime> bucketIdAndTimestamp : bucketIdsAndTimestamps.entrySet()) {
|
||||
assertThat(buckets.get((long) bucketIdAndTimestamp.getKey()).getLastWrittenTime())
|
||||
.isEqualTo(bucketIdAndTimestamp.getValue());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,140 @@
|
||||
// Copyright 2018 The Nomulus Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package google.registry.batch;
|
||||
|
||||
import static com.google.appengine.api.taskqueue.QueueFactory.getQueue;
|
||||
import static google.registry.batch.AsyncTaskEnqueuer.PARAM_REQUESTED_TIME;
|
||||
import static google.registry.batch.AsyncTaskEnqueuer.PARAM_RESAVE_TIMES;
|
||||
import static google.registry.batch.AsyncTaskEnqueuer.PARAM_RESOURCE_KEY;
|
||||
import static google.registry.batch.AsyncTaskEnqueuer.PATH_RESAVE_ENTITY;
|
||||
import static google.registry.batch.AsyncTaskEnqueuer.QUEUE_ASYNC_ACTIONS;
|
||||
import static google.registry.batch.AsyncTaskEnqueuer.QUEUE_ASYNC_DELETE;
|
||||
import static google.registry.batch.AsyncTaskEnqueuer.QUEUE_ASYNC_HOST_RENAME;
|
||||
import static google.registry.testing.DatastoreHelper.persistActiveContact;
|
||||
import static google.registry.testing.TaskQueueHelper.assertNoTasksEnqueued;
|
||||
import static google.registry.testing.TaskQueueHelper.assertTasksEnqueued;
|
||||
import static google.registry.testing.TestLogHandlerUtils.assertLogMessage;
|
||||
import static org.joda.time.Duration.standardDays;
|
||||
import static org.joda.time.Duration.standardHours;
|
||||
import static org.joda.time.Duration.standardSeconds;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import com.google.common.collect.ImmutableSortedSet;
|
||||
import com.google.common.flogger.LoggerConfig;
|
||||
import com.googlecode.objectify.Key;
|
||||
import google.registry.model.contact.ContactResource;
|
||||
import google.registry.testing.AppEngineRule;
|
||||
import google.registry.testing.FakeClock;
|
||||
import google.registry.testing.FakeSleeper;
|
||||
import google.registry.testing.InjectRule;
|
||||
import google.registry.testing.ShardableTestCase;
|
||||
import google.registry.testing.TaskQueueHelper.TaskMatcher;
|
||||
import google.registry.util.AppEngineServiceUtils;
|
||||
import google.registry.util.CapturingLogHandler;
|
||||
import google.registry.util.Retrier;
|
||||
import java.util.logging.Level;
|
||||
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 org.mockito.Mock;
|
||||
import org.mockito.junit.MockitoJUnit;
|
||||
import org.mockito.junit.MockitoRule;
|
||||
|
||||
/** Unit tests for {@link AsyncTaskEnqueuer}. */
|
||||
@RunWith(JUnit4.class)
|
||||
public class AsyncTaskEnqueuerTest extends ShardableTestCase {
|
||||
|
||||
@Rule
|
||||
public final AppEngineRule appEngine =
|
||||
AppEngineRule.builder().withDatastore().withTaskQueue().build();
|
||||
|
||||
@Rule public final InjectRule inject = new InjectRule();
|
||||
|
||||
@Rule public final MockitoRule mocks = MockitoJUnit.rule();
|
||||
|
||||
@Mock private AppEngineServiceUtils appEngineServiceUtils;
|
||||
|
||||
private AsyncTaskEnqueuer asyncTaskEnqueuer;
|
||||
private final CapturingLogHandler logHandler = new CapturingLogHandler();
|
||||
private final FakeClock clock = new FakeClock(DateTime.parse("2015-05-18T12:34:56Z"));
|
||||
|
||||
@Before
|
||||
public void setUp() {
|
||||
LoggerConfig.getConfig(AsyncTaskEnqueuer.class).addHandler(logHandler);
|
||||
when(appEngineServiceUtils.getServiceHostname("backend")).thenReturn("backend.hostname.fake");
|
||||
asyncTaskEnqueuer =
|
||||
new AsyncTaskEnqueuer(
|
||||
getQueue(QUEUE_ASYNC_ACTIONS),
|
||||
getQueue(QUEUE_ASYNC_DELETE),
|
||||
getQueue(QUEUE_ASYNC_HOST_RENAME),
|
||||
standardSeconds(90),
|
||||
appEngineServiceUtils,
|
||||
new Retrier(new FakeSleeper(clock), 1));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test_enqueueAsyncResave_success() {
|
||||
ContactResource contact = persistActiveContact("jd23456");
|
||||
asyncTaskEnqueuer.enqueueAsyncResave(contact, clock.nowUtc(), clock.nowUtc().plusDays(5));
|
||||
assertTasksEnqueued(
|
||||
QUEUE_ASYNC_ACTIONS,
|
||||
new TaskMatcher()
|
||||
.url(PATH_RESAVE_ENTITY)
|
||||
.method("POST")
|
||||
.header("Host", "backend.hostname.fake")
|
||||
.header("content-type", "application/x-www-form-urlencoded")
|
||||
.param(PARAM_RESOURCE_KEY, Key.create(contact).getString())
|
||||
.param(PARAM_REQUESTED_TIME, clock.nowUtc().toString())
|
||||
.etaDelta(
|
||||
standardDays(5).minus(standardSeconds(30)),
|
||||
standardDays(5).plus(standardSeconds(30))));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test_enqueueAsyncResave_multipleResaves() {
|
||||
ContactResource contact = persistActiveContact("jd23456");
|
||||
DateTime now = clock.nowUtc();
|
||||
asyncTaskEnqueuer.enqueueAsyncResave(
|
||||
contact,
|
||||
now,
|
||||
ImmutableSortedSet.of(now.plusHours(24), now.plusHours(50), now.plusHours(75)));
|
||||
assertTasksEnqueued(
|
||||
QUEUE_ASYNC_ACTIONS,
|
||||
new TaskMatcher()
|
||||
.url(PATH_RESAVE_ENTITY)
|
||||
.method("POST")
|
||||
.header("Host", "backend.hostname.fake")
|
||||
.header("content-type", "application/x-www-form-urlencoded")
|
||||
.param(PARAM_RESOURCE_KEY, Key.create(contact).getString())
|
||||
.param(PARAM_REQUESTED_TIME, now.toString())
|
||||
.param(
|
||||
PARAM_RESAVE_TIMES,
|
||||
"2015-05-20T14:34:56.000Z,2015-05-21T15:34:56.000Z")
|
||||
.etaDelta(
|
||||
standardHours(24).minus(standardSeconds(30)),
|
||||
standardHours(24).plus(standardSeconds(30))));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test_enqueueAsyncResave_ignoresTasksTooFarIntoFuture() throws Exception {
|
||||
ContactResource contact = persistActiveContact("jd23456");
|
||||
asyncTaskEnqueuer.enqueueAsyncResave(contact, clock.nowUtc(), clock.nowUtc().plusDays(31));
|
||||
assertNoTasksEnqueued(QUEUE_ASYNC_ACTIONS);
|
||||
assertLogMessage(logHandler, Level.INFO, "Ignoring async re-save");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
// Copyright 2017 The Nomulus Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package google.registry.batch;
|
||||
|
||||
import static com.google.monitoring.metrics.contrib.DistributionMetricSubject.assertThat;
|
||||
import static com.google.monitoring.metrics.contrib.LongMetricSubject.assertThat;
|
||||
import static google.registry.batch.AsyncTaskMetrics.OperationResult.SUCCESS;
|
||||
import static google.registry.batch.AsyncTaskMetrics.OperationType.CONTACT_AND_HOST_DELETE;
|
||||
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
import google.registry.testing.FakeClock;
|
||||
import google.registry.testing.ShardableTestCase;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.junit.runners.JUnit4;
|
||||
|
||||
/** Unit tests for {@link AsyncTaskMetrics}. */
|
||||
@RunWith(JUnit4.class)
|
||||
public class AsyncTaskMetricsTest extends ShardableTestCase {
|
||||
|
||||
private final FakeClock clock = new FakeClock();
|
||||
private final AsyncTaskMetrics asyncTaskMetrics = new AsyncTaskMetrics(clock);
|
||||
|
||||
@Test
|
||||
public void testRecordAsyncFlowResult_calculatesDurationMillisCorrectly() {
|
||||
asyncTaskMetrics.recordAsyncFlowResult(
|
||||
CONTACT_AND_HOST_DELETE,
|
||||
SUCCESS,
|
||||
clock.nowUtc().minusMinutes(10).minusSeconds(5).minusMillis(566));
|
||||
assertThat(AsyncTaskMetrics.asyncFlowOperationCounts)
|
||||
.hasValueForLabels(1, "contactAndHostDelete", "success")
|
||||
.and()
|
||||
.hasNoOtherValues();
|
||||
assertThat(AsyncTaskMetrics.asyncFlowOperationProcessingTime)
|
||||
.hasDataSetForLabels(ImmutableSet.of(605566.0), "contactAndHostDelete", "success")
|
||||
.and()
|
||||
.hasNoOtherValues();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
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 = "batch",
|
||||
srcs = glob(["*.java"]),
|
||||
deps = [
|
||||
"//java/google/registry/batch",
|
||||
"//java/google/registry/config",
|
||||
"//java/google/registry/model",
|
||||
"//java/google/registry/request",
|
||||
"//java/google/registry/util",
|
||||
"//javatests/google/registry/testing",
|
||||
"//javatests/google/registry/testing/mapreduce",
|
||||
"//third_party/objectify:objectify-v4_1",
|
||||
"@com_google_appengine_api_1_0_sdk",
|
||||
"@com_google_appengine_api_stubs",
|
||||
"@com_google_appengine_tools_appengine_gcs_client",
|
||||
"@com_google_appengine_tools_appengine_mapreduce",
|
||||
"@com_google_appengine_tools_appengine_pipeline",
|
||||
"@com_google_code_findbugs_jsr305",
|
||||
"@com_google_dagger",
|
||||
"@com_google_flogger",
|
||||
"@com_google_flogger_system_backend",
|
||||
"@com_google_guava",
|
||||
"@com_google_http_client",
|
||||
"@com_google_monitoring_client_contrib",
|
||||
"@com_google_monitoring_client_metrics",
|
||||
"@com_google_truth",
|
||||
"@com_google_truth_extensions_truth_java8_extension",
|
||||
"@javax_servlet_api",
|
||||
"@joda_time",
|
||||
"@junit",
|
||||
"@org_joda_money",
|
||||
"@org_mockito_core",
|
||||
],
|
||||
)
|
||||
|
||||
GenTestRules(
|
||||
name = "GeneratedTestRules",
|
||||
default_test_size = "large",
|
||||
test_files = glob(["*Test.java"]),
|
||||
deps = [":batch"],
|
||||
)
|
||||
@@ -0,0 +1,958 @@
|
||||
// Copyright 2017 The Nomulus Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package google.registry.batch;
|
||||
|
||||
import static com.google.appengine.api.taskqueue.QueueFactory.getQueue;
|
||||
import static com.google.common.collect.MoreCollectors.onlyElement;
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
import static com.google.common.truth.Truth8.assertThat;
|
||||
import static google.registry.batch.AsyncTaskEnqueuer.QUEUE_ASYNC_ACTIONS;
|
||||
import static google.registry.batch.AsyncTaskEnqueuer.QUEUE_ASYNC_DELETE;
|
||||
import static google.registry.batch.AsyncTaskEnqueuer.QUEUE_ASYNC_HOST_RENAME;
|
||||
import static google.registry.batch.AsyncTaskMetrics.OperationResult.STALE;
|
||||
import static google.registry.model.EppResourceUtils.loadByForeignKey;
|
||||
import static google.registry.model.eppcommon.StatusValue.PENDING_DELETE;
|
||||
import static google.registry.model.ofy.ObjectifyService.ofy;
|
||||
import static google.registry.model.reporting.HistoryEntry.Type.CONTACT_DELETE;
|
||||
import static google.registry.model.reporting.HistoryEntry.Type.CONTACT_DELETE_FAILURE;
|
||||
import static google.registry.model.reporting.HistoryEntry.Type.CONTACT_TRANSFER_REQUEST;
|
||||
import static google.registry.model.reporting.HistoryEntry.Type.HOST_DELETE;
|
||||
import static google.registry.model.reporting.HistoryEntry.Type.HOST_DELETE_FAILURE;
|
||||
import static google.registry.testing.ContactResourceSubject.assertAboutContacts;
|
||||
import static google.registry.testing.DatastoreHelper.assertNoBillingEvents;
|
||||
import static google.registry.testing.DatastoreHelper.createTld;
|
||||
import static google.registry.testing.DatastoreHelper.getOnlyHistoryEntryOfType;
|
||||
import static google.registry.testing.DatastoreHelper.getOnlyPollMessageForHistoryEntry;
|
||||
import static google.registry.testing.DatastoreHelper.getPollMessages;
|
||||
import static google.registry.testing.DatastoreHelper.newContactResource;
|
||||
import static google.registry.testing.DatastoreHelper.newDomainBase;
|
||||
import static google.registry.testing.DatastoreHelper.newHostResource;
|
||||
import static google.registry.testing.DatastoreHelper.persistActiveContact;
|
||||
import static google.registry.testing.DatastoreHelper.persistActiveHost;
|
||||
import static google.registry.testing.DatastoreHelper.persistContactWithPendingTransfer;
|
||||
import static google.registry.testing.DatastoreHelper.persistDeletedContact;
|
||||
import static google.registry.testing.DatastoreHelper.persistDeletedHost;
|
||||
import static google.registry.testing.DatastoreHelper.persistResource;
|
||||
import static google.registry.testing.HostResourceSubject.assertAboutHosts;
|
||||
import static google.registry.testing.TaskQueueHelper.assertDnsTasksEnqueued;
|
||||
import static google.registry.testing.TaskQueueHelper.assertNoTasksEnqueued;
|
||||
import static google.registry.testing.TaskQueueHelper.assertTasksEnqueued;
|
||||
import static google.registry.util.DateTimeUtils.END_OF_TIME;
|
||||
import static org.joda.time.DateTimeZone.UTC;
|
||||
import static org.joda.time.Duration.millis;
|
||||
import static org.joda.time.Duration.standardDays;
|
||||
import static org.joda.time.Duration.standardHours;
|
||||
import static org.joda.time.Duration.standardSeconds;
|
||||
import static org.mockito.ArgumentMatchers.anyString;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.verifyNoMoreInteractions;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import com.google.appengine.api.taskqueue.TaskOptions;
|
||||
import com.google.appengine.api.taskqueue.TaskOptions.Method;
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
import com.google.common.collect.Iterables;
|
||||
import com.googlecode.objectify.Key;
|
||||
import google.registry.batch.AsyncTaskMetrics.OperationResult;
|
||||
import google.registry.batch.AsyncTaskMetrics.OperationType;
|
||||
import google.registry.batch.DeleteContactsAndHostsAction.DeleteEppResourceReducer;
|
||||
import google.registry.model.EppResource;
|
||||
import google.registry.model.contact.ContactAddress;
|
||||
import google.registry.model.contact.ContactPhoneNumber;
|
||||
import google.registry.model.contact.ContactResource;
|
||||
import google.registry.model.contact.PostalInfo;
|
||||
import google.registry.model.domain.DomainBase;
|
||||
import google.registry.model.eppcommon.StatusValue;
|
||||
import google.registry.model.eppcommon.Trid;
|
||||
import google.registry.model.eppoutput.EppResponse.ResponseData;
|
||||
import google.registry.model.host.HostResource;
|
||||
import google.registry.model.ofy.Ofy;
|
||||
import google.registry.model.poll.PendingActionNotificationResponse;
|
||||
import google.registry.model.poll.PendingActionNotificationResponse.ContactPendingActionNotificationResponse;
|
||||
import google.registry.model.poll.PendingActionNotificationResponse.HostPendingActionNotificationResponse;
|
||||
import google.registry.model.poll.PollMessage;
|
||||
import google.registry.model.poll.PollMessage.OneTime;
|
||||
import google.registry.model.registry.Registry;
|
||||
import google.registry.model.reporting.HistoryEntry;
|
||||
import google.registry.model.server.Lock;
|
||||
import google.registry.model.transfer.TransferData;
|
||||
import google.registry.model.transfer.TransferResponse;
|
||||
import google.registry.model.transfer.TransferStatus;
|
||||
import google.registry.testing.FakeClock;
|
||||
import google.registry.testing.FakeResponse;
|
||||
import google.registry.testing.FakeSleeper;
|
||||
import google.registry.testing.InjectRule;
|
||||
import google.registry.testing.TaskQueueHelper.TaskMatcher;
|
||||
import google.registry.testing.mapreduce.MapreduceTestCase;
|
||||
import google.registry.util.AppEngineServiceUtils;
|
||||
import google.registry.util.RequestStatusChecker;
|
||||
import google.registry.util.Retrier;
|
||||
import google.registry.util.Sleeper;
|
||||
import google.registry.util.SystemSleeper;
|
||||
import java.util.Optional;
|
||||
import org.joda.time.DateTime;
|
||||
import org.joda.time.Duration;
|
||||
import org.junit.Before;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.junit.runners.JUnit4;
|
||||
import org.mockito.Mock;
|
||||
|
||||
/** Unit tests for {@link DeleteContactsAndHostsAction}. */
|
||||
@RunWith(JUnit4.class)
|
||||
public class DeleteContactsAndHostsActionTest
|
||||
extends MapreduceTestCase<DeleteContactsAndHostsAction> {
|
||||
|
||||
@Rule public final InjectRule inject = new InjectRule();
|
||||
|
||||
private AsyncTaskEnqueuer enqueuer;
|
||||
private final FakeClock clock = new FakeClock(DateTime.parse("2015-01-15T11:22:33Z"));
|
||||
private final FakeResponse fakeResponse = new FakeResponse();
|
||||
@Mock private RequestStatusChecker requestStatusChecker;
|
||||
|
||||
private void runMapreduce() throws Exception {
|
||||
clock.advanceBy(standardSeconds(5));
|
||||
// Apologies for the hard sleeps. Without them, the tests can be flaky because the tasks aren't
|
||||
// quite fully enqueued by the time the tests attempt to lease from the queue.
|
||||
Sleeper sleeper = new SystemSleeper();
|
||||
sleeper.sleep(millis(50));
|
||||
action.run();
|
||||
sleeper.sleep(millis(50));
|
||||
executeTasksUntilEmpty("mapreduce", clock);
|
||||
sleeper.sleep(millis(50));
|
||||
clock.advanceBy(standardSeconds(5));
|
||||
ofy().clearSessionCache();
|
||||
}
|
||||
|
||||
/** Kicks off, but does not run, the mapreduce tasks. Useful for testing validation/setup. */
|
||||
private void enqueueMapreduceOnly() {
|
||||
clock.advanceBy(standardSeconds(5));
|
||||
action.run();
|
||||
clock.advanceBy(standardSeconds(5));
|
||||
ofy().clearSessionCache();
|
||||
}
|
||||
|
||||
@Before
|
||||
public void setup() {
|
||||
inject.setStaticField(Ofy.class, "clock", clock);
|
||||
enqueuer =
|
||||
new AsyncTaskEnqueuer(
|
||||
getQueue(QUEUE_ASYNC_ACTIONS),
|
||||
getQueue(QUEUE_ASYNC_DELETE),
|
||||
getQueue(QUEUE_ASYNC_HOST_RENAME),
|
||||
Duration.ZERO,
|
||||
mock(AppEngineServiceUtils.class),
|
||||
new Retrier(new FakeSleeper(clock), 1));
|
||||
AsyncTaskMetrics asyncTaskMetricsMock = mock(AsyncTaskMetrics.class);
|
||||
action = new DeleteContactsAndHostsAction();
|
||||
action.asyncTaskMetrics = asyncTaskMetricsMock;
|
||||
inject.setStaticField(DeleteEppResourceReducer.class, "asyncTaskMetrics", asyncTaskMetricsMock);
|
||||
action.clock = clock;
|
||||
action.mrRunner = makeDefaultRunner();
|
||||
action.requestStatusChecker = requestStatusChecker;
|
||||
action.response = fakeResponse;
|
||||
action.retrier = new Retrier(new FakeSleeper(clock), 1);
|
||||
action.queue = getQueue(QUEUE_ASYNC_DELETE);
|
||||
when(requestStatusChecker.getLogId()).thenReturn("requestId");
|
||||
when(requestStatusChecker.isRunning(anyString()))
|
||||
.thenThrow(new AssertionError("Should not be called"));
|
||||
|
||||
createTld("tld");
|
||||
clock.advanceOneMilli();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSuccess_contact_referencedByActiveDomain_doesNotGetDeleted() throws Exception {
|
||||
ContactResource contact = persistContactPendingDelete("blah8221");
|
||||
persistResource(newDomainBase("example.tld", contact));
|
||||
DateTime timeEnqueued = clock.nowUtc();
|
||||
enqueuer.enqueueAsyncDelete(
|
||||
contact,
|
||||
timeEnqueued,
|
||||
"TheRegistrar",
|
||||
Trid.create("fakeClientTrid", "fakeServerTrid"),
|
||||
false);
|
||||
runMapreduce();
|
||||
ContactResource contactUpdated =
|
||||
loadByForeignKey(ContactResource.class, "blah8221", clock.nowUtc()).get();
|
||||
assertAboutContacts()
|
||||
.that(contactUpdated)
|
||||
.doesNotHaveStatusValue(PENDING_DELETE)
|
||||
.and()
|
||||
.hasDeletionTime(END_OF_TIME);
|
||||
DomainBase domainReloaded =
|
||||
loadByForeignKey(DomainBase.class, "example.tld", clock.nowUtc()).get();
|
||||
assertThat(domainReloaded.getReferencedContacts()).contains(Key.create(contactUpdated));
|
||||
HistoryEntry historyEntry =
|
||||
getOnlyHistoryEntryOfType(contactUpdated, HistoryEntry.Type.CONTACT_DELETE_FAILURE);
|
||||
assertPollMessageFor(
|
||||
historyEntry,
|
||||
"TheRegistrar",
|
||||
"Can't delete contact blah8221 because it is referenced by a domain.",
|
||||
false,
|
||||
contact,
|
||||
Optional.of("fakeClientTrid"));
|
||||
assertNoTasksEnqueued(QUEUE_ASYNC_DELETE);
|
||||
verify(action.asyncTaskMetrics).recordContactHostDeletionBatchSize(1L);
|
||||
verify(action.asyncTaskMetrics)
|
||||
.recordAsyncFlowResult(OperationType.CONTACT_DELETE, OperationResult.FAILURE, timeEnqueued);
|
||||
verifyNoMoreInteractions(action.asyncTaskMetrics);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSuccess_contact_notReferenced_getsDeleted_andPiiWipedOut() throws Exception {
|
||||
runSuccessfulContactDeletionTest(Optional.of("fakeClientTrid"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSuccess_contact_andNoClientTrid_deletesSuccessfully() throws Exception {
|
||||
runSuccessfulContactDeletionTest(Optional.empty());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test_cannotAcquireLock() {
|
||||
// Make lock acquisition fail.
|
||||
acquireLock();
|
||||
enqueueMapreduceOnly();
|
||||
assertThat(fakeResponse.getPayload()).isEqualTo("Can't acquire lock; aborting.");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test_mapreduceHasWorkToDo_lockIsAcquired() {
|
||||
ContactResource contact = persistContactPendingDelete("blah8221");
|
||||
persistResource(newDomainBase("example.tld", contact));
|
||||
DateTime timeEnqueued = clock.nowUtc();
|
||||
enqueuer.enqueueAsyncDelete(
|
||||
contact,
|
||||
timeEnqueued,
|
||||
"TheRegistrar",
|
||||
Trid.create("fakeClientTrid", "fakeServerTrid"),
|
||||
false);
|
||||
enqueueMapreduceOnly();
|
||||
assertThat(acquireLock()).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test_noTasksToLease_releasesLockImmediately() {
|
||||
enqueueMapreduceOnly();
|
||||
// If the Lock was correctly released, then we can acquire it now.
|
||||
assertThat(acquireLock()).isPresent();
|
||||
}
|
||||
|
||||
private void runSuccessfulContactDeletionTest(Optional<String> clientTrid) throws Exception {
|
||||
ContactResource contact = persistContactWithPii("jim919");
|
||||
DateTime timeEnqueued = clock.nowUtc();
|
||||
enqueuer.enqueueAsyncDelete(
|
||||
contact,
|
||||
timeEnqueued,
|
||||
"TheRegistrar",
|
||||
Trid.create(clientTrid.orElse(null), "fakeServerTrid"),
|
||||
false);
|
||||
runMapreduce();
|
||||
assertThat(loadByForeignKey(ContactResource.class, "jim919", clock.nowUtc())).isEmpty();
|
||||
ContactResource contactAfterDeletion = ofy().load().entity(contact).now();
|
||||
assertAboutContacts()
|
||||
.that(contactAfterDeletion)
|
||||
.isNotActiveAt(clock.nowUtc())
|
||||
// Note that there will be another history entry of CONTACT_PENDING_DELETE, but this is
|
||||
// added by the flow and not the mapreduce itself.
|
||||
.and()
|
||||
.hasOnlyOneHistoryEntryWhich()
|
||||
.hasType(CONTACT_DELETE);
|
||||
assertAboutContacts()
|
||||
.that(contactAfterDeletion)
|
||||
.hasNullLocalizedPostalInfo()
|
||||
.and()
|
||||
.hasNullInternationalizedPostalInfo()
|
||||
.and()
|
||||
.hasNullEmailAddress()
|
||||
.and()
|
||||
.hasNullVoiceNumber()
|
||||
.and()
|
||||
.hasNullFaxNumber();
|
||||
HistoryEntry historyEntry = getOnlyHistoryEntryOfType(contactAfterDeletion, CONTACT_DELETE);
|
||||
assertPollMessageFor(
|
||||
historyEntry, "TheRegistrar", "Deleted contact jim919.", true, contact, clientTrid);
|
||||
assertNoTasksEnqueued(QUEUE_ASYNC_DELETE);
|
||||
verify(action.asyncTaskMetrics).recordContactHostDeletionBatchSize(1L);
|
||||
verify(action.asyncTaskMetrics)
|
||||
.recordAsyncFlowResult(OperationType.CONTACT_DELETE, OperationResult.SUCCESS, timeEnqueued);
|
||||
verifyNoMoreInteractions(action.asyncTaskMetrics);
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSuccess_contactWithoutPendingTransfer_isDeletedAndHasNoTransferData()
|
||||
throws Exception {
|
||||
ContactResource contact = persistContactPendingDelete("blah8221");
|
||||
enqueuer.enqueueAsyncDelete(
|
||||
contact,
|
||||
clock.nowUtc(),
|
||||
"TheRegistrar",
|
||||
Trid.create("fakeClientTrid", "fakeServerTrid"),
|
||||
false);
|
||||
runMapreduce();
|
||||
ContactResource contactAfterDeletion = ofy().load().entity(contact).now();
|
||||
assertThat(contactAfterDeletion.getTransferData()).isEqualTo(TransferData.EMPTY);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSuccess_contactWithPendingTransfer_getsDeleted() throws Exception {
|
||||
DateTime transferRequestTime = clock.nowUtc().minusDays(3);
|
||||
ContactResource contact =
|
||||
persistContactWithPendingTransfer(
|
||||
newContactResource("sh8013").asBuilder().addStatusValue(PENDING_DELETE).build(),
|
||||
transferRequestTime,
|
||||
transferRequestTime.plus(Registry.DEFAULT_TRANSFER_GRACE_PERIOD),
|
||||
clock.nowUtc());
|
||||
enqueuer.enqueueAsyncDelete(
|
||||
contact,
|
||||
clock.nowUtc(),
|
||||
"TheRegistrar",
|
||||
Trid.create("fakeClientTrid", "fakeServerTrid"),
|
||||
false);
|
||||
runMapreduce();
|
||||
// Check that the contact is deleted as of now.
|
||||
assertThat(loadByForeignKey(ContactResource.class, "sh8013", clock.nowUtc())).isEmpty();
|
||||
// Check that it's still there (it wasn't deleted yesterday) and that it has history.
|
||||
ContactResource softDeletedContact =
|
||||
loadByForeignKey(ContactResource.class, "sh8013", clock.nowUtc().minusDays(1)).get();
|
||||
assertAboutContacts()
|
||||
.that(softDeletedContact)
|
||||
.hasOneHistoryEntryEachOfTypes(CONTACT_TRANSFER_REQUEST, CONTACT_DELETE);
|
||||
// Check that the transfer data reflects the cancelled transfer as we expect.
|
||||
TransferData oldTransferData = contact.getTransferData();
|
||||
assertThat(softDeletedContact.getTransferData())
|
||||
.isEqualTo(
|
||||
oldTransferData.copyConstantFieldsToBuilder()
|
||||
.setTransferStatus(TransferStatus.SERVER_CANCELLED)
|
||||
.setPendingTransferExpirationTime(softDeletedContact.getDeletionTime())
|
||||
.build());
|
||||
assertNoBillingEvents();
|
||||
PollMessage deletePollMessage =
|
||||
Iterables.getOnlyElement(getPollMessages("TheRegistrar", clock.nowUtc().plusMonths(1)));
|
||||
assertThat(deletePollMessage.getMsg()).isEqualTo("Deleted contact sh8013.");
|
||||
// The poll message in the future to the gaining registrar should be gone too, but there
|
||||
// should be one at the current time to the gaining registrar.
|
||||
PollMessage gainingPollMessage =
|
||||
Iterables.getOnlyElement(getPollMessages("NewRegistrar", clock.nowUtc()));
|
||||
assertThat(gainingPollMessage.getEventTime()).isLessThan(clock.nowUtc());
|
||||
assertThat(
|
||||
gainingPollMessage
|
||||
.getResponseData()
|
||||
.stream()
|
||||
.filter(TransferResponse.class::isInstance)
|
||||
.map(TransferResponse.class::cast)
|
||||
.collect(onlyElement())
|
||||
.getTransferStatus())
|
||||
.isEqualTo(TransferStatus.SERVER_CANCELLED);
|
||||
PendingActionNotificationResponse panData =
|
||||
gainingPollMessage
|
||||
.getResponseData()
|
||||
.stream()
|
||||
.filter(PendingActionNotificationResponse.class::isInstance)
|
||||
.map(PendingActionNotificationResponse.class::cast)
|
||||
.collect(onlyElement());
|
||||
assertThat(panData.getTrid())
|
||||
.isEqualTo(Trid.create("transferClient-trid", "transferServer-trid"));
|
||||
assertThat(panData.getActionResult()).isFalse();
|
||||
assertNoTasksEnqueued(QUEUE_ASYNC_DELETE);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSuccess_contact_referencedByDeletedDomain_getsDeleted() throws Exception {
|
||||
ContactResource contactUsed = persistContactPendingDelete("blah1234");
|
||||
persistResource(
|
||||
newDomainBase("example.tld", contactUsed)
|
||||
.asBuilder()
|
||||
.setDeletionTime(clock.nowUtc().minusDays(3))
|
||||
.build());
|
||||
enqueuer.enqueueAsyncDelete(
|
||||
contactUsed,
|
||||
clock.nowUtc(),
|
||||
"TheRegistrar",
|
||||
Trid.create("fakeClientTrid", "fakeServerTrid"),
|
||||
false);
|
||||
runMapreduce();
|
||||
assertThat(loadByForeignKey(ContactResource.class, "blah1234", clock.nowUtc())).isEmpty();
|
||||
ContactResource contactBeforeDeletion =
|
||||
loadByForeignKey(ContactResource.class, "blah1234", clock.nowUtc().minusDays(1)).get();
|
||||
assertAboutContacts()
|
||||
.that(contactBeforeDeletion)
|
||||
.isNotActiveAt(clock.nowUtc())
|
||||
.and()
|
||||
.hasExactlyStatusValues(StatusValue.OK)
|
||||
// Note that there will be another history entry of CONTACT_PENDING_DELETE, but this is
|
||||
// added by the flow and not the mapreduce itself.
|
||||
.and()
|
||||
.hasOnlyOneHistoryEntryWhich()
|
||||
.hasType(CONTACT_DELETE);
|
||||
HistoryEntry historyEntry = getOnlyHistoryEntryOfType(contactBeforeDeletion, CONTACT_DELETE);
|
||||
assertPollMessageFor(
|
||||
historyEntry,
|
||||
"TheRegistrar",
|
||||
"Deleted contact blah1234.",
|
||||
true,
|
||||
contactUsed,
|
||||
Optional.of("fakeClientTrid"));
|
||||
assertNoTasksEnqueued(QUEUE_ASYNC_DELETE);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSuccess_contact_notRequestedByOwner_doesNotGetDeleted() throws Exception {
|
||||
ContactResource contact = persistContactPendingDelete("jane0991");
|
||||
enqueuer.enqueueAsyncDelete(
|
||||
contact,
|
||||
clock.nowUtc(),
|
||||
"OtherRegistrar",
|
||||
Trid.create("fakeClientTrid", "fakeServerTrid"),
|
||||
false);
|
||||
runMapreduce();
|
||||
ContactResource contactAfter =
|
||||
loadByForeignKey(ContactResource.class, "jane0991", clock.nowUtc()).get();
|
||||
assertAboutContacts()
|
||||
.that(contactAfter)
|
||||
.doesNotHaveStatusValue(PENDING_DELETE)
|
||||
.and()
|
||||
.hasDeletionTime(END_OF_TIME);
|
||||
HistoryEntry historyEntry = getOnlyHistoryEntryOfType(contactAfter, CONTACT_DELETE_FAILURE);
|
||||
assertPollMessageFor(
|
||||
historyEntry,
|
||||
"OtherRegistrar",
|
||||
"Can't delete contact jane0991 because it was transferred prior to deletion.",
|
||||
false,
|
||||
contact,
|
||||
Optional.of("fakeClientTrid"));
|
||||
assertNoTasksEnqueued(QUEUE_ASYNC_DELETE);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSuccess_contact_notRequestedByOwner_isSuperuser_getsDeleted() throws Exception {
|
||||
ContactResource contact = persistContactWithPii("nate007");
|
||||
enqueuer.enqueueAsyncDelete(
|
||||
contact,
|
||||
clock.nowUtc(),
|
||||
"OtherRegistrar",
|
||||
Trid.create("fakeClientTrid", "fakeServerTrid"),
|
||||
true);
|
||||
runMapreduce();
|
||||
assertThat(loadByForeignKey(ContactResource.class, "nate007", clock.nowUtc())).isEmpty();
|
||||
ContactResource contactAfterDeletion = ofy().load().entity(contact).now();
|
||||
assertAboutContacts()
|
||||
.that(contactAfterDeletion)
|
||||
.isNotActiveAt(clock.nowUtc())
|
||||
// Note that there will be another history entry of CONTACT_PENDING_DELETE, but this is
|
||||
// added by the flow and not the mapreduce itself.
|
||||
.and()
|
||||
.hasOnlyOneHistoryEntryWhich()
|
||||
.hasType(CONTACT_DELETE);
|
||||
assertAboutContacts()
|
||||
.that(contactAfterDeletion)
|
||||
.hasNullLocalizedPostalInfo()
|
||||
.and()
|
||||
.hasNullInternationalizedPostalInfo()
|
||||
.and()
|
||||
.hasNullEmailAddress()
|
||||
.and()
|
||||
.hasNullVoiceNumber()
|
||||
.and()
|
||||
.hasNullFaxNumber();
|
||||
HistoryEntry historyEntry = getOnlyHistoryEntryOfType(contactAfterDeletion, CONTACT_DELETE);
|
||||
assertPollMessageFor(
|
||||
historyEntry,
|
||||
"OtherRegistrar",
|
||||
"Deleted contact nate007.",
|
||||
true,
|
||||
contact,
|
||||
Optional.of("fakeClientTrid"));
|
||||
assertNoTasksEnqueued(QUEUE_ASYNC_DELETE);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSuccess_targetResourcesDontExist_areDelayedForADay() throws Exception {
|
||||
ContactResource contactNotSaved = newContactResource("somecontact");
|
||||
HostResource hostNotSaved = newHostResource("a11.blah.foo");
|
||||
DateTime timeBeforeRun = clock.nowUtc();
|
||||
enqueuer.enqueueAsyncDelete(
|
||||
contactNotSaved,
|
||||
timeBeforeRun,
|
||||
"TheRegistrar",
|
||||
Trid.create("fakeClientTrid", "fakeServerTrid"),
|
||||
false);
|
||||
enqueuer.enqueueAsyncDelete(
|
||||
hostNotSaved,
|
||||
timeBeforeRun,
|
||||
"TheRegistrar",
|
||||
Trid.create("fakeClientTrid", "fakeServerTrid"),
|
||||
false);
|
||||
enqueueMapreduceOnly();
|
||||
assertTasksEnqueued(
|
||||
QUEUE_ASYNC_DELETE,
|
||||
new TaskMatcher()
|
||||
.etaDelta(standardHours(23), standardHours(25))
|
||||
.param("resourceKey", Key.create(contactNotSaved).getString())
|
||||
.param("requestingClientId", "TheRegistrar")
|
||||
.param("clientTransactionId", "fakeClientTrid")
|
||||
.param("serverTransactionId", "fakeServerTrid")
|
||||
.param("isSuperuser", "false")
|
||||
.param("requestedTime", timeBeforeRun.toString()),
|
||||
new TaskMatcher()
|
||||
.etaDelta(standardHours(23), standardHours(25))
|
||||
.param("resourceKey", Key.create(hostNotSaved).getString())
|
||||
.param("requestingClientId", "TheRegistrar")
|
||||
.param("clientTransactionId", "fakeClientTrid")
|
||||
.param("serverTransactionId", "fakeServerTrid")
|
||||
.param("isSuperuser", "false")
|
||||
.param("requestedTime", timeBeforeRun.toString()));
|
||||
assertThat(acquireLock()).isPresent();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSuccess_unparseableTasks_areDelayedForADay() throws Exception {
|
||||
TaskOptions task =
|
||||
TaskOptions.Builder.withMethod(Method.PULL).param("gobbledygook", "kljhadfgsd9f7gsdfh");
|
||||
getQueue(QUEUE_ASYNC_DELETE).add(task);
|
||||
enqueueMapreduceOnly();
|
||||
assertTasksEnqueued(
|
||||
QUEUE_ASYNC_DELETE,
|
||||
new TaskMatcher()
|
||||
.payload("gobbledygook=kljhadfgsd9f7gsdfh")
|
||||
.etaDelta(standardHours(23), standardHours(25)));
|
||||
verify(action.asyncTaskMetrics).recordContactHostDeletionBatchSize(1L);
|
||||
verifyNoMoreInteractions(action.asyncTaskMetrics);
|
||||
assertThat(acquireLock()).isPresent();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSuccess_resourcesNotInPendingDelete_areSkipped() throws Exception {
|
||||
ContactResource contact = persistActiveContact("blah2222");
|
||||
HostResource host = persistActiveHost("rustles.your.jimmies");
|
||||
DateTime timeEnqueued = clock.nowUtc();
|
||||
enqueuer.enqueueAsyncDelete(
|
||||
contact,
|
||||
timeEnqueued,
|
||||
"TheRegistrar",
|
||||
Trid.create("fakeClientTrid", "fakeServerTrid"),
|
||||
false);
|
||||
enqueuer.enqueueAsyncDelete(
|
||||
host,
|
||||
timeEnqueued,
|
||||
"TheRegistrar",
|
||||
Trid.create("fakeClientTrid", "fakeServerTrid"),
|
||||
false);
|
||||
enqueueMapreduceOnly();
|
||||
assertThat(loadByForeignKey(ContactResource.class, "blah2222", clock.nowUtc()))
|
||||
.hasValue(contact);
|
||||
assertThat(loadByForeignKey(HostResource.class, "rustles.your.jimmies", clock.nowUtc()))
|
||||
.hasValue(host);
|
||||
assertNoTasksEnqueued(QUEUE_ASYNC_DELETE);
|
||||
verify(action.asyncTaskMetrics).recordContactHostDeletionBatchSize(2L);
|
||||
verify(action.asyncTaskMetrics)
|
||||
.recordAsyncFlowResult(OperationType.CONTACT_DELETE, STALE, timeEnqueued);
|
||||
verify(action.asyncTaskMetrics)
|
||||
.recordAsyncFlowResult(OperationType.HOST_DELETE, STALE, timeEnqueued);
|
||||
verifyNoMoreInteractions(action.asyncTaskMetrics);
|
||||
assertThat(acquireLock()).isPresent();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSuccess_alreadyDeletedResources_areSkipped() throws Exception {
|
||||
ContactResource contactDeleted = persistDeletedContact("blah1236", clock.nowUtc().minusDays(2));
|
||||
HostResource hostDeleted = persistDeletedHost("a.lim.lop", clock.nowUtc().minusDays(3));
|
||||
enqueuer.enqueueAsyncDelete(
|
||||
contactDeleted,
|
||||
clock.nowUtc(),
|
||||
"TheRegistrar",
|
||||
Trid.create("fakeClientTrid", "fakeServerTrid"),
|
||||
false);
|
||||
enqueuer.enqueueAsyncDelete(
|
||||
hostDeleted,
|
||||
clock.nowUtc(),
|
||||
"TheRegistrar",
|
||||
Trid.create("fakeClientTrid", "fakeServerTrid"),
|
||||
false);
|
||||
enqueueMapreduceOnly();
|
||||
assertThat(ofy().load().entity(contactDeleted).now()).isEqualTo(contactDeleted);
|
||||
assertThat(ofy().load().entity(hostDeleted).now()).isEqualTo(hostDeleted);
|
||||
assertNoTasksEnqueued(QUEUE_ASYNC_DELETE);
|
||||
assertThat(acquireLock()).isPresent();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSuccess_host_referencedByActiveDomain_doesNotGetDeleted() throws Exception {
|
||||
HostResource host = persistHostPendingDelete("ns1.example.tld");
|
||||
persistUsedDomain("example.tld", persistActiveContact("abc456"), host);
|
||||
DateTime timeEnqueued = clock.nowUtc();
|
||||
enqueuer.enqueueAsyncDelete(
|
||||
host,
|
||||
timeEnqueued,
|
||||
"TheRegistrar",
|
||||
Trid.create("fakeClientTrid", "fakeServerTrid"),
|
||||
false);
|
||||
runMapreduce();
|
||||
HostResource hostAfter =
|
||||
loadByForeignKey(HostResource.class, "ns1.example.tld", clock.nowUtc()).get();
|
||||
assertAboutHosts()
|
||||
.that(hostAfter)
|
||||
.doesNotHaveStatusValue(PENDING_DELETE)
|
||||
.and()
|
||||
.hasDeletionTime(END_OF_TIME);
|
||||
DomainBase domain =
|
||||
loadByForeignKey(DomainBase.class, "example.tld", clock.nowUtc()).get();
|
||||
assertThat(domain.getNameservers()).contains(Key.create(hostAfter));
|
||||
HistoryEntry historyEntry = getOnlyHistoryEntryOfType(hostAfter, HOST_DELETE_FAILURE);
|
||||
assertPollMessageFor(
|
||||
historyEntry,
|
||||
"TheRegistrar",
|
||||
"Can't delete host ns1.example.tld because it is referenced by a domain.",
|
||||
false,
|
||||
host,
|
||||
Optional.of("fakeClientTrid"));
|
||||
assertNoTasksEnqueued(QUEUE_ASYNC_DELETE);
|
||||
verify(action.asyncTaskMetrics).recordContactHostDeletionBatchSize(1L);
|
||||
verify(action.asyncTaskMetrics)
|
||||
.recordAsyncFlowResult(OperationType.HOST_DELETE, OperationResult.FAILURE, timeEnqueued);
|
||||
verifyNoMoreInteractions(action.asyncTaskMetrics);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSuccess_host_notReferenced_getsDeleted() throws Exception {
|
||||
runSuccessfulHostDeletionTest(Optional.of("fakeClientTrid"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSuccess_host_andNoClientTrid_deletesSuccessfully() throws Exception {
|
||||
runSuccessfulHostDeletionTest(Optional.empty());
|
||||
}
|
||||
|
||||
private void runSuccessfulHostDeletionTest(Optional<String> clientTrid) throws Exception {
|
||||
HostResource host = persistHostPendingDelete("ns2.example.tld");
|
||||
DateTime timeEnqueued = clock.nowUtc();
|
||||
enqueuer.enqueueAsyncDelete(
|
||||
host,
|
||||
timeEnqueued,
|
||||
"TheRegistrar",
|
||||
Trid.create(clientTrid.orElse(null), "fakeServerTrid"),
|
||||
false);
|
||||
runMapreduce();
|
||||
assertThat(loadByForeignKey(HostResource.class, "ns2.example.tld", clock.nowUtc())).isEmpty();
|
||||
HostResource hostBeforeDeletion =
|
||||
loadByForeignKey(HostResource.class, "ns2.example.tld", clock.nowUtc().minusDays(1)).get();
|
||||
assertAboutHosts()
|
||||
.that(hostBeforeDeletion)
|
||||
.isNotActiveAt(clock.nowUtc())
|
||||
.and()
|
||||
.hasExactlyStatusValues(StatusValue.OK)
|
||||
// Note that there will be another history entry of HOST_PENDING_DELETE, but this is
|
||||
// added by the flow and not the mapreduce itself.
|
||||
.and()
|
||||
.hasOnlyOneHistoryEntryWhich()
|
||||
.hasType(HOST_DELETE);
|
||||
HistoryEntry historyEntry = getOnlyHistoryEntryOfType(hostBeforeDeletion, HOST_DELETE);
|
||||
assertPollMessageFor(
|
||||
historyEntry,
|
||||
"TheRegistrar",
|
||||
"Deleted host ns2.example.tld.",
|
||||
true,
|
||||
host,
|
||||
clientTrid);
|
||||
assertNoTasksEnqueued(QUEUE_ASYNC_DELETE);
|
||||
verify(action.asyncTaskMetrics).recordContactHostDeletionBatchSize(1L);
|
||||
verify(action.asyncTaskMetrics)
|
||||
.recordAsyncFlowResult(OperationType.HOST_DELETE, OperationResult.SUCCESS, timeEnqueued);
|
||||
verifyNoMoreInteractions(action.asyncTaskMetrics);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSuccess_host_referencedByDeletedDomain_getsDeleted() throws Exception {
|
||||
HostResource host = persistHostPendingDelete("ns1.example.tld");
|
||||
persistResource(
|
||||
newDomainBase("example.tld")
|
||||
.asBuilder()
|
||||
.setNameservers(ImmutableSet.of(Key.create(host)))
|
||||
.setDeletionTime(clock.nowUtc().minusDays(5))
|
||||
.build());
|
||||
enqueuer.enqueueAsyncDelete(
|
||||
host,
|
||||
clock.nowUtc(),
|
||||
"TheRegistrar",
|
||||
Trid.create("fakeClientTrid", "fakeServerTrid"),
|
||||
false);
|
||||
runMapreduce();
|
||||
assertThat(loadByForeignKey(HostResource.class, "ns1.example.tld", clock.nowUtc())).isEmpty();
|
||||
HostResource hostBeforeDeletion =
|
||||
loadByForeignKey(HostResource.class, "ns1.example.tld", clock.nowUtc().minusDays(1)).get();
|
||||
assertAboutHosts()
|
||||
.that(hostBeforeDeletion)
|
||||
.isNotActiveAt(clock.nowUtc())
|
||||
.and()
|
||||
.hasExactlyStatusValues(StatusValue.OK)
|
||||
// Note that there will be another history entry of HOST_PENDING_DELETE, but this is
|
||||
// added by the flow and not the mapreduce itself.
|
||||
.and()
|
||||
.hasOnlyOneHistoryEntryWhich()
|
||||
.hasType(HOST_DELETE);
|
||||
HistoryEntry historyEntry = getOnlyHistoryEntryOfType(hostBeforeDeletion, HOST_DELETE);
|
||||
assertPollMessageFor(
|
||||
historyEntry,
|
||||
"TheRegistrar",
|
||||
"Deleted host ns1.example.tld.",
|
||||
true,
|
||||
host,
|
||||
Optional.of("fakeClientTrid"));
|
||||
assertNoTasksEnqueued(QUEUE_ASYNC_DELETE);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSuccess_subordinateHost_getsDeleted() throws Exception {
|
||||
DomainBase domain =
|
||||
persistResource(
|
||||
newDomainBase("example.tld")
|
||||
.asBuilder()
|
||||
.setSubordinateHosts(ImmutableSet.of("ns2.example.tld"))
|
||||
.build());
|
||||
HostResource host =
|
||||
persistResource(
|
||||
persistHostPendingDelete("ns2.example.tld")
|
||||
.asBuilder()
|
||||
.setSuperordinateDomain(Key.create(domain))
|
||||
.build());
|
||||
enqueuer.enqueueAsyncDelete(
|
||||
host,
|
||||
clock.nowUtc(),
|
||||
"TheRegistrar",
|
||||
Trid.create("fakeClientTrid", "fakeServerTrid"),
|
||||
false);
|
||||
runMapreduce();
|
||||
// Check that the host is deleted as of now.
|
||||
assertThat(loadByForeignKey(HostResource.class, "ns2.example.tld", clock.nowUtc())).isEmpty();
|
||||
assertNoBillingEvents();
|
||||
assertThat(
|
||||
loadByForeignKey(DomainBase.class, "example.tld", clock.nowUtc())
|
||||
.get()
|
||||
.getSubordinateHosts())
|
||||
.isEmpty();
|
||||
assertDnsTasksEnqueued("ns2.example.tld");
|
||||
HostResource hostBeforeDeletion =
|
||||
loadByForeignKey(HostResource.class, "ns2.example.tld", clock.nowUtc().minusDays(1)).get();
|
||||
assertAboutHosts()
|
||||
.that(hostBeforeDeletion)
|
||||
.isNotActiveAt(clock.nowUtc())
|
||||
.and()
|
||||
.hasExactlyStatusValues(StatusValue.OK)
|
||||
.and()
|
||||
.hasOnlyOneHistoryEntryWhich()
|
||||
.hasType(HOST_DELETE);
|
||||
HistoryEntry historyEntry = getOnlyHistoryEntryOfType(hostBeforeDeletion, HOST_DELETE);
|
||||
assertPollMessageFor(
|
||||
historyEntry,
|
||||
"TheRegistrar",
|
||||
"Deleted host ns2.example.tld.",
|
||||
true,
|
||||
host,
|
||||
Optional.of("fakeClientTrid"));
|
||||
assertNoTasksEnqueued(QUEUE_ASYNC_DELETE);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSuccess_host_notRequestedByOwner_doesNotGetDeleted() throws Exception {
|
||||
HostResource host = persistHostPendingDelete("ns2.example.tld");
|
||||
enqueuer.enqueueAsyncDelete(
|
||||
host,
|
||||
clock.nowUtc(),
|
||||
"OtherRegistrar",
|
||||
Trid.create("fakeClientTrid", "fakeServerTrid"),
|
||||
false);
|
||||
runMapreduce();
|
||||
HostResource hostAfter =
|
||||
loadByForeignKey(HostResource.class, "ns2.example.tld", clock.nowUtc()).get();
|
||||
assertAboutHosts()
|
||||
.that(hostAfter)
|
||||
.doesNotHaveStatusValue(PENDING_DELETE)
|
||||
.and()
|
||||
.hasDeletionTime(END_OF_TIME);
|
||||
HistoryEntry historyEntry = getOnlyHistoryEntryOfType(host, HOST_DELETE_FAILURE);
|
||||
assertPollMessageFor(
|
||||
historyEntry,
|
||||
"OtherRegistrar",
|
||||
"Can't delete host ns2.example.tld because it was transferred prior to deletion.",
|
||||
false,
|
||||
host,
|
||||
Optional.of("fakeClientTrid"));
|
||||
assertNoTasksEnqueued(QUEUE_ASYNC_DELETE);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSuccess_host_notRequestedByOwner_isSuperuser_getsDeleted() throws Exception {
|
||||
HostResource host = persistHostPendingDelete("ns66.example.tld");
|
||||
enqueuer.enqueueAsyncDelete(
|
||||
host,
|
||||
clock.nowUtc(),
|
||||
"OtherRegistrar",
|
||||
Trid.create("fakeClientTrid", "fakeServerTrid"),
|
||||
true);
|
||||
runMapreduce();
|
||||
assertThat(loadByForeignKey(HostResource.class, "ns66.example.tld", clock.nowUtc())).isEmpty();
|
||||
HostResource hostBeforeDeletion =
|
||||
loadByForeignKey(HostResource.class, "ns66.example.tld", clock.nowUtc().minusDays(1)).get();
|
||||
assertAboutHosts()
|
||||
.that(hostBeforeDeletion)
|
||||
.isNotActiveAt(clock.nowUtc())
|
||||
.and()
|
||||
.hasExactlyStatusValues(StatusValue.OK)
|
||||
// Note that there will be another history entry of HOST_PENDING_DELETE, but this is
|
||||
// added by the flow and not the mapreduce itself.
|
||||
.and()
|
||||
.hasOnlyOneHistoryEntryWhich()
|
||||
.hasType(HOST_DELETE);
|
||||
HistoryEntry historyEntry = getOnlyHistoryEntryOfType(hostBeforeDeletion, HOST_DELETE);
|
||||
assertPollMessageFor(
|
||||
historyEntry,
|
||||
"OtherRegistrar",
|
||||
"Deleted host ns66.example.tld.",
|
||||
true,
|
||||
host,
|
||||
Optional.of("fakeClientTrid"));
|
||||
assertNoTasksEnqueued(QUEUE_ASYNC_DELETE);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSuccess_deleteABunchOfContactsAndHosts_butNotSome() throws Exception {
|
||||
ContactResource c1 = persistContactPendingDelete("nsaid54");
|
||||
ContactResource c2 = persistContactPendingDelete("nsaid55");
|
||||
ContactResource c3 = persistContactPendingDelete("nsaid57");
|
||||
HostResource h1 = persistHostPendingDelete("nn5.example.tld");
|
||||
HostResource h2 = persistHostPendingDelete("no.foos.ball");
|
||||
HostResource h3 = persistHostPendingDelete("slime.wars.fun");
|
||||
ContactResource c4 = persistContactPendingDelete("iaminuse6");
|
||||
HostResource h4 = persistHostPendingDelete("used.host.com");
|
||||
persistUsedDomain("usescontactandhost.tld", c4, h4);
|
||||
for (EppResource resource : ImmutableList.<EppResource>of(c1, c2, c3, c4, h1, h2, h3, h4)) {
|
||||
enqueuer.enqueueAsyncDelete(
|
||||
resource,
|
||||
clock.nowUtc(),
|
||||
"TheRegistrar",
|
||||
Trid.create("fakeClientTrid", "fakeServerTrid"),
|
||||
false);
|
||||
}
|
||||
runMapreduce();
|
||||
for (EppResource resource : ImmutableList.<EppResource>of(c1, c2, c3, h1, h2, h3)) {
|
||||
EppResource loaded = ofy().load().entity(resource).now();
|
||||
assertThat(loaded.getDeletionTime()).isLessThan(DateTime.now(UTC));
|
||||
assertThat(loaded.getStatusValues()).doesNotContain(PENDING_DELETE);
|
||||
}
|
||||
for (EppResource resource : ImmutableList.<EppResource>of(c4, h4)) {
|
||||
EppResource loaded = ofy().load().entity(resource).now();
|
||||
assertThat(loaded.getDeletionTime()).isEqualTo(END_OF_TIME);
|
||||
assertThat(loaded.getStatusValues()).doesNotContain(PENDING_DELETE);
|
||||
}
|
||||
assertNoTasksEnqueued(QUEUE_ASYNC_DELETE);
|
||||
}
|
||||
|
||||
private static ContactResource persistContactWithPii(String contactId) {
|
||||
return persistResource(
|
||||
newContactResource(contactId)
|
||||
.asBuilder()
|
||||
.setLocalizedPostalInfo(
|
||||
new PostalInfo.Builder()
|
||||
.setType(PostalInfo.Type.LOCALIZED)
|
||||
.setAddress(
|
||||
new ContactAddress.Builder()
|
||||
.setStreet(ImmutableList.of("123 Grand Ave"))
|
||||
.build())
|
||||
.build())
|
||||
.setInternationalizedPostalInfo(
|
||||
new PostalInfo.Builder()
|
||||
.setType(PostalInfo.Type.INTERNATIONALIZED)
|
||||
.setAddress(
|
||||
new ContactAddress.Builder()
|
||||
.setStreet(ImmutableList.of("123 Avenida Grande"))
|
||||
.build())
|
||||
.build())
|
||||
.setEmailAddress("bob@bob.com")
|
||||
.setVoiceNumber(new ContactPhoneNumber.Builder().setPhoneNumber("555-1212").build())
|
||||
.setFaxNumber(new ContactPhoneNumber.Builder().setPhoneNumber("555-1212").build())
|
||||
.addStatusValue(PENDING_DELETE)
|
||||
.build());
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method to check that one poll message exists with a given history entry, resource,
|
||||
* client id, and message. Also checks that the only resulting async response matches the resource
|
||||
* type, and has the appropriate actionResult, nameOrId, and Trid.
|
||||
*/
|
||||
private static void assertPollMessageFor(
|
||||
HistoryEntry historyEntry,
|
||||
String clientId,
|
||||
String msg,
|
||||
boolean expectedActionResult,
|
||||
EppResource resource,
|
||||
Optional<String> clientTrid) {
|
||||
PollMessage.OneTime pollMessage = (OneTime) getOnlyPollMessageForHistoryEntry(historyEntry);
|
||||
assertThat(pollMessage.getMsg()).isEqualTo(msg);
|
||||
assertThat(pollMessage.getClientId()).isEqualTo(clientId);
|
||||
|
||||
ImmutableList<ResponseData> pollResponses = pollMessage.getResponseData();
|
||||
assertThat(pollResponses).hasSize(1);
|
||||
ResponseData responseData = pollMessage.getResponseData().get(0);
|
||||
|
||||
String expectedResourceName;
|
||||
if (resource instanceof HostResource) {
|
||||
assertThat(responseData).isInstanceOf(HostPendingActionNotificationResponse.class);
|
||||
expectedResourceName = ((HostResource) resource).getFullyQualifiedHostName();
|
||||
} else {
|
||||
assertThat(responseData).isInstanceOf(ContactPendingActionNotificationResponse.class);
|
||||
expectedResourceName = ((ContactResource) resource).getContactId();
|
||||
}
|
||||
PendingActionNotificationResponse pendingResponse =
|
||||
(PendingActionNotificationResponse) responseData;
|
||||
assertThat(pendingResponse.getActionResult()).isEqualTo(expectedActionResult);
|
||||
assertThat(pendingResponse.getNameAsString()).isEqualTo(expectedResourceName);
|
||||
Trid trid = pendingResponse.getTrid();
|
||||
assertThat(trid.getClientTransactionId()).isEqualTo(clientTrid);
|
||||
assertThat(trid.getServerTransactionId()).isEqualTo("fakeServerTrid");
|
||||
}
|
||||
|
||||
private static ContactResource persistContactPendingDelete(String contactId) {
|
||||
return persistResource(
|
||||
newContactResource(contactId).asBuilder().addStatusValue(PENDING_DELETE).build());
|
||||
}
|
||||
|
||||
private static HostResource persistHostPendingDelete(String hostName) {
|
||||
return persistResource(
|
||||
newHostResource(hostName).asBuilder().addStatusValue(PENDING_DELETE).build());
|
||||
}
|
||||
|
||||
private static DomainBase persistUsedDomain(
|
||||
String domainName, ContactResource contact, HostResource host) {
|
||||
return persistResource(
|
||||
newDomainBase(domainName, contact)
|
||||
.asBuilder()
|
||||
.setNameservers(ImmutableSet.of(Key.create(host)))
|
||||
.build());
|
||||
}
|
||||
|
||||
private Optional<Lock> acquireLock() {
|
||||
return Lock.acquire(
|
||||
DeleteContactsAndHostsAction.class.getSimpleName(),
|
||||
null,
|
||||
standardDays(30),
|
||||
requestStatusChecker,
|
||||
false);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,328 @@
|
||||
// Copyright 2017 The Nomulus Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package google.registry.batch;
|
||||
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
import static com.google.common.truth.Truth8.assertThat;
|
||||
import static google.registry.model.EppResourceUtils.loadByForeignKey;
|
||||
import static google.registry.model.ofy.ObjectifyService.ofy;
|
||||
import static google.registry.testing.DatastoreHelper.createTld;
|
||||
import static google.registry.testing.DatastoreHelper.newDomainBase;
|
||||
import static google.registry.testing.DatastoreHelper.persistActiveDomain;
|
||||
import static google.registry.testing.DatastoreHelper.persistActiveHost;
|
||||
import static google.registry.testing.DatastoreHelper.persistDeletedDomain;
|
||||
import static google.registry.testing.DatastoreHelper.persistDomainAsDeleted;
|
||||
import static google.registry.testing.DatastoreHelper.persistResource;
|
||||
import static google.registry.testing.DatastoreHelper.persistSimpleResource;
|
||||
import static google.registry.testing.JUnitBackports.assertThrows;
|
||||
import static google.registry.testing.TaskQueueHelper.assertDnsTasksEnqueued;
|
||||
import static google.registry.util.DateTimeUtils.END_OF_TIME;
|
||||
import static google.registry.util.DateTimeUtils.START_OF_TIME;
|
||||
import static org.joda.time.DateTimeZone.UTC;
|
||||
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
import com.googlecode.objectify.Key;
|
||||
import google.registry.config.RegistryEnvironment;
|
||||
import google.registry.model.ImmutableObject;
|
||||
import google.registry.model.billing.BillingEvent;
|
||||
import google.registry.model.billing.BillingEvent.Reason;
|
||||
import google.registry.model.domain.DomainBase;
|
||||
import google.registry.model.index.EppResourceIndex;
|
||||
import google.registry.model.index.ForeignKeyIndex;
|
||||
import google.registry.model.poll.PollMessage;
|
||||
import google.registry.model.registry.Registry;
|
||||
import google.registry.model.registry.Registry.TldType;
|
||||
import google.registry.model.reporting.HistoryEntry;
|
||||
import google.registry.testing.FakeResponse;
|
||||
import google.registry.testing.mapreduce.MapreduceTestCase;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import org.joda.money.Money;
|
||||
import org.joda.time.DateTime;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.junit.runners.JUnit4;
|
||||
|
||||
/** Unit tests for {@link DeleteProberDataAction}. */
|
||||
@RunWith(JUnit4.class)
|
||||
public class DeleteProberDataActionTest extends MapreduceTestCase<DeleteProberDataAction> {
|
||||
|
||||
private static final DateTime DELETION_TIME = DateTime.parse("2010-01-01T00:00:00.000Z");
|
||||
|
||||
@Before
|
||||
public void init() {
|
||||
// Entities in these two should not be touched.
|
||||
createTld("tld", "TLD");
|
||||
// Since "example" doesn't end with .test, its entities won't be deleted even though it is of
|
||||
// TEST type.
|
||||
createTld("example", "EXAMPLE");
|
||||
persistResource(Registry.get("example").asBuilder().setTldType(TldType.TEST).build());
|
||||
|
||||
// Since "not-test.test" isn't of TEST type, its entities won't be deleted even though it ends
|
||||
// with .test.
|
||||
createTld("not-test.test", "EXTEST");
|
||||
|
||||
// Entities in these two should be deleted.
|
||||
createTld("ib-any.test", "IBANYT");
|
||||
persistResource(Registry.get("ib-any.test").asBuilder().setTldType(TldType.TEST).build());
|
||||
createTld("oa-canary.test", "OACANT");
|
||||
persistResource(Registry.get("oa-canary.test").asBuilder().setTldType(TldType.TEST).build());
|
||||
|
||||
resetAction();
|
||||
}
|
||||
|
||||
private void resetAction() {
|
||||
action = new DeleteProberDataAction();
|
||||
action.mrRunner = makeDefaultRunner();
|
||||
action.response = new FakeResponse();
|
||||
action.isDryRun = false;
|
||||
action.tlds = ImmutableSet.of();
|
||||
action.registryEnvironment = RegistryEnvironment.SANDBOX;
|
||||
action.registryAdminClientId = "TheRegistrar";
|
||||
}
|
||||
|
||||
private void runMapreduce() throws Exception {
|
||||
action.run();
|
||||
executeTasksUntilEmpty("mapreduce");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test_deletesAllAndOnlyProberData() throws Exception {
|
||||
Set<ImmutableObject> tldEntities = persistLotsOfDomains("tld");
|
||||
Set<ImmutableObject> exampleEntities = persistLotsOfDomains("example");
|
||||
Set<ImmutableObject> notTestEntities = persistLotsOfDomains("not-test.test");
|
||||
Set<ImmutableObject> ibEntities = persistLotsOfDomains("ib-any.test");
|
||||
Set<ImmutableObject> oaEntities = persistLotsOfDomains("oa-canary.test");
|
||||
runMapreduce();
|
||||
assertNotDeleted(tldEntities);
|
||||
assertNotDeleted(exampleEntities);
|
||||
assertNotDeleted(notTestEntities);
|
||||
assertDeleted(ibEntities);
|
||||
assertDeleted(oaEntities);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSuccess_deletesAllAndOnlyGivenTlds() throws Exception {
|
||||
Set<ImmutableObject> tldEntities = persistLotsOfDomains("tld");
|
||||
Set<ImmutableObject> exampleEntities = persistLotsOfDomains("example");
|
||||
Set<ImmutableObject> notTestEntities = persistLotsOfDomains("not-test.test");
|
||||
Set<ImmutableObject> ibEntities = persistLotsOfDomains("ib-any.test");
|
||||
Set<ImmutableObject> oaEntities = persistLotsOfDomains("oa-canary.test");
|
||||
action.tlds = ImmutableSet.of("example", "ib-any.test");
|
||||
runMapreduce();
|
||||
assertNotDeleted(tldEntities);
|
||||
assertNotDeleted(notTestEntities);
|
||||
assertNotDeleted(oaEntities);
|
||||
assertDeleted(exampleEntities);
|
||||
assertDeleted(ibEntities);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testFail_givenNonTestTld() {
|
||||
action.tlds = ImmutableSet.of("not-test.test");
|
||||
IllegalArgumentException thrown =
|
||||
assertThrows(IllegalArgumentException.class, this::runMapreduce);
|
||||
assertThat(thrown)
|
||||
.hasMessageThat()
|
||||
.contains("If tlds are given, they must all exist and be TEST tlds");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testFail_givenNonExistentTld() {
|
||||
action.tlds = ImmutableSet.of("non-existent.test");
|
||||
IllegalArgumentException thrown =
|
||||
assertThrows(IllegalArgumentException.class, this::runMapreduce);
|
||||
assertThat(thrown)
|
||||
.hasMessageThat()
|
||||
.contains("If tlds are given, they must all exist and be TEST tlds");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testFail_givenNonDotTestTldOnProd() {
|
||||
action.tlds = ImmutableSet.of("example");
|
||||
action.registryEnvironment = RegistryEnvironment.PRODUCTION;
|
||||
IllegalArgumentException thrown =
|
||||
assertThrows(IllegalArgumentException.class, this::runMapreduce);
|
||||
assertThat(thrown)
|
||||
.hasMessageThat()
|
||||
.contains("On production, can only work on TLDs that end with .test");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSuccess_doesntDeleteNicDomainForProbers() throws Exception {
|
||||
DomainBase nic = persistActiveDomain("nic.ib-any.test");
|
||||
ForeignKeyIndex<DomainBase> fkiNic =
|
||||
ForeignKeyIndex.load(DomainBase.class, "nic.ib-any.test", START_OF_TIME);
|
||||
Set<ImmutableObject> ibEntities = persistLotsOfDomains("ib-any.test");
|
||||
runMapreduce();
|
||||
assertDeleted(ibEntities);
|
||||
assertNotDeleted(ImmutableSet.of(nic, fkiNic));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDryRun_doesntDeleteData() throws Exception {
|
||||
Set<ImmutableObject> tldEntities = persistLotsOfDomains("tld");
|
||||
Set<ImmutableObject> oaEntities = persistLotsOfDomains("oa-canary.test");
|
||||
action.isDryRun = true;
|
||||
runMapreduce();
|
||||
assertNotDeleted(tldEntities);
|
||||
assertNotDeleted(oaEntities);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSuccess_activeDomain_isSoftDeleted() throws Exception {
|
||||
DomainBase domain = persistResource(
|
||||
newDomainBase("blah.ib-any.test")
|
||||
.asBuilder()
|
||||
.setCreationTimeForTest(DateTime.now(UTC).minusYears(1))
|
||||
.build());
|
||||
runMapreduce();
|
||||
DateTime timeAfterDeletion = DateTime.now(UTC);
|
||||
assertThat(loadByForeignKey(DomainBase.class, "blah.ib-any.test", timeAfterDeletion))
|
||||
.isEmpty();
|
||||
assertThat(ofy().load().entity(domain).now().getDeletionTime()).isLessThan(timeAfterDeletion);
|
||||
assertDnsTasksEnqueued("blah.ib-any.test");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSuccess_activeDomain_doubleMapSoftDeletes() throws Exception {
|
||||
DomainBase domain = persistResource(
|
||||
newDomainBase("blah.ib-any.test")
|
||||
.asBuilder()
|
||||
.setCreationTimeForTest(DateTime.now(UTC).minusYears(1))
|
||||
.build());
|
||||
runMapreduce();
|
||||
DateTime timeAfterDeletion = DateTime.now(UTC);
|
||||
resetAction();
|
||||
runMapreduce();
|
||||
assertThat(loadByForeignKey(DomainBase.class, "blah.ib-any.test", timeAfterDeletion))
|
||||
.isEmpty();
|
||||
assertThat(ofy().load().entity(domain).now().getDeletionTime()).isLessThan(timeAfterDeletion);
|
||||
assertDnsTasksEnqueued("blah.ib-any.test");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test_recentlyCreatedDomain_isntDeletedYet() throws Exception {
|
||||
persistResource(
|
||||
newDomainBase("blah.ib-any.test")
|
||||
.asBuilder()
|
||||
.setCreationTimeForTest(DateTime.now(UTC).minusSeconds(1))
|
||||
.build());
|
||||
runMapreduce();
|
||||
Optional<DomainBase> domain =
|
||||
loadByForeignKey(DomainBase.class, "blah.ib-any.test", DateTime.now(UTC));
|
||||
assertThat(domain).isPresent();
|
||||
assertThat(domain.get().getDeletionTime()).isEqualTo(END_OF_TIME);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDryRun_doesntSoftDeleteData() throws Exception {
|
||||
DomainBase domain = persistResource(
|
||||
newDomainBase("blah.ib-any.test")
|
||||
.asBuilder()
|
||||
.setCreationTimeForTest(DateTime.now(UTC).minusYears(1))
|
||||
.build());
|
||||
action.isDryRun = true;
|
||||
runMapreduce();
|
||||
assertThat(ofy().load().entity(domain).now().getDeletionTime()).isEqualTo(END_OF_TIME);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test_domainWithSubordinateHosts_isSkipped() throws Exception {
|
||||
persistActiveHost("ns1.blah.ib-any.test");
|
||||
DomainBase nakedDomain =
|
||||
persistDeletedDomain("todelete.ib-any.test", DateTime.now(UTC).minusYears(1));
|
||||
DomainBase domainWithSubord =
|
||||
persistDomainAsDeleted(
|
||||
newDomainBase("blah.ib-any.test")
|
||||
.asBuilder()
|
||||
.setSubordinateHosts(ImmutableSet.of("ns1.blah.ib-any.test"))
|
||||
.build(),
|
||||
DateTime.now(UTC).minusYears(1));
|
||||
runMapreduce();
|
||||
assertThat(ofy().load().entity(domainWithSubord).now()).isNotNull();
|
||||
assertThat(ofy().load().entity(nakedDomain).now()).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testFailure_registryAdminClientId_isRequiredForSoftDeletion() {
|
||||
persistResource(
|
||||
newDomainBase("blah.ib-any.test")
|
||||
.asBuilder()
|
||||
.setCreationTimeForTest(DateTime.now(UTC).minusYears(1))
|
||||
.build());
|
||||
action.registryAdminClientId = null;
|
||||
IllegalStateException thrown = assertThrows(IllegalStateException.class, this::runMapreduce);
|
||||
assertThat(thrown).hasMessageThat().contains("Registry admin client ID must be configured");
|
||||
}
|
||||
|
||||
/**
|
||||
* Persists and returns a domain and a descendant history entry, billing event, and poll message,
|
||||
* along with the ForeignKeyIndex and EppResourceIndex.
|
||||
*/
|
||||
private static Set<ImmutableObject> persistDomainAndDescendants(String fqdn) {
|
||||
DomainBase domain = persistDeletedDomain(fqdn, DELETION_TIME);
|
||||
HistoryEntry historyEntry = persistSimpleResource(
|
||||
new HistoryEntry.Builder()
|
||||
.setParent(domain)
|
||||
.setType(HistoryEntry.Type.DOMAIN_CREATE)
|
||||
.build());
|
||||
BillingEvent.OneTime billingEvent = persistSimpleResource(
|
||||
new BillingEvent.OneTime.Builder()
|
||||
.setParent(historyEntry)
|
||||
.setBillingTime(DELETION_TIME.plusYears(1))
|
||||
.setCost(Money.parse("USD 10"))
|
||||
.setPeriodYears(1)
|
||||
.setReason(Reason.CREATE)
|
||||
.setClientId("TheRegistrar")
|
||||
.setEventTime(DELETION_TIME)
|
||||
.setTargetId(fqdn)
|
||||
.build());
|
||||
PollMessage.OneTime pollMessage = persistSimpleResource(
|
||||
new PollMessage.OneTime.Builder()
|
||||
.setParent(historyEntry)
|
||||
.setEventTime(DELETION_TIME)
|
||||
.setClientId("TheRegistrar")
|
||||
.setMsg("Domain registered")
|
||||
.build());
|
||||
ForeignKeyIndex<DomainBase> fki =
|
||||
ForeignKeyIndex.load(DomainBase.class, fqdn, START_OF_TIME);
|
||||
EppResourceIndex eppIndex =
|
||||
ofy().load().entity(EppResourceIndex.create(Key.create(domain))).now();
|
||||
return ImmutableSet.of(
|
||||
domain, historyEntry, billingEvent, pollMessage, fki, eppIndex);
|
||||
}
|
||||
|
||||
private static Set<ImmutableObject> persistLotsOfDomains(String tld) {
|
||||
ImmutableSet.Builder<ImmutableObject> persistedObjects = new ImmutableSet.Builder<>();
|
||||
for (int i = 0; i < 20; i++) {
|
||||
persistedObjects.addAll(persistDomainAndDescendants(String.format("domain%d.%s", i, tld)));
|
||||
}
|
||||
return persistedObjects.build();
|
||||
}
|
||||
|
||||
private static void assertNotDeleted(Iterable<ImmutableObject> entities) {
|
||||
for (ImmutableObject entity : entities) {
|
||||
assertThat(ofy().load().entity(entity).now()).isNotNull();
|
||||
}
|
||||
}
|
||||
|
||||
private static void assertDeleted(Iterable<ImmutableObject> entities) {
|
||||
for (ImmutableObject entity : entities) {
|
||||
assertThat(ofy().load().entity(entity).now()).isNull();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,677 @@
|
||||
// Copyright 2017 The Nomulus Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package google.registry.batch;
|
||||
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
import static google.registry.model.common.Cursor.CursorType.RECURRING_BILLING;
|
||||
import static google.registry.model.domain.Period.Unit.YEARS;
|
||||
import static google.registry.model.ofy.ObjectifyService.ofy;
|
||||
import static google.registry.model.reporting.HistoryEntry.Type.DOMAIN_AUTORENEW;
|
||||
import static google.registry.testing.DatastoreHelper.assertBillingEvents;
|
||||
import static google.registry.testing.DatastoreHelper.assertBillingEventsForResource;
|
||||
import static google.registry.testing.DatastoreHelper.createTld;
|
||||
import static google.registry.testing.DatastoreHelper.getHistoryEntriesOfType;
|
||||
import static google.registry.testing.DatastoreHelper.getOnlyHistoryEntryOfType;
|
||||
import static google.registry.testing.DatastoreHelper.persistActiveDomain;
|
||||
import static google.registry.testing.DatastoreHelper.persistDeletedDomain;
|
||||
import static google.registry.testing.DatastoreHelper.persistPremiumList;
|
||||
import static google.registry.testing.DatastoreHelper.persistResource;
|
||||
import static google.registry.testing.JUnitBackports.assertThrows;
|
||||
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.collect.ImmutableSet;
|
||||
import com.google.common.collect.ImmutableSortedMap;
|
||||
import com.google.common.collect.Iterables;
|
||||
import com.googlecode.objectify.Key;
|
||||
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.Reason;
|
||||
import google.registry.model.common.Cursor;
|
||||
import google.registry.model.domain.DomainBase;
|
||||
import google.registry.model.domain.Period;
|
||||
import google.registry.model.ofy.Ofy;
|
||||
import google.registry.model.registry.Registry;
|
||||
import google.registry.model.reporting.DomainTransactionRecord;
|
||||
import google.registry.model.reporting.DomainTransactionRecord.TransactionReportField;
|
||||
import google.registry.model.reporting.HistoryEntry;
|
||||
import google.registry.testing.FakeClock;
|
||||
import google.registry.testing.FakeResponse;
|
||||
import google.registry.testing.InjectRule;
|
||||
import google.registry.testing.mapreduce.MapreduceTestCase;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
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;
|
||||
|
||||
/** Unit tests for {@link ExpandRecurringBillingEventsAction}. */
|
||||
@RunWith(JUnit4.class)
|
||||
public class ExpandRecurringBillingEventsActionTest
|
||||
extends MapreduceTestCase<ExpandRecurringBillingEventsAction> {
|
||||
@Rule
|
||||
public final InjectRule inject = new InjectRule();
|
||||
|
||||
private final DateTime beginningOfTest = DateTime.parse("2000-10-02T00:00:00Z");
|
||||
private final FakeClock clock = new FakeClock(beginningOfTest);
|
||||
|
||||
DomainBase domain;
|
||||
HistoryEntry historyEntry;
|
||||
BillingEvent.Recurring recurring;
|
||||
|
||||
@Before
|
||||
public void init() {
|
||||
inject.setStaticField(Ofy.class, "clock", clock);
|
||||
action = new ExpandRecurringBillingEventsAction();
|
||||
action.mrRunner = makeDefaultRunner();
|
||||
action.clock = clock;
|
||||
action.cursorTimeParam = Optional.empty();
|
||||
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) {
|
||||
ofy().transact(() -> ofy().save().entity(Cursor.createGlobal(RECURRING_BILLING, cursorTime)));
|
||||
}
|
||||
|
||||
void runMapreduce() throws Exception {
|
||||
action.response = new FakeResponse();
|
||||
action.run();
|
||||
executeTasksUntilEmpty("mapreduce", clock);
|
||||
ofy().clearSessionCache();
|
||||
}
|
||||
|
||||
void assertCursorAt(DateTime expectedCursorTime) {
|
||||
Cursor cursor = ofy().load().key(Cursor.createGlobalKey(RECURRING_BILLING)).now();
|
||||
assertThat(cursor).isNotNull();
|
||||
assertThat(cursor.getCursorTime()).isEqualTo(expectedCursorTime);
|
||||
}
|
||||
|
||||
void assertHistoryEntryMatches(
|
||||
DomainBase domain, HistoryEntry actual, String clientId, DateTime billingTime) {
|
||||
assertThat(actual.getBySuperuser()).isFalse();
|
||||
assertThat(actual.getClientId()).isEqualTo(clientId);
|
||||
assertThat(actual.getParent()).isEqualTo(Key.create(domain));
|
||||
assertThat(actual.getPeriod()).isEqualTo(Period.create(1, YEARS));
|
||||
assertThat(actual.getReason())
|
||||
.isEqualTo("Domain autorenewal by ExpandRecurringBillingEventsAction");
|
||||
assertThat(actual.getRequestedByRegistrar()).isFalse();
|
||||
assertThat(actual.getType()).isEqualTo(DOMAIN_AUTORENEW);
|
||||
assertThat(actual.getDomainTransactionRecords())
|
||||
.containsExactly(
|
||||
DomainTransactionRecord.create(
|
||||
"tld",
|
||||
billingTime,
|
||||
TransactionReportField.NET_RENEWS_1_YR,
|
||||
1));
|
||||
}
|
||||
|
||||
private OneTime.Builder defaultOneTimeBuilder() {
|
||||
return 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))
|
||||
.setPeriodYears(1)
|
||||
.setReason(Reason.RENEW)
|
||||
.setSyntheticCreationTime(beginningOfTest)
|
||||
.setCancellationMatchingBillingEvent(Key.create(recurring))
|
||||
.setTargetId(domain.getFullyQualifiedDomainName());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSuccess_expandSingleEvent() throws Exception {
|
||||
persistResource(recurring);
|
||||
action.cursorTimeParam = Optional.of(START_OF_TIME);
|
||||
runMapreduce();
|
||||
HistoryEntry persistedEntry = getOnlyHistoryEntryOfType(domain, DOMAIN_AUTORENEW);
|
||||
assertHistoryEntryMatches(
|
||||
domain, persistedEntry, "TheRegistrar", DateTime.parse("2000-02-19T00:00:00Z"));
|
||||
BillingEvent.OneTime expected = defaultOneTimeBuilder()
|
||||
.setParent(persistedEntry)
|
||||
.build();
|
||||
assertBillingEventsForResource(domain, expected, recurring);
|
||||
assertCursorAt(beginningOfTest);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSuccess_expandSingleEvent_deletedDomain() throws Exception {
|
||||
DateTime deletionTime = DateTime.parse("2000-08-01T00:00:00Z");
|
||||
DomainBase deletedDomain = persistDeletedDomain("deleted.tld", deletionTime);
|
||||
historyEntry = persistResource(new HistoryEntry.Builder().setParent(deletedDomain).build());
|
||||
recurring = persistResource(new BillingEvent.Recurring.Builder()
|
||||
.setParent(historyEntry)
|
||||
.setClientId(deletedDomain.getCreationClientId())
|
||||
.setEventTime(DateTime.parse("2000-01-05T00:00:00Z"))
|
||||
.setFlags(ImmutableSet.of(Flag.AUTO_RENEW))
|
||||
.setId(2L)
|
||||
.setReason(Reason.RENEW)
|
||||
.setRecurrenceEndTime(deletionTime)
|
||||
.setTargetId(deletedDomain.getFullyQualifiedDomainName())
|
||||
.build());
|
||||
action.cursorTimeParam = Optional.of(START_OF_TIME);
|
||||
runMapreduce();
|
||||
HistoryEntry persistedEntry = getOnlyHistoryEntryOfType(deletedDomain, DOMAIN_AUTORENEW);
|
||||
assertHistoryEntryMatches(
|
||||
deletedDomain, persistedEntry, "TheRegistrar", DateTime.parse("2000-02-19T00:00:00Z"));
|
||||
BillingEvent.OneTime expected = defaultOneTimeBuilder()
|
||||
.setParent(persistedEntry)
|
||||
.setTargetId(deletedDomain.getFullyQualifiedDomainName())
|
||||
.build();
|
||||
assertBillingEventsForResource(deletedDomain, expected, recurring);
|
||||
assertCursorAt(beginningOfTest);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSuccess_expandSingleEvent_idempotentForDuplicateRuns() throws Exception {
|
||||
persistResource(recurring);
|
||||
action.cursorTimeParam = Optional.of(START_OF_TIME);
|
||||
runMapreduce();
|
||||
HistoryEntry persistedEntry = getOnlyHistoryEntryOfType(domain, DOMAIN_AUTORENEW);
|
||||
assertHistoryEntryMatches(
|
||||
domain, persistedEntry, "TheRegistrar", DateTime.parse("2000-02-19T00:00:00Z"));
|
||||
BillingEvent.OneTime expected = defaultOneTimeBuilder().setParent(persistedEntry).build();
|
||||
assertCursorAt(beginningOfTest);
|
||||
DateTime beginningOfSecondRun = clock.nowUtc();
|
||||
action.response = new FakeResponse();
|
||||
runMapreduce();
|
||||
assertCursorAt(beginningOfSecondRun);
|
||||
assertBillingEventsForResource(domain, expected, recurring);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSuccess_expandSingleEvent_idempotentForExistingOneTime() throws Exception {
|
||||
persistResource(recurring);
|
||||
BillingEvent.OneTime persisted = persistResource(defaultOneTimeBuilder()
|
||||
.setParent(historyEntry)
|
||||
.build());
|
||||
action.cursorTimeParam = Optional.of(START_OF_TIME);
|
||||
runMapreduce();
|
||||
// No new history entries should be generated
|
||||
assertThat(getHistoryEntriesOfType(domain, DOMAIN_AUTORENEW)).isEmpty();
|
||||
assertCursorAt(beginningOfTest);
|
||||
// No additional billing events should be generated
|
||||
assertBillingEventsForResource(domain, persisted, recurring);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSuccess_expandSingleEvent_notIdempotentForDifferentBillingTime()
|
||||
throws Exception {
|
||||
persistResource(recurring);
|
||||
action.cursorTimeParam = Optional.of(START_OF_TIME);
|
||||
runMapreduce();
|
||||
HistoryEntry persistedEntry = getOnlyHistoryEntryOfType(domain, DOMAIN_AUTORENEW);
|
||||
assertHistoryEntryMatches(
|
||||
domain, persistedEntry, "TheRegistrar", DateTime.parse("2000-02-19T00:00:00Z"));
|
||||
BillingEvent.OneTime expected = defaultOneTimeBuilder().setParent(persistedEntry).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());
|
||||
assertCursorAt(beginningOfTest);
|
||||
assertBillingEventsForResource(domain, persisted, expected, recurring);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSuccess_expandSingleEvent_notIdempotentForDifferentRecurring()
|
||||
throws Exception {
|
||||
persistResource(recurring);
|
||||
BillingEvent.Recurring recurring2 = persistResource(recurring.asBuilder()
|
||||
.setId(3L)
|
||||
.build());
|
||||
action.cursorTimeParam = Optional.of(START_OF_TIME);
|
||||
runMapreduce();
|
||||
List<HistoryEntry> persistedEntries =
|
||||
getHistoryEntriesOfType(domain, DOMAIN_AUTORENEW);
|
||||
for (HistoryEntry persistedEntry : persistedEntries) {
|
||||
assertHistoryEntryMatches(
|
||||
domain, persistedEntry, "TheRegistrar", DateTime.parse("2000-02-19T00:00:00Z"));
|
||||
}
|
||||
assertThat(persistedEntries).hasSize(2);
|
||||
BillingEvent.OneTime expected = defaultOneTimeBuilder()
|
||||
.setParent(persistedEntries.get(0))
|
||||
.build();
|
||||
// Persist an otherwise identical billing event that differs only in recurring event key.
|
||||
BillingEvent.OneTime persisted = expected.asBuilder()
|
||||
.setParent(persistedEntries.get(1))
|
||||
.setCancellationMatchingBillingEvent(Key.create(recurring2))
|
||||
.build();
|
||||
assertCursorAt(beginningOfTest);
|
||||
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();
|
||||
// No new history entries should be generated
|
||||
assertThat(getHistoryEntriesOfType(domain, DOMAIN_AUTORENEW)).isEmpty();
|
||||
assertBillingEventsForResource(domain, recurring);
|
||||
assertCursorAt(beginningOfTest);
|
||||
}
|
||||
|
||||
@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();
|
||||
// No new history entries should be generated
|
||||
assertThat(getHistoryEntriesOfType(domain, DOMAIN_AUTORENEW)).isEmpty();
|
||||
assertBillingEventsForResource(domain, recurring);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSuccess_expandSingleEvent_billingTimeAtCursorTime() throws Exception {
|
||||
persistResource(recurring);
|
||||
action.cursorTimeParam = Optional.of(DateTime.parse("2000-02-19T00:00:00Z"));
|
||||
runMapreduce();
|
||||
HistoryEntry persistedEntry = getOnlyHistoryEntryOfType(domain, DOMAIN_AUTORENEW);
|
||||
assertHistoryEntryMatches(
|
||||
domain, persistedEntry, "TheRegistrar", DateTime.parse("2000-02-19T00:00:00Z"));
|
||||
BillingEvent.OneTime expected = defaultOneTimeBuilder().setParent(persistedEntry).build();
|
||||
assertBillingEventsForResource(domain, expected, recurring);
|
||||
assertCursorAt(beginningOfTest);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSuccess_expandSingleEvent_cursorTimeBetweenEventAndBillingTime()
|
||||
throws Exception {
|
||||
persistResource(recurring);
|
||||
action.cursorTimeParam = Optional.of(DateTime.parse("2000-01-12T00:00:00Z"));
|
||||
runMapreduce();
|
||||
HistoryEntry persistedEntry = getOnlyHistoryEntryOfType(domain, DOMAIN_AUTORENEW);
|
||||
assertHistoryEntryMatches(
|
||||
domain, persistedEntry, "TheRegistrar", DateTime.parse("2000-02-19T00:00:00Z"));
|
||||
BillingEvent.OneTime expected = defaultOneTimeBuilder().setParent(persistedEntry).build();
|
||||
assertBillingEventsForResource(domain, expected, recurring);
|
||||
assertCursorAt(beginningOfTest);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSuccess_expandSingleEvent_billingTimeAtExecutionTime() throws Exception {
|
||||
DateTime testTime = DateTime.parse("2000-02-19T00:00:00Z").minusMillis(1);
|
||||
persistResource(recurring);
|
||||
action.cursorTimeParam = Optional.of(START_OF_TIME);
|
||||
// Clock is advanced one milli in runMapreduce()
|
||||
clock.setTo(testTime);
|
||||
runMapreduce();
|
||||
// No new history entries should be generated
|
||||
assertThat(getHistoryEntriesOfType(domain, DOMAIN_AUTORENEW)).isEmpty();
|
||||
// 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(testTime);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSuccess_expandSingleEvent_multipleYearCreate() throws Exception {
|
||||
DateTime testTime = beginningOfTest.plusYears(2);
|
||||
action.cursorTimeParam = Optional.of(recurring.getEventTime());
|
||||
recurring =
|
||||
persistResource(
|
||||
recurring.asBuilder().setEventTime(recurring.getEventTime().plusYears(2)).build());
|
||||
clock.setTo(testTime);
|
||||
runMapreduce();
|
||||
HistoryEntry persistedEntry = getOnlyHistoryEntryOfType(domain, DOMAIN_AUTORENEW);
|
||||
assertHistoryEntryMatches(
|
||||
domain, persistedEntry, "TheRegistrar", DateTime.parse("2002-02-19T00:00:00Z"));
|
||||
BillingEvent.OneTime expected = defaultOneTimeBuilder()
|
||||
.setBillingTime(DateTime.parse("2002-02-19T00:00:00Z"))
|
||||
.setEventTime(DateTime.parse("2002-01-05T00:00:00Z"))
|
||||
.setParent(persistedEntry)
|
||||
.setSyntheticCreationTime(testTime)
|
||||
.build();
|
||||
assertBillingEventsForResource(domain, expected, recurring);
|
||||
assertCursorAt(testTime);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSuccess_expandSingleEvent_withCursor() throws Exception {
|
||||
persistResource(recurring);
|
||||
saveCursor(START_OF_TIME);
|
||||
runMapreduce();
|
||||
HistoryEntry persistedEntry = getOnlyHistoryEntryOfType(domain, DOMAIN_AUTORENEW);
|
||||
assertHistoryEntryMatches(
|
||||
domain, persistedEntry, "TheRegistrar", DateTime.parse("2000-02-19T00:00:00Z"));
|
||||
BillingEvent.OneTime expected = defaultOneTimeBuilder().setParent(persistedEntry).build();
|
||||
assertBillingEventsForResource(domain, expected, recurring);
|
||||
assertCursorAt(beginningOfTest);
|
||||
}
|
||||
|
||||
@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();
|
||||
// No new history entries should be generated
|
||||
assertThat(getHistoryEntriesOfType(domain, DOMAIN_AUTORENEW)).isEmpty();
|
||||
assertBillingEventsForResource(domain, recurring);
|
||||
assertCursorAt(beginningOfTest);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSuccess_expandSingleEvent_recurrenceEndBeforeEvent() throws Exception {
|
||||
// This can occur when a domain is transferred or deleted before a domain comes up for renewal.
|
||||
recurring = persistResource(recurring.asBuilder()
|
||||
.setRecurrenceEndTime(recurring.getEventTime().minusDays(5))
|
||||
.build());
|
||||
action.cursorTimeParam = Optional.of(START_OF_TIME);
|
||||
runMapreduce();
|
||||
// No new history entries should be generated
|
||||
assertThat(getHistoryEntriesOfType(domain, DOMAIN_AUTORENEW)).isEmpty();
|
||||
assertBillingEventsForResource(domain, recurring);
|
||||
assertCursorAt(beginningOfTest);
|
||||
}
|
||||
|
||||
@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();
|
||||
// No new history entries should be generated
|
||||
assertThat(getHistoryEntriesOfType(domain, DOMAIN_AUTORENEW)).isEmpty();
|
||||
assertBillingEventsForResource(domain, recurring);
|
||||
assertCursorAt(START_OF_TIME); // Cursor doesn't move on a dry run.
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSuccess_expandSingleEvent_multipleYears() throws Exception {
|
||||
DateTime testTime = clock.nowUtc().plusYears(5);
|
||||
clock.setTo(testTime);
|
||||
List<BillingEvent> expectedEvents = new ArrayList<>();
|
||||
expectedEvents.add(persistResource(recurring));
|
||||
action.cursorTimeParam = Optional.of(START_OF_TIME);
|
||||
runMapreduce();
|
||||
List<HistoryEntry> persistedEntries =
|
||||
getHistoryEntriesOfType(domain, DOMAIN_AUTORENEW);
|
||||
assertThat(persistedEntries).hasSize(6);
|
||||
DateTime eventDate = DateTime.parse("2000-01-05T00:00:00Z");
|
||||
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++) {
|
||||
assertHistoryEntryMatches(
|
||||
domain,
|
||||
persistedEntries.get(year),
|
||||
"TheRegistrar",
|
||||
billingDate.plusYears(year));
|
||||
expectedEvents.add(defaultOneTimeBuilder()
|
||||
.setBillingTime(billingDate.plusYears(year))
|
||||
.setEventTime(eventDate.plusYears(year))
|
||||
.setParent(persistedEntries.get(year))
|
||||
.setSyntheticCreationTime(testTime)
|
||||
.build());
|
||||
}
|
||||
assertBillingEventsForResource(domain, Iterables.toArray(expectedEvents, BillingEvent.class));
|
||||
assertCursorAt(testTime);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSuccess_expandSingleEvent_multipleYears_cursorInBetweenYears() throws Exception {
|
||||
DateTime testTime = clock.nowUtc().plusYears(5);
|
||||
clock.setTo(testTime);
|
||||
List<BillingEvent> expectedEvents = new ArrayList<>();
|
||||
expectedEvents.add(persistResource(recurring));
|
||||
saveCursor(DateTime.parse("2003-10-02T00:00:00Z"));
|
||||
runMapreduce();
|
||||
List<HistoryEntry> persistedEntries =
|
||||
getHistoryEntriesOfType(domain, DOMAIN_AUTORENEW);
|
||||
assertThat(persistedEntries).hasSize(2);
|
||||
DateTime eventDate = DateTime.parse("2004-01-05T00:00:00Z");
|
||||
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++) {
|
||||
assertHistoryEntryMatches(
|
||||
domain, persistedEntries.get(year), "TheRegistrar", billingDate.plusYears(year));
|
||||
expectedEvents.add(defaultOneTimeBuilder()
|
||||
.setBillingTime(billingDate.plusYears(year))
|
||||
.setParent(persistedEntries.get(year))
|
||||
.setEventTime(eventDate.plusYears(year))
|
||||
.setSyntheticCreationTime(testTime)
|
||||
.build());
|
||||
}
|
||||
assertBillingEventsForResource(
|
||||
domain, Iterables.toArray(expectedEvents, BillingEvent.class));
|
||||
assertCursorAt(testTime);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSuccess_singleEvent_beforeRenewal() throws Exception {
|
||||
DateTime testTime = DateTime.parse("2000-01-04T00:00:00Z");
|
||||
clock.setTo(testTime);
|
||||
persistResource(recurring);
|
||||
action.cursorTimeParam = Optional.of(START_OF_TIME);
|
||||
runMapreduce();
|
||||
// No new history entries should be generated
|
||||
assertThat(getHistoryEntriesOfType(domain, DOMAIN_AUTORENEW)).isEmpty();
|
||||
assertBillingEventsForResource(domain, recurring);
|
||||
assertCursorAt(testTime);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSuccess_singleEvent_afterRecurrenceEnd() throws Exception {
|
||||
DateTime testTime = beginningOfTest.plusYears(2);
|
||||
clock.setTo(testTime);
|
||||
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();
|
||||
HistoryEntry persistedEntry = getOnlyHistoryEntryOfType(domain, DOMAIN_AUTORENEW);
|
||||
assertHistoryEntryMatches(
|
||||
domain, persistedEntry, "TheRegistrar", DateTime.parse("2000-02-19T00:00:00Z"));
|
||||
BillingEvent.OneTime expected = defaultOneTimeBuilder()
|
||||
.setBillingTime(DateTime.parse("2000-02-19T00:00:00Z"))
|
||||
.setParent(persistedEntry)
|
||||
.setSyntheticCreationTime(testTime)
|
||||
.build();
|
||||
assertBillingEventsForResource(domain, recurring, expected);
|
||||
assertCursorAt(testTime);
|
||||
}
|
||||
|
||||
@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();
|
||||
HistoryEntry persistedEntry = getOnlyHistoryEntryOfType(domain, DOMAIN_AUTORENEW);
|
||||
assertHistoryEntryMatches(
|
||||
domain, persistedEntry, "TheRegistrar", DateTime.parse("2000-02-29T00:00:00Z"));
|
||||
BillingEvent.OneTime expected = defaultOneTimeBuilder()
|
||||
.setBillingTime(DateTime.parse("2000-02-29T00:00:00Z"))
|
||||
.setEventTime(DateTime.parse("2000-01-15T00:00:00Z"))
|
||||
.setParent(persistedEntry)
|
||||
.build();
|
||||
assertBillingEventsForResource(domain, expected, recurring);
|
||||
assertCursorAt(beginningOfTest);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSuccess_expandSingleEvent_billingTimeNotOnLeapYear() throws Exception {
|
||||
DateTime testTime = DateTime.parse("2001-12-01T00:00:00Z");
|
||||
recurring =
|
||||
persistResource(
|
||||
recurring.asBuilder().setEventTime(DateTime.parse("2001-01-15T00:00:00Z")).build());
|
||||
action.cursorTimeParam = Optional.of(START_OF_TIME);
|
||||
clock.setTo(testTime);
|
||||
runMapreduce();
|
||||
HistoryEntry persistedEntry = getOnlyHistoryEntryOfType(domain, DOMAIN_AUTORENEW);
|
||||
assertHistoryEntryMatches(
|
||||
domain, persistedEntry, "TheRegistrar", DateTime.parse("2001-03-01T00:00:00Z"));
|
||||
BillingEvent.OneTime expected = defaultOneTimeBuilder()
|
||||
.setBillingTime(DateTime.parse("2001-03-01T00:00:00Z"))
|
||||
.setEventTime(DateTime.parse("2001-01-15T00:00:00Z"))
|
||||
.setParent(persistedEntry)
|
||||
.setSyntheticCreationTime(testTime)
|
||||
.build();
|
||||
assertBillingEventsForResource(domain, expected, recurring);
|
||||
assertCursorAt(testTime);
|
||||
}
|
||||
|
||||
@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();
|
||||
List<HistoryEntry> persistedEntries = getHistoryEntriesOfType(domain, DOMAIN_AUTORENEW);
|
||||
assertThat(persistedEntries).hasSize(2);
|
||||
assertHistoryEntryMatches(
|
||||
domain, persistedEntries.get(0), "TheRegistrar", DateTime.parse("2000-02-19T00:00:00Z"));
|
||||
BillingEvent.OneTime expected = defaultOneTimeBuilder()
|
||||
.setParent(persistedEntries.get(0))
|
||||
.setCancellationMatchingBillingEvent(Key.create(recurring))
|
||||
.build();
|
||||
assertHistoryEntryMatches(
|
||||
domain, persistedEntries.get(1), "TheRegistrar", DateTime.parse("2000-05-20T00:00:00Z"));
|
||||
BillingEvent.OneTime expected2 = defaultOneTimeBuilder()
|
||||
.setBillingTime(DateTime.parse("2000-05-20T00:00:00Z"))
|
||||
.setEventTime(DateTime.parse("2000-04-05T00:00:00Z"))
|
||||
.setParent(persistedEntries.get(1))
|
||||
.setCancellationMatchingBillingEvent(Key.create(recurring2))
|
||||
.build();
|
||||
assertBillingEventsForResource(domain, expected, expected2, recurring, recurring2);
|
||||
assertCursorAt(beginningOfTest);
|
||||
}
|
||||
|
||||
@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();
|
||||
HistoryEntry persistedEntry = getOnlyHistoryEntryOfType(domain, DOMAIN_AUTORENEW);
|
||||
assertHistoryEntryMatches(
|
||||
domain, persistedEntry, "TheRegistrar", DateTime.parse("2000-02-19T00:00:00Z"));
|
||||
BillingEvent.OneTime expected = defaultOneTimeBuilder()
|
||||
.setParent(persistedEntry)
|
||||
.setCost(Money.of(USD, 100))
|
||||
.build();
|
||||
assertBillingEventsForResource(domain, expected, recurring);
|
||||
assertCursorAt(beginningOfTest);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSuccess_varyingRenewPrices() throws Exception {
|
||||
DateTime testTime = beginningOfTest.plusYears(1);
|
||||
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(testTime);
|
||||
persistResource(recurring);
|
||||
action.cursorTimeParam = Optional.of(START_OF_TIME);
|
||||
runMapreduce();
|
||||
List<HistoryEntry> persistedEntries =
|
||||
getHistoryEntriesOfType(domain, DOMAIN_AUTORENEW);
|
||||
assertThat(persistedEntries).hasSize(2);
|
||||
DateTime eventDate = DateTime.parse("2000-01-05T00:00:00Z");
|
||||
DateTime billingDate = DateTime.parse("2000-02-19T00:00:00Z");
|
||||
assertHistoryEntryMatches(domain, persistedEntries.get(0), "TheRegistrar", billingDate);
|
||||
BillingEvent.OneTime cheaper = defaultOneTimeBuilder()
|
||||
.setBillingTime(billingDate)
|
||||
.setEventTime(eventDate)
|
||||
.setParent(persistedEntries.get(0))
|
||||
.setCost(Money.of(USD, 8))
|
||||
.setSyntheticCreationTime(testTime)
|
||||
.build();
|
||||
assertHistoryEntryMatches(
|
||||
domain, persistedEntries.get(1), "TheRegistrar", billingDate.plusYears(1));
|
||||
BillingEvent.OneTime expensive = cheaper.asBuilder()
|
||||
.setCost(Money.of(USD, 10))
|
||||
.setBillingTime(billingDate.plusYears(1))
|
||||
.setEventTime(eventDate.plusYears(1))
|
||||
.setParent(persistedEntries.get(1))
|
||||
.build();
|
||||
assertBillingEventsForResource(domain, recurring, cheaper, expensive);
|
||||
assertCursorAt(testTime);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testFailure_cursorAfterExecutionTime() {
|
||||
action.cursorTimeParam = Optional.of(clock.nowUtc().plusYears(1));
|
||||
IllegalArgumentException thrown =
|
||||
assertThrows(IllegalArgumentException.class, this::runMapreduce);
|
||||
assertThat(thrown)
|
||||
.hasMessageThat()
|
||||
.contains("Cursor time must be earlier than execution time.");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testFailure_cursorAtExecutionTime() {
|
||||
// The clock advances one milli on runMapreduce.
|
||||
action.cursorTimeParam = Optional.of(clock.nowUtc().plusMillis(1));
|
||||
IllegalArgumentException thrown =
|
||||
assertThrows(IllegalArgumentException.class, this::runMapreduce);
|
||||
assertThat(thrown)
|
||||
.hasMessageThat()
|
||||
.contains("Cursor time must be earlier than execution time.");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testFailure_mapperException_doesNotMoveCursor() throws Exception {
|
||||
saveCursor(START_OF_TIME); // Need a saved cursor to verify that it didn't move.
|
||||
// Set target to a TLD that doesn't exist.
|
||||
recurring = persistResource(recurring.asBuilder().setTargetId("domain.junk").build());
|
||||
runMapreduce();
|
||||
// No new history entries should be generated
|
||||
assertThat(getHistoryEntriesOfType(domain, DOMAIN_AUTORENEW)).isEmpty();
|
||||
assertBillingEvents(recurring); // only the bogus one in Datastore
|
||||
assertCursorAt(START_OF_TIME); // Cursor doesn't move on a failure.
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,251 @@
|
||||
// Copyright 2017 The Nomulus Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package google.registry.batch;
|
||||
|
||||
import static com.google.appengine.api.taskqueue.QueueFactory.getQueue;
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
import static com.google.common.truth.Truth8.assertThat;
|
||||
import static google.registry.batch.AsyncTaskEnqueuer.QUEUE_ASYNC_ACTIONS;
|
||||
import static google.registry.batch.AsyncTaskEnqueuer.QUEUE_ASYNC_DELETE;
|
||||
import static google.registry.batch.AsyncTaskEnqueuer.QUEUE_ASYNC_HOST_RENAME;
|
||||
import static google.registry.batch.AsyncTaskMetrics.OperationType.DNS_REFRESH;
|
||||
import static google.registry.model.ofy.ObjectifyService.ofy;
|
||||
import static google.registry.testing.DatastoreHelper.createTld;
|
||||
import static google.registry.testing.DatastoreHelper.newDomainBase;
|
||||
import static google.registry.testing.DatastoreHelper.newHostResource;
|
||||
import static google.registry.testing.DatastoreHelper.persistActiveHost;
|
||||
import static google.registry.testing.DatastoreHelper.persistDeletedHost;
|
||||
import static google.registry.testing.DatastoreHelper.persistResource;
|
||||
import static google.registry.testing.TaskQueueHelper.assertDnsTasksEnqueued;
|
||||
import static google.registry.testing.TaskQueueHelper.assertNoDnsTasksEnqueued;
|
||||
import static google.registry.testing.TaskQueueHelper.assertNoTasksEnqueued;
|
||||
import static google.registry.testing.TaskQueueHelper.assertTasksEnqueued;
|
||||
import static google.registry.util.DateTimeUtils.START_OF_TIME;
|
||||
import static org.joda.time.Duration.millis;
|
||||
import static org.joda.time.Duration.standardDays;
|
||||
import static org.joda.time.Duration.standardHours;
|
||||
import static org.joda.time.Duration.standardSeconds;
|
||||
import static org.mockito.ArgumentMatchers.anyString;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.times;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.verifyNoMoreInteractions;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import com.googlecode.objectify.Key;
|
||||
import google.registry.batch.AsyncTaskMetrics.OperationResult;
|
||||
import google.registry.batch.RefreshDnsOnHostRenameAction.RefreshDnsOnHostRenameReducer;
|
||||
import google.registry.model.host.HostResource;
|
||||
import google.registry.model.server.Lock;
|
||||
import google.registry.testing.FakeClock;
|
||||
import google.registry.testing.FakeResponse;
|
||||
import google.registry.testing.FakeSleeper;
|
||||
import google.registry.testing.InjectRule;
|
||||
import google.registry.testing.TaskQueueHelper.TaskMatcher;
|
||||
import google.registry.testing.mapreduce.MapreduceTestCase;
|
||||
import google.registry.util.AppEngineServiceUtils;
|
||||
import google.registry.util.RequestStatusChecker;
|
||||
import google.registry.util.Retrier;
|
||||
import google.registry.util.Sleeper;
|
||||
import google.registry.util.SystemSleeper;
|
||||
import java.util.Optional;
|
||||
import org.joda.time.DateTime;
|
||||
import org.joda.time.Duration;
|
||||
import org.junit.Before;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.junit.runners.JUnit4;
|
||||
import org.mockito.Mock;
|
||||
|
||||
/** Unit tests for {@link RefreshDnsOnHostRenameAction}. */
|
||||
@RunWith(JUnit4.class)
|
||||
public class RefreshDnsOnHostRenameActionTest
|
||||
extends MapreduceTestCase<RefreshDnsOnHostRenameAction> {
|
||||
|
||||
@Rule public final InjectRule inject = new InjectRule();
|
||||
|
||||
private AsyncTaskEnqueuer enqueuer;
|
||||
private final FakeClock clock = new FakeClock(DateTime.parse("2015-01-15T11:22:33Z"));
|
||||
private final FakeResponse fakeResponse = new FakeResponse();
|
||||
@Mock private RequestStatusChecker requestStatusChecker;
|
||||
|
||||
@Before
|
||||
public void setup() {
|
||||
createTld("tld");
|
||||
enqueuer =
|
||||
new AsyncTaskEnqueuer(
|
||||
getQueue(QUEUE_ASYNC_ACTIONS),
|
||||
getQueue(QUEUE_ASYNC_DELETE),
|
||||
getQueue(QUEUE_ASYNC_HOST_RENAME),
|
||||
Duration.ZERO,
|
||||
mock(AppEngineServiceUtils.class),
|
||||
new Retrier(new FakeSleeper(clock), 1));
|
||||
AsyncTaskMetrics asyncTaskMetricsMock = mock(AsyncTaskMetrics.class);
|
||||
action = new RefreshDnsOnHostRenameAction();
|
||||
action.asyncTaskMetrics = asyncTaskMetricsMock;
|
||||
inject.setStaticField(
|
||||
RefreshDnsOnHostRenameReducer.class, "asyncTaskMetrics", asyncTaskMetricsMock);
|
||||
action.clock = clock;
|
||||
action.mrRunner = makeDefaultRunner();
|
||||
action.pullQueue = getQueue(QUEUE_ASYNC_HOST_RENAME);
|
||||
action.requestStatusChecker = requestStatusChecker;
|
||||
action.response = fakeResponse;
|
||||
action.retrier = new Retrier(new FakeSleeper(clock), 1);
|
||||
when(requestStatusChecker.getLogId()).thenReturn("requestId");
|
||||
when(requestStatusChecker.isRunning(anyString()))
|
||||
.thenThrow(new AssertionError("Should not be called"));
|
||||
}
|
||||
|
||||
private void runMapreduce() throws Exception {
|
||||
clock.advanceOneMilli();
|
||||
// Use hard sleeps to ensure that the tasks are enqueued properly and will be leased.
|
||||
Sleeper sleeper = new SystemSleeper();
|
||||
sleeper.sleep(millis(50));
|
||||
action.run();
|
||||
sleeper.sleep(millis(50));
|
||||
executeTasksUntilEmpty("mapreduce", clock);
|
||||
sleeper.sleep(millis(50));
|
||||
clock.advanceBy(standardSeconds(5));
|
||||
ofy().clearSessionCache();
|
||||
}
|
||||
|
||||
/** Kicks off, but does not run, the mapreduce tasks. Useful for testing validation/setup. */
|
||||
private void enqueueMapreduceOnly() {
|
||||
clock.advanceOneMilli();
|
||||
action.run();
|
||||
clock.advanceBy(standardSeconds(5));
|
||||
ofy().clearSessionCache();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSuccess_dnsUpdateEnqueued() throws Exception {
|
||||
HostResource host = persistActiveHost("ns1.example.tld");
|
||||
persistResource(newDomainBase("example.tld", host));
|
||||
persistResource(newDomainBase("otherexample.tld", host));
|
||||
persistResource(newDomainBase("untouched.tld", persistActiveHost("ns2.example.tld")));
|
||||
DateTime timeEnqueued = clock.nowUtc();
|
||||
enqueuer.enqueueAsyncDnsRefresh(host, timeEnqueued);
|
||||
runMapreduce();
|
||||
assertDnsTasksEnqueued("example.tld", "otherexample.tld");
|
||||
assertNoTasksEnqueued(QUEUE_ASYNC_HOST_RENAME);
|
||||
verify(action.asyncTaskMetrics).recordDnsRefreshBatchSize(1L);
|
||||
verify(action.asyncTaskMetrics)
|
||||
.recordAsyncFlowResult(DNS_REFRESH, OperationResult.SUCCESS, timeEnqueued);
|
||||
verifyNoMoreInteractions(action.asyncTaskMetrics);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSuccess_multipleHostsProcessedInBatch() throws Exception {
|
||||
HostResource host1 = persistActiveHost("ns1.example.tld");
|
||||
HostResource host2 = persistActiveHost("ns2.example.tld");
|
||||
HostResource host3 = persistActiveHost("ns3.example.tld");
|
||||
persistResource(newDomainBase("example1.tld", host1));
|
||||
persistResource(newDomainBase("example2.tld", host2));
|
||||
persistResource(newDomainBase("example3.tld", host3));
|
||||
DateTime timeEnqueued = clock.nowUtc();
|
||||
DateTime laterTimeEnqueued = timeEnqueued.plus(standardSeconds(10));
|
||||
enqueuer.enqueueAsyncDnsRefresh(host1, timeEnqueued);
|
||||
enqueuer.enqueueAsyncDnsRefresh(host2, timeEnqueued);
|
||||
enqueuer.enqueueAsyncDnsRefresh(host3, laterTimeEnqueued);
|
||||
runMapreduce();
|
||||
assertDnsTasksEnqueued("example1.tld", "example2.tld", "example3.tld");
|
||||
assertNoTasksEnqueued(QUEUE_ASYNC_HOST_RENAME);
|
||||
verify(action.asyncTaskMetrics).recordDnsRefreshBatchSize(3L);
|
||||
verify(action.asyncTaskMetrics, times(2))
|
||||
.recordAsyncFlowResult(DNS_REFRESH, OperationResult.SUCCESS, timeEnqueued);
|
||||
verify(action.asyncTaskMetrics)
|
||||
.recordAsyncFlowResult(DNS_REFRESH, OperationResult.SUCCESS, laterTimeEnqueued);
|
||||
verifyNoMoreInteractions(action.asyncTaskMetrics);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSuccess_deletedHost_doesntTriggerDnsRefresh() throws Exception {
|
||||
HostResource host = persistDeletedHost("ns11.fakesss.tld", clock.nowUtc().minusDays(4));
|
||||
persistResource(newDomainBase("example1.tld", host));
|
||||
DateTime timeEnqueued = clock.nowUtc();
|
||||
enqueuer.enqueueAsyncDnsRefresh(host, timeEnqueued);
|
||||
runMapreduce();
|
||||
assertNoDnsTasksEnqueued();
|
||||
assertNoTasksEnqueued(QUEUE_ASYNC_HOST_RENAME);
|
||||
verify(action.asyncTaskMetrics).recordDnsRefreshBatchSize(1L);
|
||||
verify(action.asyncTaskMetrics)
|
||||
.recordAsyncFlowResult(DNS_REFRESH, OperationResult.STALE, timeEnqueued);
|
||||
verifyNoMoreInteractions(action.asyncTaskMetrics);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSuccess_noDnsTasksForDeletedDomain() throws Exception {
|
||||
HostResource renamedHost = persistActiveHost("ns1.example.tld");
|
||||
persistResource(
|
||||
newDomainBase("example.tld", renamedHost)
|
||||
.asBuilder()
|
||||
.setDeletionTime(START_OF_TIME)
|
||||
.build());
|
||||
enqueuer.enqueueAsyncDnsRefresh(renamedHost, clock.nowUtc());
|
||||
runMapreduce();
|
||||
assertNoDnsTasksEnqueued();
|
||||
assertNoTasksEnqueued(QUEUE_ASYNC_HOST_RENAME);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRun_hostDoesntExist_delaysTask() throws Exception {
|
||||
HostResource host = newHostResource("ns1.example.tld");
|
||||
enqueuer.enqueueAsyncDnsRefresh(host, clock.nowUtc());
|
||||
enqueueMapreduceOnly();
|
||||
assertNoDnsTasksEnqueued();
|
||||
assertTasksEnqueued(
|
||||
QUEUE_ASYNC_HOST_RENAME,
|
||||
new TaskMatcher()
|
||||
.etaDelta(standardHours(23), standardHours(25))
|
||||
.param("hostKey", Key.create(host).getString()));
|
||||
assertThat(acquireLock()).isPresent();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test_cannotAcquireLock() {
|
||||
// Make lock acquisition fail.
|
||||
acquireLock();
|
||||
enqueueMapreduceOnly();
|
||||
assertThat(fakeResponse.getPayload()).isEqualTo("Can't acquire lock; aborting.");
|
||||
assertNoDnsTasksEnqueued();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test_mapreduceHasWorkToDo_lockIsAcquired() {
|
||||
HostResource host = persistActiveHost("ns1.example.tld");
|
||||
enqueuer.enqueueAsyncDnsRefresh(host, clock.nowUtc());
|
||||
enqueueMapreduceOnly();
|
||||
assertThat(acquireLock()).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test_noTasksToLease_releasesLockImmediately() throws Exception {
|
||||
enqueueMapreduceOnly();
|
||||
assertNoDnsTasksEnqueued();
|
||||
assertNoTasksEnqueued(QUEUE_ASYNC_HOST_RENAME);
|
||||
// If the Lock was correctly released, then we can acquire it now.
|
||||
assertThat(acquireLock()).isPresent();
|
||||
}
|
||||
|
||||
private Optional<Lock> acquireLock() {
|
||||
return Lock.acquire(
|
||||
RefreshDnsOnHostRenameAction.class.getSimpleName(),
|
||||
null,
|
||||
standardDays(30),
|
||||
requestStatusChecker,
|
||||
false);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
// Copyright 2017 The Nomulus Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package google.registry.batch;
|
||||
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
import static google.registry.model.ofy.ObjectifyService.ofy;
|
||||
import static google.registry.testing.DatastoreHelper.persistActiveContact;
|
||||
import static google.registry.testing.DatastoreHelper.persistContactWithPendingTransfer;
|
||||
import static org.joda.time.DateTimeZone.UTC;
|
||||
|
||||
import google.registry.model.contact.ContactResource;
|
||||
import google.registry.model.transfer.TransferStatus;
|
||||
import google.registry.testing.FakeResponse;
|
||||
import google.registry.testing.mapreduce.MapreduceTestCase;
|
||||
import org.joda.time.DateTime;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.junit.runners.JUnit4;
|
||||
|
||||
/** Unit tests for {@link ResaveAllEppResourcesAction}. */
|
||||
@RunWith(JUnit4.class)
|
||||
public class ResaveAllEppResourcesActionTest
|
||||
extends MapreduceTestCase<ResaveAllEppResourcesAction> {
|
||||
|
||||
@Before
|
||||
public void init() {
|
||||
action = new ResaveAllEppResourcesAction();
|
||||
action.mrRunner = makeDefaultRunner();
|
||||
action.response = new FakeResponse();
|
||||
}
|
||||
|
||||
private void runMapreduce() throws Exception {
|
||||
action.run();
|
||||
executeTasksUntilEmpty("mapreduce");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test_mapreduceSuccessfullyResavesEntity() throws Exception {
|
||||
ContactResource contact = persistActiveContact("test123");
|
||||
DateTime creationTime = contact.getUpdateAutoTimestamp().getTimestamp();
|
||||
assertThat(ofy().load().entity(contact).now().getUpdateAutoTimestamp().getTimestamp())
|
||||
.isEqualTo(creationTime);
|
||||
ofy().clearSessionCache();
|
||||
runMapreduce();
|
||||
assertThat(ofy().load().entity(contact).now().getUpdateAutoTimestamp().getTimestamp())
|
||||
.isGreaterThan(creationTime);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test_mapreduceResolvesPendingTransfer() throws Exception {
|
||||
DateTime now = DateTime.now(UTC);
|
||||
// Set up a contact with a transfer that implicitly completed five days ago.
|
||||
ContactResource contact =
|
||||
persistContactWithPendingTransfer(
|
||||
persistActiveContact("meh789"),
|
||||
now.minusDays(10),
|
||||
now.minusDays(10),
|
||||
now.minusDays(10));
|
||||
assertThat(contact.getTransferData().getTransferStatus()).isEqualTo(TransferStatus.PENDING);
|
||||
runMapreduce();
|
||||
|
||||
ofy().clearSessionCache();
|
||||
// The transfer should be effective after the contact is re-saved, as it should've been
|
||||
// projected to the current time.
|
||||
ContactResource resavedContact = ofy().load().entity(contact).now();
|
||||
assertThat(resavedContact.getTransferData().getTransferStatus())
|
||||
.isEqualTo(TransferStatus.SERVER_APPROVED);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,167 @@
|
||||
// Copyright 2018 The Nomulus Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package google.registry.batch;
|
||||
|
||||
import static com.google.appengine.api.taskqueue.QueueFactory.getQueue;
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
import static google.registry.batch.AsyncTaskEnqueuer.PARAM_REQUESTED_TIME;
|
||||
import static google.registry.batch.AsyncTaskEnqueuer.PARAM_RESOURCE_KEY;
|
||||
import static google.registry.batch.AsyncTaskEnqueuer.PATH_RESAVE_ENTITY;
|
||||
import static google.registry.batch.AsyncTaskEnqueuer.QUEUE_ASYNC_ACTIONS;
|
||||
import static google.registry.batch.AsyncTaskEnqueuer.QUEUE_ASYNC_DELETE;
|
||||
import static google.registry.batch.AsyncTaskEnqueuer.QUEUE_ASYNC_HOST_RENAME;
|
||||
import static google.registry.model.ofy.ObjectifyService.ofy;
|
||||
import static google.registry.testing.DatastoreHelper.createTld;
|
||||
import static google.registry.testing.DatastoreHelper.newDomainBase;
|
||||
import static google.registry.testing.DatastoreHelper.persistActiveContact;
|
||||
import static google.registry.testing.DatastoreHelper.persistDomainWithDependentResources;
|
||||
import static google.registry.testing.DatastoreHelper.persistDomainWithPendingTransfer;
|
||||
import static google.registry.testing.DatastoreHelper.persistResource;
|
||||
import static google.registry.testing.TaskQueueHelper.assertTasksEnqueued;
|
||||
import static org.joda.time.Duration.standardDays;
|
||||
import static org.joda.time.Duration.standardSeconds;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
import com.google.common.collect.ImmutableSortedSet;
|
||||
import com.googlecode.objectify.Key;
|
||||
import google.registry.model.ImmutableObject;
|
||||
import google.registry.model.domain.DomainBase;
|
||||
import google.registry.model.domain.GracePeriod;
|
||||
import google.registry.model.domain.rgp.GracePeriodStatus;
|
||||
import google.registry.model.eppcommon.StatusValue;
|
||||
import google.registry.model.ofy.Ofy;
|
||||
import google.registry.request.Response;
|
||||
import google.registry.testing.AppEngineRule;
|
||||
import google.registry.testing.FakeClock;
|
||||
import google.registry.testing.FakeSleeper;
|
||||
import google.registry.testing.InjectRule;
|
||||
import google.registry.testing.ShardableTestCase;
|
||||
import google.registry.testing.TaskQueueHelper.TaskMatcher;
|
||||
import google.registry.util.AppEngineServiceUtils;
|
||||
import google.registry.util.Retrier;
|
||||
import org.joda.time.DateTime;
|
||||
import org.joda.time.Duration;
|
||||
import org.junit.Before;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.junit.runners.JUnit4;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.MockitoJUnit;
|
||||
import org.mockito.junit.MockitoRule;
|
||||
|
||||
/** Unit tests for {@link ResaveEntityAction}. */
|
||||
@RunWith(JUnit4.class)
|
||||
public class ResaveEntityActionTest extends ShardableTestCase {
|
||||
|
||||
@Rule
|
||||
public final AppEngineRule appEngine =
|
||||
AppEngineRule.builder().withDatastore().withTaskQueue().build();
|
||||
|
||||
@Rule public final InjectRule inject = new InjectRule();
|
||||
@Rule public final MockitoRule mocks = MockitoJUnit.rule();
|
||||
|
||||
@Mock private AppEngineServiceUtils appEngineServiceUtils;
|
||||
@Mock private Response response;
|
||||
private final FakeClock clock = new FakeClock(DateTime.parse("2016-02-11T10:00:00Z"));
|
||||
private AsyncTaskEnqueuer asyncTaskEnqueuer;
|
||||
|
||||
@Before
|
||||
public void before() {
|
||||
inject.setStaticField(Ofy.class, "clock", clock);
|
||||
when(appEngineServiceUtils.getServiceHostname("backend")).thenReturn("backend.hostname.fake");
|
||||
asyncTaskEnqueuer =
|
||||
new AsyncTaskEnqueuer(
|
||||
getQueue(QUEUE_ASYNC_ACTIONS),
|
||||
getQueue(QUEUE_ASYNC_DELETE),
|
||||
getQueue(QUEUE_ASYNC_HOST_RENAME),
|
||||
Duration.ZERO,
|
||||
appEngineServiceUtils,
|
||||
new Retrier(new FakeSleeper(clock), 1));
|
||||
createTld("tld");
|
||||
}
|
||||
|
||||
private void runAction(
|
||||
Key<ImmutableObject> resourceKey,
|
||||
DateTime requestedTime,
|
||||
ImmutableSortedSet<DateTime> resaveTimes) {
|
||||
ResaveEntityAction action =
|
||||
new ResaveEntityAction(
|
||||
resourceKey, requestedTime, resaveTimes, asyncTaskEnqueuer, response);
|
||||
action.run();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test_domainPendingTransfer_isResavedAndTransferCompleted() {
|
||||
DomainBase domain =
|
||||
persistDomainWithPendingTransfer(
|
||||
persistDomainWithDependentResources(
|
||||
"domain",
|
||||
"tld",
|
||||
persistActiveContact("jd1234"),
|
||||
DateTime.parse("2016-02-06T10:00:00Z"),
|
||||
DateTime.parse("2016-02-06T10:00:00Z"),
|
||||
DateTime.parse("2017-01-02T10:11:00Z")),
|
||||
DateTime.parse("2016-02-06T10:00:00Z"),
|
||||
DateTime.parse("2016-02-11T10:00:00Z"),
|
||||
DateTime.parse("2017-01-02T10:11:00Z"),
|
||||
DateTime.parse("2016-02-06T10:00:00Z"));
|
||||
clock.advanceOneMilli();
|
||||
assertThat(domain.getCurrentSponsorClientId()).isEqualTo("TheRegistrar");
|
||||
runAction(Key.create(domain), DateTime.parse("2016-02-06T10:00:01Z"), ImmutableSortedSet.of());
|
||||
DomainBase resavedDomain = ofy().load().entity(domain).now();
|
||||
assertThat(resavedDomain.getCurrentSponsorClientId()).isEqualTo("NewRegistrar");
|
||||
verify(response).setPayload("Entity re-saved.");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test_domainPendingDeletion_isResavedAndReenqueued() {
|
||||
DomainBase domain =
|
||||
persistResource(
|
||||
newDomainBase("domain.tld")
|
||||
.asBuilder()
|
||||
.setDeletionTime(clock.nowUtc().plusDays(35))
|
||||
.setStatusValues(ImmutableSet.of(StatusValue.PENDING_DELETE))
|
||||
.setGracePeriods(
|
||||
ImmutableSet.of(
|
||||
GracePeriod.createWithoutBillingEvent(
|
||||
GracePeriodStatus.REDEMPTION,
|
||||
clock.nowUtc().plusDays(30),
|
||||
"TheRegistrar")))
|
||||
.build());
|
||||
clock.advanceBy(standardDays(30));
|
||||
DateTime requestedTime = clock.nowUtc();
|
||||
|
||||
assertThat(domain.getGracePeriods()).isNotEmpty();
|
||||
runAction(Key.create(domain), requestedTime, ImmutableSortedSet.of(requestedTime.plusDays(5)));
|
||||
DomainBase resavedDomain = ofy().load().entity(domain).now();
|
||||
assertThat(resavedDomain.getGracePeriods()).isEmpty();
|
||||
|
||||
assertTasksEnqueued(
|
||||
QUEUE_ASYNC_ACTIONS,
|
||||
new TaskMatcher()
|
||||
.url(PATH_RESAVE_ENTITY)
|
||||
.method("POST")
|
||||
.header("Host", "backend.hostname.fake")
|
||||
.header("content-type", "application/x-www-form-urlencoded")
|
||||
.param(PARAM_RESOURCE_KEY, Key.create(resavedDomain).getString())
|
||||
.param(PARAM_REQUESTED_TIME, requestedTime.toString())
|
||||
.etaDelta(
|
||||
standardDays(5).minus(standardSeconds(30)),
|
||||
standardDays(5).plus(standardSeconds(30))));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
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 = "beam",
|
||||
srcs = glob(["*.java"]),
|
||||
deps = [
|
||||
"//java/google/registry/beam",
|
||||
"//javatests/google/registry/testing",
|
||||
"@com_google_dagger",
|
||||
"@com_google_guava",
|
||||
"@com_google_truth",
|
||||
"@com_google_truth_extensions_truth_java8_extension",
|
||||
"@junit",
|
||||
"@org_apache_avro",
|
||||
"@org_apache_beam_runners_direct_java",
|
||||
"@org_apache_beam_runners_google_cloud_dataflow_java",
|
||||
"@org_apache_beam_sdks_java_core",
|
||||
"@org_apache_beam_sdks_java_io_google_cloud_platform",
|
||||
"@org_mockito_core",
|
||||
],
|
||||
)
|
||||
|
||||
GenTestRules(
|
||||
name = "GeneratedTestRules",
|
||||
default_test_size = "small",
|
||||
test_files = glob(["*Test.java"]),
|
||||
deps = [":beam"],
|
||||
)
|
||||
@@ -0,0 +1,86 @@
|
||||
// Copyright 2018 The Nomulus Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package google.registry.beam;
|
||||
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
import static google.registry.testing.JUnitBackports.assertThrows;
|
||||
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import org.apache.avro.Schema;
|
||||
import org.apache.avro.generic.GenericData;
|
||||
import org.apache.avro.generic.GenericRecord;
|
||||
import org.apache.beam.sdk.io.gcp.bigquery.SchemaAndRecord;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.junit.runners.JUnit4;
|
||||
|
||||
/** Unit tests for {@link BeamUtils} */
|
||||
@RunWith(JUnit4.class)
|
||||
public class BeamUtilsTest {
|
||||
|
||||
private static final String GENERIC_SCHEMA =
|
||||
"{\"name\": \"AnObject\", "
|
||||
+ "\"type\": \"record\", "
|
||||
+ "\"fields\": ["
|
||||
+ "{\"name\": \"aString\", \"type\": \"string\"},"
|
||||
+ "{\"name\": \"aFloat\", \"type\": \"float\"}"
|
||||
+ "]}";
|
||||
|
||||
private SchemaAndRecord schemaAndRecord;
|
||||
|
||||
@Before
|
||||
public void initializeRecord() {
|
||||
// Create a record with a given JSON schema.
|
||||
GenericRecord record = new GenericData.Record(new Schema.Parser().parse(GENERIC_SCHEMA));
|
||||
record.put("aString", "hello world");
|
||||
record.put("aFloat", 2.54);
|
||||
schemaAndRecord = new SchemaAndRecord(record, null);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testExtractField_fieldExists_returnsExpectedStringValues() {
|
||||
assertThat(BeamUtils.extractField(schemaAndRecord.getRecord(), "aString"))
|
||||
.isEqualTo("hello world");
|
||||
assertThat(BeamUtils.extractField(schemaAndRecord.getRecord(), "aFloat")).isEqualTo("2.54");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testExtractField_fieldDoesntExist_returnsNull() {
|
||||
schemaAndRecord.getRecord().put("aFloat", null);
|
||||
assertThat(BeamUtils.extractField(schemaAndRecord.getRecord(), "aFloat")).isEqualTo("null");
|
||||
assertThat(BeamUtils.extractField(schemaAndRecord.getRecord(), "missing")).isEqualTo("null");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCheckFieldsNotNull_noExceptionIfAllPresent() {
|
||||
BeamUtils.checkFieldsNotNull(ImmutableList.of("aString", "aFloat"), schemaAndRecord);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCheckFieldsNotNull_fieldMissing_throwsException() {
|
||||
IllegalStateException expected =
|
||||
assertThrows(
|
||||
IllegalStateException.class,
|
||||
() ->
|
||||
BeamUtils.checkFieldsNotNull(
|
||||
ImmutableList.of("aString", "aFloat", "notAField"), schemaAndRecord));
|
||||
assertThat(expected)
|
||||
.hasMessageThat()
|
||||
.isEqualTo(
|
||||
"Read unexpected null value for field(s) notAField for record "
|
||||
+ "{\"aString\": \"hello world\", \"aFloat\": 2.54}");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
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 = "invoicing",
|
||||
srcs = glob(["*.java"]),
|
||||
resources = glob(["testdata/*"]),
|
||||
deps = [
|
||||
"//java/google/registry/beam/invoicing",
|
||||
"//java/google/registry/util",
|
||||
"//javatests/google/registry/testing",
|
||||
"@com_google_apis_google_api_services_bigquery",
|
||||
"@com_google_dagger",
|
||||
"@com_google_guava",
|
||||
"@com_google_truth",
|
||||
"@com_google_truth_extensions_truth_java8_extension",
|
||||
"@junit",
|
||||
"@org_apache_avro",
|
||||
"@org_apache_beam_runners_direct_java",
|
||||
"@org_apache_beam_runners_google_cloud_dataflow_java",
|
||||
"@org_apache_beam_sdks_java_core",
|
||||
"@org_apache_beam_sdks_java_io_google_cloud_platform",
|
||||
"@org_mockito_core",
|
||||
],
|
||||
)
|
||||
|
||||
GenTestRules(
|
||||
name = "GeneratedTestRules",
|
||||
default_test_size = "small",
|
||||
medium_tests = ["InvoicingPipelineTest.java"],
|
||||
test_files = glob(["*Test.java"]),
|
||||
deps = [":invoicing"],
|
||||
)
|
||||
@@ -0,0 +1,205 @@
|
||||
// Copyright 2018 The Nomulus Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package google.registry.beam.invoicing;
|
||||
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
import static google.registry.testing.JUnitBackports.assertThrows;
|
||||
|
||||
import google.registry.beam.invoicing.BillingEvent.InvoiceGroupingKey;
|
||||
import google.registry.beam.invoicing.BillingEvent.InvoiceGroupingKey.InvoiceGroupingKeyCoder;
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.time.ZoneId;
|
||||
import java.time.ZonedDateTime;
|
||||
import org.apache.avro.Schema;
|
||||
import org.apache.avro.generic.GenericData;
|
||||
import org.apache.avro.generic.GenericRecord;
|
||||
import org.apache.beam.sdk.io.gcp.bigquery.SchemaAndRecord;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.junit.runners.JUnit4;
|
||||
|
||||
/** Unit tests for {@link BillingEvent} */
|
||||
@RunWith(JUnit4.class)
|
||||
public class BillingEventTest {
|
||||
|
||||
private static final String BILLING_EVENT_SCHEMA =
|
||||
"{\"name\": \"BillingEvent\", "
|
||||
+ "\"type\": \"record\", "
|
||||
+ "\"fields\": ["
|
||||
+ "{\"name\": \"id\", \"type\": \"long\"},"
|
||||
+ "{\"name\": \"billingTime\", \"type\": \"string\"},"
|
||||
+ "{\"name\": \"eventTime\", \"type\": \"string\"},"
|
||||
+ "{\"name\": \"registrarId\", \"type\": \"string\"},"
|
||||
+ "{\"name\": \"billingId\", \"type\": \"long\"},"
|
||||
+ "{\"name\": \"poNumber\", \"type\": \"string\"},"
|
||||
+ "{\"name\": \"tld\", \"type\": \"string\"},"
|
||||
+ "{\"name\": \"action\", \"type\": \"string\"},"
|
||||
+ "{\"name\": \"domain\", \"type\": \"string\"},"
|
||||
+ "{\"name\": \"repositoryId\", \"type\": \"string\"},"
|
||||
+ "{\"name\": \"years\", \"type\": \"int\"},"
|
||||
+ "{\"name\": \"currency\", \"type\": \"string\"},"
|
||||
+ "{\"name\": \"amount\", \"type\": \"float\"},"
|
||||
+ "{\"name\": \"flags\", \"type\": \"string\"}"
|
||||
+ "]}";
|
||||
|
||||
private SchemaAndRecord schemaAndRecord;
|
||||
|
||||
@Before
|
||||
public void initializeRecord() {
|
||||
// Create a record with a given JSON schema.
|
||||
schemaAndRecord = new SchemaAndRecord(createRecord(), null);
|
||||
}
|
||||
|
||||
private GenericRecord createRecord() {
|
||||
GenericRecord record = new GenericData.Record(new Schema.Parser().parse(BILLING_EVENT_SCHEMA));
|
||||
record.put("id", "1");
|
||||
record.put("billingTime", 1508835963000000L);
|
||||
record.put("eventTime", 1484870383000000L);
|
||||
record.put("registrarId", "myRegistrar");
|
||||
record.put("billingId", "12345-CRRHELLO");
|
||||
record.put("poNumber", "");
|
||||
record.put("tld", "test");
|
||||
record.put("action", "RENEW");
|
||||
record.put("domain", "example.test");
|
||||
record.put("repositoryId", "123456");
|
||||
record.put("years", 5);
|
||||
record.put("currency", "USD");
|
||||
record.put("amount", 20.5);
|
||||
record.put("flags", "AUTO_RENEW SYNTHETIC");
|
||||
return record;
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testParseBillingEventFromRecord_success() {
|
||||
BillingEvent event = BillingEvent.parseFromRecord(schemaAndRecord);
|
||||
assertThat(event.id()).isEqualTo(1);
|
||||
assertThat(event.billingTime())
|
||||
.isEqualTo(ZonedDateTime.of(2017, 10, 24, 9, 6, 3, 0, ZoneId.of("UTC")));
|
||||
assertThat(event.eventTime())
|
||||
.isEqualTo(ZonedDateTime.of(2017, 1, 19, 23, 59, 43, 0, ZoneId.of("UTC")));
|
||||
assertThat(event.registrarId()).isEqualTo("myRegistrar");
|
||||
assertThat(event.billingId()).isEqualTo("12345-CRRHELLO");
|
||||
assertThat(event.poNumber()).isEmpty();
|
||||
assertThat(event.tld()).isEqualTo("test");
|
||||
assertThat(event.action()).isEqualTo("RENEW");
|
||||
assertThat(event.domain()).isEqualTo("example.test");
|
||||
assertThat(event.repositoryId()).isEqualTo("123456");
|
||||
assertThat(event.years()).isEqualTo(5);
|
||||
assertThat(event.currency()).isEqualTo("USD");
|
||||
assertThat(event.amount()).isEqualTo(20.5);
|
||||
assertThat(event.flags()).isEqualTo("AUTO_RENEW SYNTHETIC");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testParseBillingEventFromRecord_sunriseCreate_reducedPrice_success() {
|
||||
schemaAndRecord.getRecord().put("flags", "SUNRISE");
|
||||
BillingEvent event = BillingEvent.parseFromRecord(schemaAndRecord);
|
||||
assertThat(event.amount()).isEqualTo(17.43);
|
||||
assertThat(event.flags()).isEqualTo("SUNRISE");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testParseBillingEventFromRecord_anchorTenant_zeroPrice_success() {
|
||||
schemaAndRecord.getRecord().put("flags", "SUNRISE ANCHOR_TENANT");
|
||||
BillingEvent event = BillingEvent.parseFromRecord(schemaAndRecord);
|
||||
assertThat(event.amount()).isZero();
|
||||
assertThat(event.flags()).isEqualTo("SUNRISE ANCHOR_TENANT");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testParseBillingEventFromRecord_nullValue_throwsException() {
|
||||
schemaAndRecord.getRecord().put("tld", null);
|
||||
assertThrows(IllegalStateException.class, () -> BillingEvent.parseFromRecord(schemaAndRecord));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testConvertBillingEvent_toCsv() {
|
||||
BillingEvent event = BillingEvent.parseFromRecord(schemaAndRecord);
|
||||
assertThat(event.toCsv())
|
||||
.isEqualTo("1,2017-10-24 09:06:03 UTC,2017-01-19 23:59:43 UTC,myRegistrar,"
|
||||
+ "12345-CRRHELLO,test,RENEW,example.test,123456,5,USD,20.50,AUTO_RENEW");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGenerateBillingEventFilename() {
|
||||
BillingEvent event = BillingEvent.parseFromRecord(schemaAndRecord);
|
||||
assertThat(event.toFilename("2017-10")).isEqualTo("invoice_details_2017-10_myRegistrar_test");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetInvoiceGroupingKey_fromBillingEvent() {
|
||||
BillingEvent event = BillingEvent.parseFromRecord(schemaAndRecord);
|
||||
InvoiceGroupingKey invoiceKey = event.getInvoiceGroupingKey();
|
||||
assertThat(invoiceKey.startDate()).isEqualTo("2017-10-01");
|
||||
assertThat(invoiceKey.endDate()).isEqualTo("2022-09-30");
|
||||
assertThat(invoiceKey.productAccountKey()).isEqualTo("12345-CRRHELLO");
|
||||
assertThat(invoiceKey.usageGroupingKey()).isEqualTo("myRegistrar - test");
|
||||
assertThat(invoiceKey.description()).isEqualTo("RENEW | TLD: test | TERM: 5-year");
|
||||
assertThat(invoiceKey.unitPrice()).isEqualTo(20.5);
|
||||
assertThat(invoiceKey.unitPriceCurrency()).isEqualTo("USD");
|
||||
assertThat(invoiceKey.poNumber()).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test_nonNullPoNumber() {
|
||||
GenericRecord record = createRecord();
|
||||
record.put("poNumber", "905610");
|
||||
BillingEvent event = BillingEvent.parseFromRecord(new SchemaAndRecord(record, null));
|
||||
assertThat(event.poNumber()).isEqualTo("905610");
|
||||
InvoiceGroupingKey invoiceKey = event.getInvoiceGroupingKey();
|
||||
assertThat(invoiceKey.poNumber()).isEqualTo("905610");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testConvertInvoiceGroupingKey_toCsv() {
|
||||
BillingEvent event = BillingEvent.parseFromRecord(schemaAndRecord);
|
||||
InvoiceGroupingKey invoiceKey = event.getInvoiceGroupingKey();
|
||||
assertThat(invoiceKey.toCsv(3L))
|
||||
.isEqualTo(
|
||||
"2017-10-01,2022-09-30,12345-CRRHELLO,61.50,USD,10125,1,PURCHASE,"
|
||||
+ "myRegistrar - test,3,RENEW | TLD: test | TERM: 5-year,20.50,USD,");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testInvoiceGroupingKeyCoder_deterministicSerialization() throws IOException {
|
||||
InvoiceGroupingKey invoiceKey =
|
||||
BillingEvent.parseFromRecord(schemaAndRecord).getInvoiceGroupingKey();
|
||||
InvoiceGroupingKeyCoder coder = new InvoiceGroupingKeyCoder();
|
||||
ByteArrayOutputStream outStream = new ByteArrayOutputStream();
|
||||
coder.encode(invoiceKey, outStream);
|
||||
InputStream inStream = new ByteArrayInputStream(outStream.toByteArray());
|
||||
assertThat(coder.decode(inStream)).isEqualTo(invoiceKey);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetDetailReportHeader() {
|
||||
assertThat(BillingEvent.getHeader())
|
||||
.isEqualTo(
|
||||
"id,billingTime,eventTime,registrarId,billingId,poNumber,tld,action,"
|
||||
+ "domain,repositoryId,years,currency,amount,flags");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetOverallInvoiceHeader() {
|
||||
assertThat(InvoiceGroupingKey.invoiceHeader())
|
||||
.isEqualTo("StartDate,EndDate,ProductAccountKey,Amount,AmountCurrency,BillingProductCode,"
|
||||
+ "SalesChannel,LineItemType,UsageGroupingKey,Quantity,Description,UnitPrice,"
|
||||
+ "UnitPriceCurrency,PONumber");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,219 @@
|
||||
// Copyright 2018 The Nomulus Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package google.registry.beam.invoicing;
|
||||
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.google.common.collect.ImmutableMap;
|
||||
import google.registry.util.ResourceUtils;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.time.ZoneId;
|
||||
import java.time.ZonedDateTime;
|
||||
import java.util.Map.Entry;
|
||||
import org.apache.beam.runners.direct.DirectRunner;
|
||||
import org.apache.beam.sdk.options.PipelineOptions;
|
||||
import org.apache.beam.sdk.options.PipelineOptionsFactory;
|
||||
import org.apache.beam.sdk.options.ValueProvider.StaticValueProvider;
|
||||
import org.apache.beam.sdk.testing.TestPipeline;
|
||||
import org.apache.beam.sdk.transforms.Create;
|
||||
import org.apache.beam.sdk.values.PCollection;
|
||||
import org.junit.Before;
|
||||
import org.junit.BeforeClass;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.junit.rules.TemporaryFolder;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.junit.runners.JUnit4;
|
||||
|
||||
/** Unit tests for {@link InvoicingPipeline}. */
|
||||
@RunWith(JUnit4.class)
|
||||
public class InvoicingPipelineTest {
|
||||
|
||||
private static PipelineOptions pipelineOptions;
|
||||
|
||||
@BeforeClass
|
||||
public static void initializePipelineOptions() {
|
||||
pipelineOptions = PipelineOptionsFactory.create();
|
||||
pipelineOptions.setRunner(DirectRunner.class);
|
||||
}
|
||||
|
||||
@Rule public final transient TestPipeline p = TestPipeline.fromOptions(pipelineOptions);
|
||||
@Rule public final TemporaryFolder tempFolder = new TemporaryFolder();
|
||||
|
||||
private InvoicingPipeline invoicingPipeline;
|
||||
|
||||
@Before
|
||||
public void initializePipeline() throws IOException {
|
||||
invoicingPipeline = new InvoicingPipeline();
|
||||
invoicingPipeline.projectId = "test-project";
|
||||
File beamTempFolder = tempFolder.newFolder();
|
||||
invoicingPipeline.beamBucketUrl = beamTempFolder.getAbsolutePath();
|
||||
invoicingPipeline.invoiceFilePrefix = "REG-INV";
|
||||
invoicingPipeline.beamStagingUrl = beamTempFolder.getAbsolutePath() + "/staging";
|
||||
invoicingPipeline.invoiceTemplateUrl =
|
||||
beamTempFolder.getAbsolutePath() + "/templates/invoicing";
|
||||
invoicingPipeline.billingBucketUrl = tempFolder.getRoot().getAbsolutePath();
|
||||
}
|
||||
|
||||
private ImmutableList<BillingEvent> getInputEvents() {
|
||||
return ImmutableList.of(
|
||||
BillingEvent.create(
|
||||
1,
|
||||
ZonedDateTime.of(2017, 10, 4, 0, 0, 0, 0, ZoneId.of("UTC")),
|
||||
ZonedDateTime.of(2017, 10, 4, 0, 0, 0, 0, ZoneId.of("UTC")),
|
||||
"theRegistrar",
|
||||
"234",
|
||||
"",
|
||||
"test",
|
||||
"RENEW",
|
||||
"mydomain.test",
|
||||
"REPO-ID",
|
||||
3,
|
||||
"USD",
|
||||
20.5,
|
||||
""),
|
||||
BillingEvent.create(
|
||||
1,
|
||||
ZonedDateTime.of(2017, 10, 4, 0, 0, 0, 0, ZoneId.of("UTC")),
|
||||
ZonedDateTime.of(2017, 10, 4, 0, 0, 0, 0, ZoneId.of("UTC")),
|
||||
"theRegistrar",
|
||||
"234",
|
||||
"",
|
||||
"test",
|
||||
"RENEW",
|
||||
"mydomain2.test",
|
||||
"REPO-ID",
|
||||
3,
|
||||
"USD",
|
||||
20.5,
|
||||
""),
|
||||
BillingEvent.create(
|
||||
1,
|
||||
ZonedDateTime.of(2017, 10, 2, 0, 0, 0, 0, ZoneId.of("UTC")),
|
||||
ZonedDateTime.of(2017, 9, 29, 0, 0, 0, 0, ZoneId.of("UTC")),
|
||||
"theRegistrar",
|
||||
"234",
|
||||
"",
|
||||
"hello",
|
||||
"CREATE",
|
||||
"mydomain3.hello",
|
||||
"REPO-ID",
|
||||
5,
|
||||
"JPY",
|
||||
70.75,
|
||||
""),
|
||||
BillingEvent.create(
|
||||
1,
|
||||
ZonedDateTime.of(2017, 10, 4, 0, 0, 0, 0, ZoneId.of("UTC")),
|
||||
ZonedDateTime.of(2017, 10, 4, 0, 0, 0, 0, ZoneId.of("UTC")),
|
||||
"bestdomains",
|
||||
"456",
|
||||
"116688",
|
||||
"test",
|
||||
"RENEW",
|
||||
"mydomain4.test",
|
||||
"REPO-ID",
|
||||
1,
|
||||
"USD",
|
||||
20.5,
|
||||
""),
|
||||
BillingEvent.create(
|
||||
1,
|
||||
ZonedDateTime.of(2017, 10, 4, 0, 0, 0, 0, ZoneId.of("UTC")),
|
||||
ZonedDateTime.of(2017, 10, 4, 0, 0, 0, 0, ZoneId.of("UTC")),
|
||||
"anotherRegistrar",
|
||||
"789",
|
||||
"",
|
||||
"test",
|
||||
"CREATE",
|
||||
"mydomain5.test",
|
||||
"REPO-ID",
|
||||
1,
|
||||
"USD",
|
||||
0,
|
||||
"SUNRISE ANCHOR_TENANT"));
|
||||
}
|
||||
|
||||
/** Returns a map from filename to expected contents for detail reports. */
|
||||
private ImmutableMap<String, ImmutableList<String>> getExpectedDetailReportMap() {
|
||||
return ImmutableMap.of(
|
||||
"invoice_details_2017-10_theRegistrar_test.csv",
|
||||
ImmutableList.of(
|
||||
"1,2017-10-04 00:00:00 UTC,2017-10-04 00:00:00 UTC,theRegistrar,234,"
|
||||
+ "test,RENEW,mydomain2.test,REPO-ID,3,USD,20.50,",
|
||||
"1,2017-10-04 00:00:00 UTC,2017-10-04 00:00:00 UTC,theRegistrar,234,"
|
||||
+ "test,RENEW,mydomain.test,REPO-ID,3,USD,20.50,"),
|
||||
"invoice_details_2017-10_theRegistrar_hello.csv",
|
||||
ImmutableList.of(
|
||||
"1,2017-10-02 00:00:00 UTC,2017-09-29 00:00:00 UTC,theRegistrar,234,"
|
||||
+ "hello,CREATE,mydomain3.hello,REPO-ID,5,JPY,70.75,"),
|
||||
"invoice_details_2017-10_bestdomains_test.csv",
|
||||
ImmutableList.of(
|
||||
"1,2017-10-04 00:00:00 UTC,2017-10-04 00:00:00 UTC,bestdomains,456,"
|
||||
+ "test,RENEW,mydomain4.test,REPO-ID,1,USD,20.50,"),
|
||||
"invoice_details_2017-10_anotherRegistrar_test.csv",
|
||||
ImmutableList.of(
|
||||
"1,2017-10-04 00:00:00 UTC,2017-10-04 00:00:00 UTC,anotherRegistrar,789,"
|
||||
+ "test,CREATE,mydomain5.test,REPO-ID,1,USD,0.00,SUNRISE ANCHOR_TENANT"));
|
||||
}
|
||||
|
||||
private ImmutableList<String> getExpectedInvoiceOutput() {
|
||||
return ImmutableList.of(
|
||||
"2017-10-01,2020-09-30,234,41.00,USD,10125,1,PURCHASE,theRegistrar - test,2,"
|
||||
+ "RENEW | TLD: test | TERM: 3-year,20.50,USD,",
|
||||
"2017-10-01,2022-09-30,234,70.75,JPY,10125,1,PURCHASE,theRegistrar - hello,1,"
|
||||
+ "CREATE | TLD: hello | TERM: 5-year,70.75,JPY,",
|
||||
"2017-10-01,2018-09-30,456,20.50,USD,10125,1,PURCHASE,bestdomains - test,1,"
|
||||
+ "RENEW | TLD: test | TERM: 1-year,20.50,USD,116688");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testEndToEndPipeline_generatesExpectedFiles() throws Exception {
|
||||
ImmutableList<BillingEvent> inputRows = getInputEvents();
|
||||
PCollection<BillingEvent> input = p.apply(Create.of(inputRows));
|
||||
invoicingPipeline.applyTerminalTransforms(input, StaticValueProvider.of("2017-10"));
|
||||
p.run();
|
||||
|
||||
for (Entry<String, ImmutableList<String>> entry : getExpectedDetailReportMap().entrySet()) {
|
||||
ImmutableList<String> detailReport = resultFileContents(entry.getKey());
|
||||
assertThat(detailReport.get(0))
|
||||
.isEqualTo("id,billingTime,eventTime,registrarId,billingId,poNumber,tld,action,"
|
||||
+ "domain,repositoryId,years,currency,amount,flags");
|
||||
assertThat(detailReport.subList(1, detailReport.size()))
|
||||
.containsExactlyElementsIn(entry.getValue());
|
||||
}
|
||||
|
||||
ImmutableList<String> overallInvoice = resultFileContents("REG-INV-2017-10.csv");
|
||||
assertThat(overallInvoice.get(0))
|
||||
.isEqualTo(
|
||||
"StartDate,EndDate,ProductAccountKey,Amount,AmountCurrency,BillingProductCode,"
|
||||
+ "SalesChannel,LineItemType,UsageGroupingKey,Quantity,Description,UnitPrice,"
|
||||
+ "UnitPriceCurrency,PONumber");
|
||||
assertThat(overallInvoice.subList(1, overallInvoice.size()))
|
||||
.containsExactlyElementsIn(getExpectedInvoiceOutput());
|
||||
}
|
||||
|
||||
/** Returns the text contents of a file under the beamBucket/results directory. */
|
||||
private ImmutableList<String> resultFileContents(String filename) throws Exception {
|
||||
File resultFile =
|
||||
new File(
|
||||
String.format(
|
||||
"%s/invoices/2017-10/%s", tempFolder.getRoot().getAbsolutePath(), filename));
|
||||
return ImmutableList.copyOf(
|
||||
ResourceUtils.readResourceUtf8(resultFile.toURI().toURL()).split("\n"));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
// Copyright 2018 The Nomulus Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package google.registry.beam.invoicing;
|
||||
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import google.registry.testing.TestDataHelper;
|
||||
import org.apache.beam.sdk.io.DefaultFilenamePolicy.Params;
|
||||
import org.apache.beam.sdk.io.FileBasedSink;
|
||||
import org.apache.beam.sdk.options.ValueProvider;
|
||||
import org.apache.beam.sdk.options.ValueProvider.StaticValueProvider;
|
||||
import org.apache.beam.sdk.transforms.SerializableFunction;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.junit.runners.JUnit4;
|
||||
|
||||
/** Unit tests for {@link InvoicingUtils}. */
|
||||
@RunWith(JUnit4.class)
|
||||
public class InvoicingUtilsTest {
|
||||
|
||||
@Test
|
||||
public void testDestinationFunction_generatesProperFileParams() {
|
||||
SerializableFunction<BillingEvent, Params> destinationFunction =
|
||||
InvoicingUtils.makeDestinationFunction("my/directory", StaticValueProvider.of("2017-10"));
|
||||
|
||||
BillingEvent billingEvent = mock(BillingEvent.class);
|
||||
// We mock BillingEvent to make the test independent of the implementation of toFilename()
|
||||
when(billingEvent.toFilename(any())).thenReturn("invoice_details_2017-10_registrar_tld");
|
||||
|
||||
assertThat(destinationFunction.apply(billingEvent))
|
||||
.isEqualTo(
|
||||
new Params()
|
||||
.withShardTemplate("")
|
||||
.withSuffix(".csv")
|
||||
.withBaseFilename(
|
||||
FileBasedSink.convertToFileResourceIfPossible(
|
||||
"my/directory/2017-10/invoice_details_2017-10_registrar_tld")));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testEmptyDestinationParams() {
|
||||
assertThat(InvoicingUtils.makeEmptyDestinationParams("my/directory"))
|
||||
.isEqualTo(
|
||||
new Params()
|
||||
.withBaseFilename(
|
||||
FileBasedSink.convertToFileResourceIfPossible("my/directory/FAILURES")));
|
||||
}
|
||||
|
||||
/** Asserts that the instantiated sql template matches a golden expected file. */
|
||||
@Test
|
||||
public void testMakeQueryProvider() {
|
||||
ValueProvider<String> queryProvider =
|
||||
InvoicingUtils.makeQueryProvider(StaticValueProvider.of("2017-10"), "my-project-id");
|
||||
assertThat(queryProvider.get()).isEqualTo(loadFile("billing_events_test.sql"));
|
||||
}
|
||||
|
||||
/** Returns a {@link String} from a file in the {@code billing/testdata/} directory. */
|
||||
private static String loadFile(String filename) {
|
||||
return TestDataHelper.loadFile(InvoicingUtilsTest.class, filename);
|
||||
}
|
||||
}
|
||||
+102
@@ -0,0 +1,102 @@
|
||||
#standardSQL
|
||||
-- Copyright 2017 The Nomulus Authors. All Rights Reserved.
|
||||
--
|
||||
-- Licensed under the Apache License, Version 2.0 (the "License");
|
||||
-- you may not use this file except in compliance with the License.
|
||||
-- You may obtain a copy of the License at
|
||||
--
|
||||
-- http://www.apache.org/licenses/LICENSE-2.0
|
||||
--
|
||||
-- Unless required by applicable law or agreed to in writing, software
|
||||
-- distributed under the License is distributed on an "AS IS" BASIS,
|
||||
-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
-- See the License for the specific language governing permissions and
|
||||
-- limitations under the License.
|
||||
|
||||
-- This query gathers all non-canceled billing events for a given
|
||||
-- YEAR_MONTH in yyyy-MM format.
|
||||
|
||||
SELECT
|
||||
__key__.id AS id,
|
||||
billingTime,
|
||||
eventTime,
|
||||
BillingEvent.clientId AS registrarId,
|
||||
RegistrarData.accountId AS billingId,
|
||||
RegistrarData.poNumber AS poNumber,
|
||||
tld,
|
||||
reason as action,
|
||||
targetId as domain,
|
||||
BillingEvent.domainRepoId as repositoryId,
|
||||
periodYears as years,
|
||||
BillingEvent.currency AS currency,
|
||||
BillingEvent.amount as amount,
|
||||
-- We'll strip out non-useful flags downstream
|
||||
ARRAY_TO_STRING(flags, " ") AS flags
|
||||
FROM (
|
||||
SELECT
|
||||
*,
|
||||
-- We store cost as "CURRENCY AMOUNT" such as "JPY 800" or "USD 20.00"
|
||||
SPLIT(cost, ' ')[OFFSET(0)] AS currency,
|
||||
SPLIT(cost, ' ')[OFFSET(1)] AS amount,
|
||||
-- Extract everything after the first dot in the domain as the TLD
|
||||
REGEXP_EXTRACT(targetId, r'[.](.+)') AS tld,
|
||||
-- __key__.path looks like '"DomainBase", "<repoId>", ...'
|
||||
REGEXP_REPLACE(SPLIT(__key__.path, ', ')[OFFSET(1)], '"', '')
|
||||
AS domainRepoId,
|
||||
COALESCE(cancellationMatchingBillingEvent.path,
|
||||
__key__.path) AS cancellationMatchingPath
|
||||
FROM
|
||||
`my-project-id.latest_datastore_export.OneTime`
|
||||
-- Only include real TLDs (filter prober data)
|
||||
WHERE
|
||||
REGEXP_EXTRACT(targetId, r'[.](.+)') IN (
|
||||
SELECT
|
||||
tldStr
|
||||
FROM
|
||||
`my-project-id.latest_datastore_export.Registry`
|
||||
WHERE
|
||||
-- TODO(b/18092292): Add a filter for tldState (not PDT/PREDELEGATION)
|
||||
tldType = 'REAL') ) AS BillingEvent
|
||||
-- Gather billing ID from registrar table
|
||||
-- This is a 'JOIN' as opposed to 'LEFT JOIN' to filter out
|
||||
-- non-billable registrars
|
||||
JOIN (
|
||||
SELECT
|
||||
__key__.name AS clientId,
|
||||
billingIdentifier,
|
||||
IFNULL(poNumber, '') AS poNumber,
|
||||
r.billingAccountMap.currency[SAFE_OFFSET(index)] AS currency,
|
||||
r.billingAccountMap.accountId[SAFE_OFFSET(index)] AS accountId
|
||||
FROM
|
||||
`my-project-id.latest_datastore_export.Registrar` AS r,
|
||||
UNNEST(GENERATE_ARRAY(0, ARRAY_LENGTH(r.billingAccountMap.currency) - 1))
|
||||
AS index
|
||||
WHERE billingAccountMap IS NOT NULL
|
||||
AND type = 'REAL') AS RegistrarData
|
||||
ON
|
||||
BillingEvent.clientId = RegistrarData.clientId
|
||||
AND BillingEvent.currency = RegistrarData.currency
|
||||
-- Gather cancellations
|
||||
LEFT JOIN (
|
||||
SELECT __key__.id AS cancellationId,
|
||||
COALESCE(refOneTime.path, refRecurring.path) AS cancelledEventPath,
|
||||
eventTime as cancellationTime,
|
||||
billingTime as cancellationBillingTime
|
||||
FROM
|
||||
(SELECT
|
||||
*,
|
||||
-- Count everything after first dot as TLD (to support multi-part TLDs).
|
||||
REGEXP_EXTRACT(targetId, r'[.](.+)') AS tld
|
||||
FROM
|
||||
`my-project-id.latest_datastore_export.Cancellation`)
|
||||
) AS Cancellation
|
||||
ON BillingEvent.cancellationMatchingPath = Cancellation.cancelledEventPath
|
||||
AND BillingEvent.billingTime = Cancellation.cancellationBillingTime
|
||||
WHERE billingTime BETWEEN TIMESTAMP('2017-10-01 00:00:00.000000')
|
||||
AND TIMESTAMP('2017-10-31 23:59:59.999999')
|
||||
-- Filter out canceled events
|
||||
AND Cancellation.cancellationId IS NULL
|
||||
ORDER BY
|
||||
billingTime DESC,
|
||||
id,
|
||||
tld
|
||||
@@ -0,0 +1,39 @@
|
||||
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 = "spec11",
|
||||
srcs = glob(["*.java"]),
|
||||
deps = [
|
||||
"//java/google/registry/beam/spec11",
|
||||
"//java/google/registry/util",
|
||||
"//javatests/google/registry/testing",
|
||||
"@com_google_dagger",
|
||||
"@com_google_guava",
|
||||
"@com_google_truth",
|
||||
"@com_google_truth_extensions_truth_java8_extension",
|
||||
"@junit",
|
||||
"@org_apache_avro",
|
||||
"@org_apache_beam_runners_direct_java",
|
||||
"@org_apache_beam_runners_google_cloud_dataflow_java",
|
||||
"@org_apache_beam_sdks_java_core",
|
||||
"@org_apache_beam_sdks_java_io_google_cloud_platform",
|
||||
"@org_apache_httpcomponents_httpclient",
|
||||
"@org_apache_httpcomponents_httpcore",
|
||||
"@org_json",
|
||||
"@org_mockito_core",
|
||||
],
|
||||
)
|
||||
|
||||
GenTestRules(
|
||||
name = "GeneratedTestRules",
|
||||
default_test_size = "small",
|
||||
test_files = glob(["*Test.java"]),
|
||||
deps = [":spec11"],
|
||||
)
|
||||
@@ -0,0 +1,298 @@
|
||||
// Copyright 2018 The Nomulus Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package google.registry.beam.spec11;
|
||||
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
import static java.nio.charset.StandardCharsets.UTF_8;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
import static org.mockito.Mockito.withSettings;
|
||||
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.google.common.io.CharStreams;
|
||||
import google.registry.beam.spec11.SafeBrowsingTransforms.EvaluateSafeBrowsingFn;
|
||||
import google.registry.testing.FakeClock;
|
||||
import google.registry.testing.FakeSleeper;
|
||||
import google.registry.util.ResourceUtils;
|
||||
import google.registry.util.Retrier;
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStreamReader;
|
||||
import java.io.ObjectInputStream;
|
||||
import java.io.ObjectOutputStream;
|
||||
import java.io.Serializable;
|
||||
import java.util.Comparator;
|
||||
import java.util.function.Supplier;
|
||||
import org.apache.beam.runners.direct.DirectRunner;
|
||||
import org.apache.beam.sdk.options.PipelineOptions;
|
||||
import org.apache.beam.sdk.options.PipelineOptionsFactory;
|
||||
import org.apache.beam.sdk.options.ValueProvider.StaticValueProvider;
|
||||
import org.apache.beam.sdk.testing.TestPipeline;
|
||||
import org.apache.beam.sdk.transforms.Create;
|
||||
import org.apache.beam.sdk.values.PCollection;
|
||||
import org.apache.http.ProtocolVersion;
|
||||
import org.apache.http.client.methods.CloseableHttpResponse;
|
||||
import org.apache.http.client.methods.HttpPost;
|
||||
import org.apache.http.entity.BasicHttpEntity;
|
||||
import org.apache.http.impl.client.CloseableHttpClient;
|
||||
import org.apache.http.message.BasicStatusLine;
|
||||
import org.json.JSONArray;
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
import org.junit.Before;
|
||||
import org.junit.BeforeClass;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.junit.rules.TemporaryFolder;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.junit.runners.JUnit4;
|
||||
import org.mockito.invocation.InvocationOnMock;
|
||||
import org.mockito.stubbing.Answer;
|
||||
|
||||
/** Unit tests for {@link Spec11Pipeline}. */
|
||||
@RunWith(JUnit4.class)
|
||||
public class Spec11PipelineTest {
|
||||
|
||||
private static PipelineOptions pipelineOptions;
|
||||
|
||||
@BeforeClass
|
||||
public static void initializePipelineOptions() {
|
||||
pipelineOptions = PipelineOptionsFactory.create();
|
||||
pipelineOptions.setRunner(DirectRunner.class);
|
||||
}
|
||||
|
||||
@Rule public final transient TestPipeline p = TestPipeline.fromOptions(pipelineOptions);
|
||||
@Rule public final TemporaryFolder tempFolder = new TemporaryFolder();
|
||||
|
||||
private Spec11Pipeline spec11Pipeline;
|
||||
|
||||
@Before
|
||||
public void initializePipeline() throws IOException {
|
||||
spec11Pipeline = new Spec11Pipeline();
|
||||
spec11Pipeline.projectId = "test-project";
|
||||
spec11Pipeline.reportingBucketUrl = tempFolder.getRoot().getAbsolutePath();
|
||||
File beamTempFolder = tempFolder.newFolder();
|
||||
spec11Pipeline.beamStagingUrl = beamTempFolder.getAbsolutePath() + "/staging";
|
||||
spec11Pipeline.spec11TemplateUrl = beamTempFolder.getAbsolutePath() + "/templates/invoicing";
|
||||
}
|
||||
|
||||
private static final ImmutableList<String> BAD_DOMAINS =
|
||||
ImmutableList.of("111.com", "222.com", "444.com", "no-email.com");
|
||||
|
||||
private ImmutableList<Subdomain> getInputDomains() {
|
||||
ImmutableList.Builder<Subdomain> subdomainsBuilder = new ImmutableList.Builder<>();
|
||||
// Put in at least 2 batches worth (x > 490) to guarantee multiple executions.
|
||||
// Put in half for theRegistrar and half for someRegistrar
|
||||
for (int i = 0; i < 255; i++) {
|
||||
subdomainsBuilder.add(
|
||||
Subdomain.create(String.format("%s.com", i), "theRegistrar", "fake@theRegistrar.com"));
|
||||
}
|
||||
for (int i = 255; i < 510; i++) {
|
||||
subdomainsBuilder.add(
|
||||
Subdomain.create(String.format("%s.com", i), "someRegistrar", "fake@someRegistrar.com"));
|
||||
}
|
||||
subdomainsBuilder.add(Subdomain.create("no-email.com", "noEmailRegistrar", ""));
|
||||
return subdomainsBuilder.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests the end-to-end Spec11 pipeline with mocked out API calls.
|
||||
*
|
||||
* <p>We suppress the (Serializable & Supplier) dual-casted lambda warnings because the supplier
|
||||
* produces an explicitly serializable mock, which is safe to cast.
|
||||
*/
|
||||
@Test
|
||||
@SuppressWarnings("unchecked")
|
||||
public void testEndToEndPipeline_generatesExpectedFiles() throws Exception {
|
||||
// Establish mocks for testing
|
||||
ImmutableList<Subdomain> inputRows = getInputDomains();
|
||||
CloseableHttpClient httpClient = mock(CloseableHttpClient.class, withSettings().serializable());
|
||||
|
||||
// Return a mock HttpResponse that returns a JSON response based on the request.
|
||||
when(httpClient.execute(any(HttpPost.class))).thenAnswer(new HttpResponder());
|
||||
|
||||
EvaluateSafeBrowsingFn evalFn =
|
||||
new EvaluateSafeBrowsingFn(
|
||||
StaticValueProvider.of("apikey"),
|
||||
new Retrier(new FakeSleeper(new FakeClock()), 3),
|
||||
(Serializable & Supplier) () -> httpClient);
|
||||
|
||||
// Apply input and evaluation transforms
|
||||
PCollection<Subdomain> input = p.apply(Create.of(inputRows));
|
||||
spec11Pipeline.evaluateUrlHealth(input, evalFn, StaticValueProvider.of("2018-06-01"));
|
||||
p.run();
|
||||
|
||||
// Verify header and 4 threat matches for 3 registrars are found
|
||||
ImmutableList<String> generatedReport = resultFileContents();
|
||||
assertThat(generatedReport).hasSize(4);
|
||||
assertThat(generatedReport.get(0))
|
||||
.isEqualTo("Map from registrar email / name to detected subdomain threats:");
|
||||
|
||||
// The output file can put the registrar emails and bad URLs in any order.
|
||||
// We cannot rely on the JSON toString to sort because the keys are not always in the same
|
||||
// order, so we must rely on length even though that's not ideal.
|
||||
ImmutableList<String> sortedLines =
|
||||
ImmutableList.sortedCopyOf(
|
||||
Comparator.comparingInt(String::length), generatedReport.subList(1, 4));
|
||||
|
||||
JSONObject noEmailRegistrarJSON = new JSONObject(sortedLines.get(0));
|
||||
assertThat(noEmailRegistrarJSON.get("registrarEmailAddress")).isEqualTo("");
|
||||
assertThat(noEmailRegistrarJSON.get("registrarClientId")).isEqualTo("noEmailRegistrar");
|
||||
assertThat(noEmailRegistrarJSON.has("threatMatches")).isTrue();
|
||||
JSONArray noEmailThreatMatch = noEmailRegistrarJSON.getJSONArray("threatMatches");
|
||||
assertThat(noEmailThreatMatch.length()).isEqualTo(1);
|
||||
assertThat(noEmailThreatMatch.getJSONObject(0).get("fullyQualifiedDomainName"))
|
||||
.isEqualTo("no-email.com");
|
||||
assertThat(noEmailThreatMatch.getJSONObject(0).get("threatType"))
|
||||
.isEqualTo("MALWARE");
|
||||
|
||||
JSONObject someRegistrarJSON = new JSONObject(sortedLines.get(1));
|
||||
assertThat(someRegistrarJSON.get("registrarEmailAddress")).isEqualTo("fake@someRegistrar.com");
|
||||
assertThat(someRegistrarJSON.get("registrarClientId")).isEqualTo("someRegistrar");
|
||||
assertThat(someRegistrarJSON.has("threatMatches")).isTrue();
|
||||
JSONArray someThreatMatch = someRegistrarJSON.getJSONArray("threatMatches");
|
||||
assertThat(someThreatMatch.length()).isEqualTo(1);
|
||||
assertThat(someThreatMatch.getJSONObject(0).get("fullyQualifiedDomainName"))
|
||||
.isEqualTo("444.com");
|
||||
assertThat(someThreatMatch.getJSONObject(0).get("threatType"))
|
||||
.isEqualTo("MALWARE");
|
||||
|
||||
// theRegistrar has two ThreatMatches, we have to parse it explicitly
|
||||
JSONObject theRegistrarJSON = new JSONObject(sortedLines.get(2));
|
||||
assertThat(theRegistrarJSON.get("registrarEmailAddress")).isEqualTo("fake@theRegistrar.com");
|
||||
assertThat(theRegistrarJSON.get("registrarClientId")).isEqualTo("theRegistrar");
|
||||
assertThat(theRegistrarJSON.has("threatMatches")).isTrue();
|
||||
JSONArray theThreatMatches = theRegistrarJSON.getJSONArray("threatMatches");
|
||||
assertThat(theThreatMatches.length()).isEqualTo(2);
|
||||
ImmutableList<String> threatMatchStrings =
|
||||
ImmutableList.of(
|
||||
theThreatMatches.getJSONObject(0).toString(),
|
||||
theThreatMatches.getJSONObject(1).toString());
|
||||
assertThat(threatMatchStrings)
|
||||
.containsExactly(
|
||||
new JSONObject()
|
||||
.put("fullyQualifiedDomainName", "111.com")
|
||||
.put("threatType", "MALWARE")
|
||||
.put("threatEntryMetadata", "NONE")
|
||||
.put("platformType", "WINDOWS")
|
||||
.toString(),
|
||||
new JSONObject()
|
||||
.put("fullyQualifiedDomainName", "222.com")
|
||||
.put("threatType", "MALWARE")
|
||||
.put("threatEntryMetadata", "NONE")
|
||||
.put("platformType", "WINDOWS")
|
||||
.toString());
|
||||
}
|
||||
|
||||
/**
|
||||
* A serializable {@link Answer} that returns a mock HTTP response based on the HTTP request's
|
||||
* content.
|
||||
*/
|
||||
private static class HttpResponder implements Answer<CloseableHttpResponse>, Serializable {
|
||||
@Override
|
||||
public CloseableHttpResponse answer(InvocationOnMock invocation) throws Throwable {
|
||||
return getMockResponse(
|
||||
CharStreams.toString(
|
||||
new InputStreamReader(
|
||||
((HttpPost) invocation.getArguments()[0]).getEntity().getContent(), UTF_8)));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a {@link CloseableHttpResponse} containing either positive (threat found) or negative
|
||||
* (no threat) API examples based on the request data.
|
||||
*/
|
||||
private static CloseableHttpResponse getMockResponse(String request) throws JSONException {
|
||||
// Determine which bad URLs are in the request (if any)
|
||||
ImmutableList<String> badUrls =
|
||||
BAD_DOMAINS.stream().filter(request::contains).collect(ImmutableList.toImmutableList());
|
||||
|
||||
CloseableHttpResponse httpResponse =
|
||||
mock(CloseableHttpResponse.class, withSettings().serializable());
|
||||
when(httpResponse.getStatusLine())
|
||||
.thenReturn(
|
||||
new BasicStatusLine(new ProtocolVersion("HTTP", 1, 1), 200, "Done"));
|
||||
when(httpResponse.getEntity())
|
||||
.thenReturn(new FakeHttpEntity(getAPIResponse(badUrls)));
|
||||
return httpResponse;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the expected API response for a list of bad URLs.
|
||||
*
|
||||
* <p>If there are no badUrls in the list, this returns the empty JSON string "{}".
|
||||
*/
|
||||
private static String getAPIResponse(ImmutableList<String> badUrls) throws JSONException {
|
||||
JSONObject response = new JSONObject();
|
||||
if (badUrls.isEmpty()) {
|
||||
return response.toString();
|
||||
}
|
||||
// Create a threatMatch for each badUrl
|
||||
JSONArray matches = new JSONArray();
|
||||
for (String badUrl : badUrls) {
|
||||
matches.put(
|
||||
new JSONObject()
|
||||
.put("threatType", "MALWARE")
|
||||
.put("platformType", "WINDOWS")
|
||||
.put("threatEntryType", "URL")
|
||||
.put("threat", new JSONObject().put("url", badUrl))
|
||||
.put("cacheDuration", "300.000s"));
|
||||
}
|
||||
response.put("matches", matches);
|
||||
return response.toString();
|
||||
}
|
||||
|
||||
/** A serializable HttpEntity fake that returns {@link String} content. */
|
||||
private static class FakeHttpEntity extends BasicHttpEntity implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = 105738294571L;
|
||||
|
||||
private String content;
|
||||
|
||||
private void writeObject(ObjectOutputStream oos) throws IOException {
|
||||
oos.defaultWriteObject();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the {@link FakeHttpEntity} content upon deserialization.
|
||||
*
|
||||
* <p>This allows us to use {@link #getContent()} as-is, fully emulating the behavior of {@link
|
||||
* BasicHttpEntity} regardless of serialization.
|
||||
*/
|
||||
private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException {
|
||||
ois.defaultReadObject();
|
||||
super.setContent(new ByteArrayInputStream(this.content.getBytes(UTF_8)));
|
||||
}
|
||||
|
||||
FakeHttpEntity(String content) {
|
||||
this.content = content;
|
||||
super.setContent(new ByteArrayInputStream(this.content.getBytes(UTF_8)));
|
||||
}
|
||||
}
|
||||
|
||||
/** Returns the text contents of a file under the beamBucket/results directory. */
|
||||
private ImmutableList<String> resultFileContents() throws Exception {
|
||||
File resultFile =
|
||||
new File(
|
||||
String.format(
|
||||
"%s/icann/spec11/2018-06/SPEC11_MONTHLY_REPORT_2018-06-01",
|
||||
tempFolder.getRoot().getAbsolutePath()));
|
||||
return ImmutableList.copyOf(
|
||||
ResourceUtils.readResourceUtf8(resultFile.toURI().toURL()).split("\n"));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
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 = "bigquery",
|
||||
srcs = glob(["*.java"]),
|
||||
resources = glob(["testdata/*"]),
|
||||
deps = [
|
||||
"//java/google/registry/bigquery",
|
||||
"//java/google/registry/util",
|
||||
"//javatests/google/registry/testing",
|
||||
"@com_google_apis_google_api_services_bigquery",
|
||||
"@com_google_guava",
|
||||
"@com_google_http_client",
|
||||
"@com_google_truth",
|
||||
"@com_google_truth_extensions_truth_java8_extension",
|
||||
"@joda_time",
|
||||
"@junit",
|
||||
"@org_mockito_core",
|
||||
],
|
||||
)
|
||||
|
||||
GenTestRules(
|
||||
name = "GeneratedTestRules",
|
||||
test_files = glob(["*Test.java"]),
|
||||
deps = [":bigquery"],
|
||||
)
|
||||
@@ -0,0 +1,31 @@
|
||||
// Copyright 2017 The Nomulus Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package google.registry.bigquery;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.junit.runners.JUnit4;
|
||||
|
||||
/** Unit tests for {@link BigqueryConnection}. */
|
||||
@RunWith(JUnit4.class)
|
||||
public class BigqueryConnectionTest {
|
||||
|
||||
@Test
|
||||
public void testNothing() {
|
||||
// Placeholder test class for now.
|
||||
// TODO(b/16569089): figure out a good way for testing our Bigquery usage overall - maybe unit
|
||||
// tests here, maybe end-to-end testing.
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,144 @@
|
||||
// Copyright 2017 The Nomulus Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package google.registry.bigquery;
|
||||
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
import static google.registry.bigquery.BigqueryUtils.fromBigqueryTimestampString;
|
||||
import static google.registry.bigquery.BigqueryUtils.toBigqueryTimestamp;
|
||||
import static google.registry.bigquery.BigqueryUtils.toBigqueryTimestampString;
|
||||
import static google.registry.bigquery.BigqueryUtils.toJobReferenceString;
|
||||
import static google.registry.testing.JUnitBackports.assertThrows;
|
||||
import static google.registry.util.DateTimeUtils.END_OF_TIME;
|
||||
import static google.registry.util.DateTimeUtils.START_OF_TIME;
|
||||
|
||||
import com.google.api.services.bigquery.model.JobReference;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import org.joda.time.DateTime;
|
||||
import org.joda.time.DateTimeZone;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.junit.runners.JUnit4;
|
||||
|
||||
/** Unit tests for {@link BigqueryUtils}. */
|
||||
@RunWith(JUnit4.class)
|
||||
public class BigqueryUtilsTest {
|
||||
private static final DateTime DATE_0 = DateTime.parse("2014-07-17T20:35:42Z");
|
||||
private static final DateTime DATE_1 = DateTime.parse("2014-07-17T20:35:42.1Z");
|
||||
private static final DateTime DATE_2 = DateTime.parse("2014-07-17T20:35:42.12Z");
|
||||
private static final DateTime DATE_3 = DateTime.parse("2014-07-17T20:35:42.123Z");
|
||||
|
||||
@Test
|
||||
public void test_toBigqueryTimestampString() {
|
||||
assertThat(toBigqueryTimestampString(START_OF_TIME)).isEqualTo("1970-01-01 00:00:00.000");
|
||||
assertThat(toBigqueryTimestampString(DATE_0)).isEqualTo("2014-07-17 20:35:42.000");
|
||||
assertThat(toBigqueryTimestampString(DATE_1)).isEqualTo("2014-07-17 20:35:42.100");
|
||||
assertThat(toBigqueryTimestampString(DATE_2)).isEqualTo("2014-07-17 20:35:42.120");
|
||||
assertThat(toBigqueryTimestampString(DATE_3)).isEqualTo("2014-07-17 20:35:42.123");
|
||||
assertThat(toBigqueryTimestampString(END_OF_TIME)).isEqualTo("294247-01-10 04:00:54.775");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test_toBigqueryTimestampString_convertsToUtc() {
|
||||
assertThat(toBigqueryTimestampString(START_OF_TIME.withZone(DateTimeZone.forOffsetHours(5))))
|
||||
.isEqualTo("1970-01-01 00:00:00.000");
|
||||
assertThat(toBigqueryTimestampString(DateTime.parse("1970-01-01T00:00:00-0500")))
|
||||
.isEqualTo("1970-01-01 05:00:00.000");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test_fromBigqueryTimestampString_startAndEndOfTime() {
|
||||
assertThat(fromBigqueryTimestampString("1970-01-01 00:00:00 UTC")).isEqualTo(START_OF_TIME);
|
||||
assertThat(fromBigqueryTimestampString("294247-01-10 04:00:54.775 UTC")).isEqualTo(END_OF_TIME);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test_fromBigqueryTimestampString_trailingZerosOkay() {
|
||||
assertThat(fromBigqueryTimestampString("2014-07-17 20:35:42 UTC")).isEqualTo(DATE_0);
|
||||
assertThat(fromBigqueryTimestampString("2014-07-17 20:35:42.0 UTC")).isEqualTo(DATE_0);
|
||||
assertThat(fromBigqueryTimestampString("2014-07-17 20:35:42.00 UTC")).isEqualTo(DATE_0);
|
||||
assertThat(fromBigqueryTimestampString("2014-07-17 20:35:42.000 UTC")).isEqualTo(DATE_0);
|
||||
assertThat(fromBigqueryTimestampString("2014-07-17 20:35:42.1 UTC")).isEqualTo(DATE_1);
|
||||
assertThat(fromBigqueryTimestampString("2014-07-17 20:35:42.10 UTC")).isEqualTo(DATE_1);
|
||||
assertThat(fromBigqueryTimestampString("2014-07-17 20:35:42.100 UTC")).isEqualTo(DATE_1);
|
||||
assertThat(fromBigqueryTimestampString("2014-07-17 20:35:42.12 UTC")).isEqualTo(DATE_2);
|
||||
assertThat(fromBigqueryTimestampString("2014-07-17 20:35:42.120 UTC")).isEqualTo(DATE_2);
|
||||
assertThat(fromBigqueryTimestampString("2014-07-17 20:35:42.123 UTC")).isEqualTo(DATE_3);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testFailure_fromBigqueryTimestampString_nonUtcTimeZone() {
|
||||
assertThrows(
|
||||
IllegalArgumentException.class,
|
||||
() -> fromBigqueryTimestampString("2014-01-01 01:01:01 +05:00"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testFailure_fromBigqueryTimestampString_noTimeZone() {
|
||||
assertThrows(
|
||||
IllegalArgumentException.class, () -> fromBigqueryTimestampString("2014-01-01 01:01:01"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testFailure_fromBigqueryTimestampString_tooManyMillisecondDigits() {
|
||||
assertThrows(
|
||||
IllegalArgumentException.class,
|
||||
() -> fromBigqueryTimestampString("2014-01-01 01:01:01.1234 UTC"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test_toBigqueryTimestamp_timeunitConversion() {
|
||||
assertThat(toBigqueryTimestamp(1234567890L, TimeUnit.SECONDS))
|
||||
.isEqualTo("1234567890.000000");
|
||||
assertThat(toBigqueryTimestamp(1234567890123L, TimeUnit.MILLISECONDS))
|
||||
.isEqualTo("1234567890.123000");
|
||||
assertThat(toBigqueryTimestamp(1234567890123000L, TimeUnit.MICROSECONDS))
|
||||
.isEqualTo("1234567890.123000");
|
||||
assertThat(toBigqueryTimestamp(1234567890123000000L, TimeUnit.NANOSECONDS))
|
||||
.isEqualTo("1234567890.123000");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test_toBigqueryTimestamp_timeunitConversionForZero() {
|
||||
assertThat(toBigqueryTimestamp(0L, TimeUnit.SECONDS)).isEqualTo("0.000000");
|
||||
assertThat(toBigqueryTimestamp(0L, TimeUnit.MILLISECONDS)).isEqualTo("0.000000");
|
||||
assertThat(toBigqueryTimestamp(0L, TimeUnit.MICROSECONDS)).isEqualTo("0.000000");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test_toBigqueryTimestamp_datetimeConversion() {
|
||||
assertThat(toBigqueryTimestamp(START_OF_TIME)).isEqualTo("0.000000");
|
||||
assertThat(toBigqueryTimestamp(DATE_0)).isEqualTo("1405629342.000000");
|
||||
assertThat(toBigqueryTimestamp(DATE_1)).isEqualTo("1405629342.100000");
|
||||
assertThat(toBigqueryTimestamp(DATE_2)).isEqualTo("1405629342.120000");
|
||||
assertThat(toBigqueryTimestamp(DATE_3)).isEqualTo("1405629342.123000");
|
||||
assertThat(toBigqueryTimestamp(END_OF_TIME)).isEqualTo("9223372036854.775000");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test_toJobReferenceString_normalSucceeds() {
|
||||
assertThat(toJobReferenceString(new JobReference().setProjectId("foo").setJobId("bar")))
|
||||
.isEqualTo("foo:bar");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test_toJobReferenceString_emptyReferenceSucceeds() {
|
||||
assertThat(toJobReferenceString(new JobReference())).isEqualTo("null:null");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test_toJobReferenceString_nullThrowsNpe() {
|
||||
assertThrows(NullPointerException.class, () -> toJobReferenceString(null));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
// Copyright 2018 The Nomulus Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package google.registry.bigquery;
|
||||
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
import static google.registry.bigquery.BigqueryUtils.FieldType.STRING;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import com.google.api.services.bigquery.Bigquery;
|
||||
import com.google.api.services.bigquery.model.Dataset;
|
||||
import com.google.api.services.bigquery.model.Table;
|
||||
import com.google.api.services.bigquery.model.TableFieldSchema;
|
||||
import com.google.api.services.bigquery.model.TableReference;
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.google.common.collect.ImmutableMap;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.junit.runners.JUnit4;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
|
||||
/** Unit tests for {@link CheckedBigquery}. */
|
||||
@RunWith(JUnit4.class)
|
||||
public class CheckedBigqueryTest {
|
||||
|
||||
private final Bigquery bigquery = mock(Bigquery.class);
|
||||
private final Bigquery.Datasets bigqueryDatasets = mock(Bigquery.Datasets.class);
|
||||
private final Bigquery.Datasets.Insert bigqueryDatasetsInsert =
|
||||
mock(Bigquery.Datasets.Insert.class);
|
||||
private final Bigquery.Tables bigqueryTables = mock(Bigquery.Tables.class);
|
||||
private final Bigquery.Tables.Insert bigqueryTablesInsert = mock(Bigquery.Tables.Insert.class);
|
||||
|
||||
private CheckedBigquery checkedBigquery;
|
||||
|
||||
@Before
|
||||
public void before() throws Exception {
|
||||
when(bigquery.datasets()).thenReturn(bigqueryDatasets);
|
||||
when(bigqueryDatasets.insert(eq("Project-Id"), any(Dataset.class)))
|
||||
.thenReturn(bigqueryDatasetsInsert);
|
||||
when(bigquery.tables()).thenReturn(bigqueryTables);
|
||||
when(bigqueryTables.insert(eq("Project-Id"), any(String.class), any(Table.class)))
|
||||
.thenReturn(bigqueryTablesInsert);
|
||||
checkedBigquery = new CheckedBigquery();
|
||||
checkedBigquery.bigquery = bigquery;
|
||||
checkedBigquery.bigquerySchemas =
|
||||
new ImmutableMap.Builder<String, ImmutableList<TableFieldSchema>>()
|
||||
.put(
|
||||
"Table-Id",
|
||||
ImmutableList.of(new TableFieldSchema().setName("column1").setType(STRING.name())))
|
||||
.put(
|
||||
"Table2",
|
||||
ImmutableList.of(new TableFieldSchema().setName("column1").setType(STRING.name())))
|
||||
.build();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSuccess_datastoreCreation() throws Exception {
|
||||
checkedBigquery.ensureDataSetExists("Project-Id", "Dataset-Id");
|
||||
|
||||
ArgumentCaptor<Dataset> datasetArg = ArgumentCaptor.forClass(Dataset.class);
|
||||
verify(bigqueryDatasets).insert(eq("Project-Id"), datasetArg.capture());
|
||||
assertThat(datasetArg.getValue().getDatasetReference().getProjectId())
|
||||
.isEqualTo("Project-Id");
|
||||
assertThat(datasetArg.getValue().getDatasetReference().getDatasetId())
|
||||
.isEqualTo("Dataset-Id");
|
||||
verify(bigqueryDatasetsInsert).execute();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSuccess_datastoreAndTableCreation() throws Exception {
|
||||
checkedBigquery.ensureDataSetAndTableExist("Project-Id", "Dataset2", "Table2");
|
||||
|
||||
ArgumentCaptor<Dataset> datasetArg = ArgumentCaptor.forClass(Dataset.class);
|
||||
verify(bigqueryDatasets).insert(eq("Project-Id"), datasetArg.capture());
|
||||
assertThat(datasetArg.getValue().getDatasetReference().getProjectId())
|
||||
.isEqualTo("Project-Id");
|
||||
assertThat(datasetArg.getValue().getDatasetReference().getDatasetId())
|
||||
.isEqualTo("Dataset2");
|
||||
verify(bigqueryDatasetsInsert).execute();
|
||||
|
||||
ArgumentCaptor<Table> tableArg = ArgumentCaptor.forClass(Table.class);
|
||||
verify(bigqueryTables).insert(eq("Project-Id"), eq("Dataset2"), tableArg.capture());
|
||||
TableReference ref = tableArg.getValue().getTableReference();
|
||||
assertThat(ref.getProjectId()).isEqualTo("Project-Id");
|
||||
assertThat(ref.getDatasetId()).isEqualTo("Dataset2");
|
||||
assertThat(ref.getTableId()).isEqualTo("Table2");
|
||||
assertThat(tableArg.getValue().getSchema().getFields())
|
||||
.containsExactly(new TableFieldSchema().setName("column1").setType(STRING.name()));
|
||||
verify(bigqueryTablesInsert).execute();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
package(
|
||||
default_testonly = 1,
|
||||
default_visibility = ["//java/google/registry:registry_project"],
|
||||
)
|
||||
|
||||
licenses(["notice"]) # Apache 2.0
|
||||
|
||||
load("//java/google/registry/builddefs:zip_file.bzl", "zip_file")
|
||||
load("//javatests/google/registry/builddefs:zip_contents_test.bzl", "zip_contents_test")
|
||||
|
||||
genrule(
|
||||
name = "generated",
|
||||
outs = ["generated.txt"],
|
||||
cmd = "echo generated >$@",
|
||||
)
|
||||
|
||||
zip_file(
|
||||
name = "basic",
|
||||
srcs = [
|
||||
"generated.txt",
|
||||
"hello.txt",
|
||||
"world.txt",
|
||||
],
|
||||
out = "basic.zip",
|
||||
mappings = {"": ""},
|
||||
)
|
||||
|
||||
zip_contents_test(
|
||||
name = "zip_emptyMapping_leavesShortPathsInTact",
|
||||
src = "basic.zip",
|
||||
contents = {
|
||||
"domain_registry/javatests/google/registry/builddefs/generated.txt": "generated",
|
||||
"domain_registry/javatests/google/registry/builddefs/hello.txt": "hello",
|
||||
"domain_registry/javatests/google/registry/builddefs/world.txt": "world",
|
||||
},
|
||||
)
|
||||
|
||||
zip_file(
|
||||
name = "stripped",
|
||||
srcs = ["hello.txt"],
|
||||
out = "stripped.zip",
|
||||
mappings = {"domain_registry/javatests/google/registry/builddefs": ""},
|
||||
)
|
||||
|
||||
zip_contents_test(
|
||||
name = "zip_prefixRemoval_works",
|
||||
src = "stripped.zip",
|
||||
contents = {"hello.txt": "hello"},
|
||||
)
|
||||
|
||||
zip_file(
|
||||
name = "repath",
|
||||
srcs = [
|
||||
"generated.txt",
|
||||
"hello.txt",
|
||||
"world.txt",
|
||||
],
|
||||
out = "repath.zip",
|
||||
mappings = {
|
||||
"domain_registry/javatests/google/registry/builddefs": "a/b/c",
|
||||
"domain_registry/javatests/google/registry/builddefs/generated.txt": "x/y/z/generated.txt",
|
||||
},
|
||||
)
|
||||
|
||||
zip_contents_test(
|
||||
name = "zip_pathReplacement_works",
|
||||
src = "repath.zip",
|
||||
contents = {
|
||||
"a/b/c/hello.txt": "hello",
|
||||
"a/b/c/world.txt": "world",
|
||||
"x/y/z/generated.txt": "generated",
|
||||
},
|
||||
)
|
||||
|
||||
zip_file(
|
||||
name = "overridden",
|
||||
srcs = ["override/hello.txt"],
|
||||
out = "overridden.zip",
|
||||
mappings = {"domain_registry/javatests/google/registry/builddefs/override": "a/b/c"},
|
||||
deps = [":repath"],
|
||||
)
|
||||
|
||||
zip_contents_test(
|
||||
name = "zip_fileWithSameMappingAsDependentRule_prefersMyMapping",
|
||||
src = "overridden.zip",
|
||||
contents = {
|
||||
"a/b/c/hello.txt": "OMG IM AN OVERRIDE",
|
||||
"a/b/c/world.txt": "world",
|
||||
"x/y/z/generated.txt": "generated",
|
||||
},
|
||||
)
|
||||
@@ -0,0 +1 @@
|
||||
hello
|
||||
@@ -0,0 +1 @@
|
||||
OMG IM AN OVERRIDE
|
||||
@@ -0,0 +1 @@
|
||||
world
|
||||
@@ -0,0 +1,66 @@
|
||||
# Copyright 2017 The Nomulus Authors. All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
"""Build rule for unit testing the zip_file() rule."""
|
||||
|
||||
load("//java/google/registry/builddefs:defs.bzl", "ZIPPER")
|
||||
|
||||
def _impl(ctx):
|
||||
"""Implementation of zip_contents_test() rule."""
|
||||
cmd = [
|
||||
"set -e",
|
||||
'repo="$(pwd)"',
|
||||
'zipper="${repo}/%s"' % ctx.file._zipper.short_path,
|
||||
'archive="${repo}/%s"' % ctx.file.src.short_path,
|
||||
('listing="$("${zipper}" v "${archive}"' +
|
||||
' | grep -v ^d | awk \'{print $3}\' | LC_ALL=C sort)"'),
|
||||
'if [[ "${listing}" != "%s" ]]; then' % (
|
||||
"\n".join(ctx.attr.contents.keys())
|
||||
),
|
||||
' echo "archive had different file listing:"',
|
||||
' "${zipper}" v "${archive}" | grep -v ^d',
|
||||
" exit 1",
|
||||
"fi",
|
||||
'tmp="$(mktemp -d "${TMPDIR:-/tmp}/zip_contents_test.XXXXXXXXXX")"',
|
||||
'cd "${tmp}"',
|
||||
'"${zipper}" x "${archive}"',
|
||||
]
|
||||
for path, data in ctx.attr.contents.items():
|
||||
cmd += [
|
||||
'if [[ "$(cat "%s")" != "%s" ]]; then' % (path, data),
|
||||
' echo "%s had different contents:"' % path,
|
||||
' cat "%s"' % path,
|
||||
" exit 1",
|
||||
"fi",
|
||||
]
|
||||
cmd += [
|
||||
'cd "${repo}"',
|
||||
'rm -rf "${tmp}"',
|
||||
]
|
||||
ctx.actions.write(
|
||||
output = ctx.outputs.executable,
|
||||
content = "\n".join(cmd),
|
||||
is_executable = True,
|
||||
)
|
||||
return struct(runfiles = ctx.runfiles([ctx.file.src, ctx.file._zipper]))
|
||||
|
||||
zip_contents_test = rule(
|
||||
implementation = _impl,
|
||||
test = True,
|
||||
attrs = {
|
||||
"src": attr.label(allow_single_file = True),
|
||||
"contents": attr.string_dict(),
|
||||
"_zipper": attr.label(default = Label(ZIPPER), allow_single_file = True),
|
||||
},
|
||||
)
|
||||
@@ -0,0 +1,28 @@
|
||||
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 = "config",
|
||||
srcs = glob(["*.java"]),
|
||||
deps = [
|
||||
"//java/google/registry/config",
|
||||
"//javatests/google/registry/testing",
|
||||
"@com_google_auto_value",
|
||||
"@com_google_guava",
|
||||
"@com_google_truth",
|
||||
"@com_google_truth_extensions_truth_java8_extension",
|
||||
"@junit",
|
||||
],
|
||||
)
|
||||
|
||||
GenTestRules(
|
||||
name = "GeneratedTestRules",
|
||||
test_files = glob(["*Test.java"]),
|
||||
deps = [":config"],
|
||||
)
|
||||
@@ -0,0 +1,33 @@
|
||||
// Copyright 2017 The Nomulus Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package google.registry.config;
|
||||
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
import static google.registry.config.RegistryConfig.CONFIG_SETTINGS;
|
||||
import static google.registry.config.RegistryConfig.ConfigModule.provideReservedTermsExportDisclaimer;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.junit.runners.JUnit4;
|
||||
|
||||
@RunWith(JUnit4.class)
|
||||
public class RegistryConfigTest {
|
||||
|
||||
@Test
|
||||
public void test_reservedTermsExportDisclaimer_isPrependedWithOctothorpes() {
|
||||
assertThat(provideReservedTermsExportDisclaimer(CONFIG_SETTINGS.get()))
|
||||
.isEqualTo("# Disclaimer line 1.\n" + "# Line 2 is this 1.");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
// Copyright 2017 The Nomulus Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package google.registry.config;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.junit.runners.JUnit4;
|
||||
|
||||
/** Unit tests for {@link RegistryEnvironment}. */
|
||||
@RunWith(JUnit4.class)
|
||||
public class RegistryEnvironmentTest {
|
||||
|
||||
@Test
|
||||
public void testGet() {
|
||||
RegistryEnvironment.get();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
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 = "cron",
|
||||
srcs = glob(["*.java"]),
|
||||
deps = [
|
||||
"//java/google/registry/cron",
|
||||
"//java/google/registry/model",
|
||||
"//java/google/registry/util",
|
||||
"//javatests/google/registry/testing",
|
||||
"//third_party/objectify:objectify-v4_1",
|
||||
"@com_google_appengine_api_1_0_sdk",
|
||||
"@com_google_appengine_api_stubs",
|
||||
"@com_google_appengine_testing",
|
||||
"@com_google_guava",
|
||||
"@com_google_truth",
|
||||
"@com_google_truth_extensions_truth_java8_extension",
|
||||
"@junit",
|
||||
],
|
||||
)
|
||||
|
||||
GenTestRules(
|
||||
name = "GeneratedTestRules",
|
||||
test_files = glob(["*Test.java"]),
|
||||
deps = [":cron"],
|
||||
)
|
||||
@@ -0,0 +1,68 @@
|
||||
// Copyright 2017 The Nomulus Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package google.registry.cron;
|
||||
|
||||
import static google.registry.cron.CommitLogFanoutAction.BUCKET_PARAM;
|
||||
import static google.registry.testing.TaskQueueHelper.assertTasksEnqueued;
|
||||
|
||||
import com.google.common.base.Joiner;
|
||||
import google.registry.model.ofy.CommitLogBucket;
|
||||
import google.registry.testing.AppEngineRule;
|
||||
import google.registry.testing.TaskQueueHelper.TaskMatcher;
|
||||
import google.registry.util.Retrier;
|
||||
import google.registry.util.TaskQueueUtils;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.junit.runners.JUnit4;
|
||||
|
||||
/** Unit tests for {@link CommitLogFanoutAction}. */
|
||||
@RunWith(JUnit4.class)
|
||||
public class CommitLogFanoutActionTest {
|
||||
|
||||
private static final String ENDPOINT = "/the/servlet";
|
||||
private static final String QUEUE = "the-queue";
|
||||
|
||||
@Rule
|
||||
public final AppEngineRule appEngine = AppEngineRule.builder()
|
||||
.withDatastore()
|
||||
.withTaskQueue(Joiner.on('\n').join(
|
||||
"<?xml version=\"1.0\" encoding=\"UTF-8\"?>",
|
||||
"<queue-entries>",
|
||||
" <queue>",
|
||||
" <name>the-queue</name>",
|
||||
" <rate>1/s</rate>",
|
||||
" </queue>",
|
||||
"</queue-entries>"))
|
||||
.build();
|
||||
|
||||
@Test
|
||||
public void testSuccess() {
|
||||
CommitLogFanoutAction action = new CommitLogFanoutAction();
|
||||
action.taskQueueUtils = new TaskQueueUtils(new Retrier(null, 1));
|
||||
action.endpoint = ENDPOINT;
|
||||
action.queue = QUEUE;
|
||||
action.jitterSeconds = Optional.empty();
|
||||
action.run();
|
||||
List<TaskMatcher> matchers = new ArrayList<>();
|
||||
for (int bucketId : CommitLogBucket.getBucketIds()) {
|
||||
matchers.add(new TaskMatcher().url(ENDPOINT).param(BUCKET_PARAM, Integer.toString(bucketId)));
|
||||
}
|
||||
assertTasksEnqueued(QUEUE, matchers);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,263 @@
|
||||
// Copyright 2017 The Nomulus Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package google.registry.cron;
|
||||
|
||||
import static com.google.common.collect.ImmutableList.toImmutableList;
|
||||
import static com.google.common.collect.Iterables.getLast;
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
import static google.registry.testing.DatastoreHelper.createTlds;
|
||||
import static google.registry.testing.DatastoreHelper.persistResource;
|
||||
import static google.registry.testing.JUnitBackports.assertThrows;
|
||||
import static google.registry.testing.TaskQueueHelper.assertNoTasksEnqueued;
|
||||
import static google.registry.testing.TaskQueueHelper.assertTasksEnqueued;
|
||||
|
||||
import com.google.appengine.api.taskqueue.dev.QueueStateInfo.TaskStateInfo;
|
||||
import com.google.appengine.tools.development.testing.LocalTaskQueueTestConfig;
|
||||
import com.google.common.base.Joiner;
|
||||
import com.google.common.base.Splitter;
|
||||
import com.google.common.collect.ImmutableListMultimap;
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
import google.registry.model.registry.Registry;
|
||||
import google.registry.model.registry.Registry.TldType;
|
||||
import google.registry.testing.AppEngineRule;
|
||||
import google.registry.testing.FakeResponse;
|
||||
import google.registry.testing.TaskQueueHelper.TaskMatcher;
|
||||
import google.registry.util.Retrier;
|
||||
import google.registry.util.TaskQueueUtils;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.stream.Stream;
|
||||
import org.junit.Before;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.junit.runners.JUnit4;
|
||||
|
||||
/** Unit tests for {@link TldFanoutAction}. */
|
||||
@RunWith(JUnit4.class)
|
||||
public class TldFanoutActionTest {
|
||||
|
||||
private static final String ENDPOINT = "/the/servlet";
|
||||
private static final String QUEUE = "the-queue";
|
||||
private final FakeResponse response = new FakeResponse();
|
||||
|
||||
@Rule
|
||||
public final AppEngineRule appEngine = AppEngineRule.builder()
|
||||
.withDatastore()
|
||||
.withTaskQueue(Joiner.on('\n').join(
|
||||
"<?xml version=\"1.0\" encoding=\"UTF-8\"?>",
|
||||
"<queue-entries>",
|
||||
" <queue>",
|
||||
" <name>the-queue</name>",
|
||||
" <rate>1/s</rate>",
|
||||
" </queue>",
|
||||
"</queue-entries>"))
|
||||
.build();
|
||||
|
||||
private static ImmutableListMultimap<String, String> getParamsMap(String... keysAndValues) {
|
||||
ImmutableListMultimap.Builder<String, String> params = new ImmutableListMultimap.Builder<>();
|
||||
params.put("queue", QUEUE);
|
||||
params.put("endpoint", ENDPOINT);
|
||||
for (int i = 0; i < keysAndValues.length; i += 2) {
|
||||
params.put(keysAndValues[i], keysAndValues[i + 1]);
|
||||
}
|
||||
return params.build();
|
||||
}
|
||||
|
||||
private void run(ImmutableListMultimap<String, String> params) {
|
||||
TldFanoutAction action = new TldFanoutAction();
|
||||
action.params = params;
|
||||
action.endpoint = getLast(params.get("endpoint"));
|
||||
action.queue = getLast(params.get("queue"));
|
||||
action.excludes = params.containsKey("exclude")
|
||||
? ImmutableSet.copyOf(Splitter.on(',').split(params.get("exclude").get(0)))
|
||||
: ImmutableSet.of();
|
||||
action.taskQueueUtils = new TaskQueueUtils(new Retrier(null, 1));
|
||||
action.response = response;
|
||||
action.runInEmpty = params.containsKey("runInEmpty");
|
||||
action.forEachRealTld = params.containsKey("forEachRealTld");
|
||||
action.forEachTestTld = params.containsKey("forEachTestTld");
|
||||
action.jitterSeconds = Optional.empty();
|
||||
action.run();
|
||||
}
|
||||
|
||||
@Before
|
||||
public void before() {
|
||||
createTlds("com", "net", "org", "example");
|
||||
persistResource(Registry.get("example").asBuilder().setTldType(TldType.TEST).build());
|
||||
}
|
||||
|
||||
private static void assertTasks(String... tasks) {
|
||||
assertTasksEnqueued(
|
||||
QUEUE,
|
||||
Stream.of(tasks).map(
|
||||
namespace ->
|
||||
new TaskMatcher()
|
||||
.url(ENDPOINT)
|
||||
.header("content-type", "application/x-www-form-urlencoded")
|
||||
.param("tld", namespace))
|
||||
.collect(toImmutableList()));
|
||||
}
|
||||
|
||||
private static void assertTaskWithoutTld() {
|
||||
assertTasksEnqueued(
|
||||
QUEUE,
|
||||
new TaskMatcher()
|
||||
.url(ENDPOINT)
|
||||
.header("content-type", "application/x-www-form-urlencoded"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSuccess_methodPostIsDefault() {
|
||||
run(getParamsMap("runInEmpty", ""));
|
||||
assertTasksEnqueued(QUEUE, new TaskMatcher().method("POST"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testFailure_noTlds() {
|
||||
assertThrows(IllegalArgumentException.class, () -> run(getParamsMap()));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSuccess_runInEmpty() {
|
||||
run(getParamsMap("runInEmpty", ""));
|
||||
assertTaskWithoutTld();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSuccess_forEachRealTld() {
|
||||
run(getParamsMap("forEachRealTld", ""));
|
||||
assertTasks("com", "net", "org");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSuccess_forEachTestTld() {
|
||||
run(getParamsMap("forEachTestTld", ""));
|
||||
assertTasks("example");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSuccess_forEachTestTldAndForEachRealTld() {
|
||||
run(getParamsMap(
|
||||
"forEachTestTld", "",
|
||||
"forEachRealTld", ""));
|
||||
assertTasks("com", "net", "org", "example");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSuccess_runEverywhere() {
|
||||
run(getParamsMap("forEachTestTld", "", "forEachRealTld", ""));
|
||||
assertTasks("com", "net", "org", "example");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSuccess_excludeRealTlds() {
|
||||
run(getParamsMap(
|
||||
"forEachRealTld", "",
|
||||
"exclude", "com,net"));
|
||||
assertTasks("org");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSuccess_excludeTestTlds() {
|
||||
run(getParamsMap(
|
||||
"forEachTestTld", "",
|
||||
"exclude", "example"));
|
||||
assertNoTasksEnqueued(QUEUE);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSuccess_excludeNonexistentTlds() {
|
||||
run(getParamsMap(
|
||||
"forEachTestTld", "",
|
||||
"forEachRealTld", "",
|
||||
"exclude", "foo"));
|
||||
assertTasks("com", "net", "org", "example");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testFailure_runInEmptyAndTest() {
|
||||
assertThrows(
|
||||
IllegalArgumentException.class,
|
||||
() ->
|
||||
run(
|
||||
getParamsMap(
|
||||
"runInEmpty", "",
|
||||
"forEachTestTld", "")));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testFailure_runInEmptyAndReal() {
|
||||
assertThrows(
|
||||
IllegalArgumentException.class,
|
||||
() ->
|
||||
run(
|
||||
getParamsMap(
|
||||
"runInEmpty", "",
|
||||
"forEachRealTld", "")));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testFailure_runInEmptyAndExclude() {
|
||||
assertThrows(
|
||||
IllegalArgumentException.class,
|
||||
() ->
|
||||
run(
|
||||
getParamsMap(
|
||||
"runInEmpty", "",
|
||||
"exclude", "foo")));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSuccess_additionalArgsFlowThroughToPostParams() {
|
||||
run(getParamsMap("forEachTestTld", "", "newkey", "newval"));
|
||||
assertTasksEnqueued(QUEUE,
|
||||
new TaskMatcher().url("/the/servlet").param("newkey", "newval"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSuccess_returnHttpResponse() {
|
||||
run(getParamsMap("forEachRealTld", "", "endpoint", "/the/servlet"));
|
||||
|
||||
List<TaskStateInfo> taskList =
|
||||
LocalTaskQueueTestConfig.getLocalTaskQueue().getQueueStateInfo().get(QUEUE).getTaskInfo();
|
||||
|
||||
assertThat(taskList).hasSize(3);
|
||||
String expectedResponse = String.format(
|
||||
"OK: Launched the following 3 tasks in queue the-queue\n"
|
||||
+ "- Task: '%s', tld: 'com', endpoint: '/the/servlet'\n"
|
||||
+ "- Task: '%s', tld: 'net', endpoint: '/the/servlet'\n"
|
||||
+ "- Task: '%s', tld: 'org', endpoint: '/the/servlet'\n",
|
||||
taskList.get(0).getTaskName(),
|
||||
taskList.get(1).getTaskName(),
|
||||
taskList.get(2).getTaskName());
|
||||
assertThat(response.getPayload()).isEqualTo(expectedResponse);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSuccess_returnHttpResponse_runInEmpty() {
|
||||
run(getParamsMap("runInEmpty", "", "endpoint", "/the/servlet"));
|
||||
|
||||
List<TaskStateInfo> taskList =
|
||||
LocalTaskQueueTestConfig.getLocalTaskQueue().getQueueStateInfo().get(QUEUE).getTaskInfo();
|
||||
|
||||
assertThat(taskList).hasSize(1);
|
||||
String expectedResponse = String.format(
|
||||
"OK: Launched the following 1 tasks in queue the-queue\n"
|
||||
+ "- Task: '%s', tld: '', endpoint: '/the/servlet'\n",
|
||||
taskList.get(0).getTaskName());
|
||||
assertThat(response.getPayload()).isEqualTo(expectedResponse);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
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 = "dns",
|
||||
srcs = glob(["*.java"]),
|
||||
deps = [
|
||||
"//java/google/registry/config",
|
||||
"//java/google/registry/cron",
|
||||
"//java/google/registry/dns",
|
||||
"//java/google/registry/dns:constants",
|
||||
"//java/google/registry/dns/writer",
|
||||
"//java/google/registry/model",
|
||||
"//java/google/registry/module/backend",
|
||||
"//java/google/registry/request",
|
||||
"//java/google/registry/request/lock",
|
||||
"//java/google/registry/util",
|
||||
"//javatests/google/registry/testing",
|
||||
"//third_party/objectify:objectify-v4_1",
|
||||
"@com_google_appengine_api_1_0_sdk",
|
||||
"@com_google_appengine_api_stubs",
|
||||
"@com_google_dagger",
|
||||
"@com_google_guava",
|
||||
"@com_google_truth",
|
||||
"@com_google_truth_extensions_truth_java8_extension",
|
||||
"@javax_servlet_api",
|
||||
"@joda_time",
|
||||
"@junit",
|
||||
"@org_mockito_core",
|
||||
],
|
||||
)
|
||||
|
||||
GenTestRules(
|
||||
name = "GeneratedTestRules",
|
||||
test_files = glob(["*Test.java"]),
|
||||
deps = [":dns"],
|
||||
)
|
||||
@@ -0,0 +1,120 @@
|
||||
// Copyright 2017 The Nomulus Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package google.registry.dns;
|
||||
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
import static google.registry.testing.DatastoreHelper.createTld;
|
||||
import static google.registry.testing.DatastoreHelper.persistActiveDomain;
|
||||
import static google.registry.testing.DatastoreHelper.persistActiveSubordinateHost;
|
||||
import static google.registry.testing.JUnitBackports.assertThrows;
|
||||
import static google.registry.testing.TaskQueueHelper.assertDnsTasksEnqueued;
|
||||
import static google.registry.testing.TaskQueueHelper.assertNoDnsTasksEnqueued;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import google.registry.model.ofy.Ofy;
|
||||
import google.registry.request.HttpException.NotFoundException;
|
||||
import google.registry.request.RequestModule;
|
||||
import google.registry.testing.AppEngineRule;
|
||||
import google.registry.testing.FakeClock;
|
||||
import google.registry.testing.InjectRule;
|
||||
import java.io.PrintWriter;
|
||||
import java.io.StringWriter;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
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;
|
||||
|
||||
/** Unit tests for Dagger injection of the DNS package. */
|
||||
@RunWith(JUnit4.class)
|
||||
public final class DnsInjectionTest {
|
||||
|
||||
@Rule
|
||||
public final AppEngineRule appEngine = AppEngineRule.builder()
|
||||
.withDatastore()
|
||||
.withTaskQueue()
|
||||
.build();
|
||||
|
||||
@Rule
|
||||
public final InjectRule inject = new InjectRule();
|
||||
|
||||
private final HttpServletRequest req = mock(HttpServletRequest.class);
|
||||
private final HttpServletResponse rsp = mock(HttpServletResponse.class);
|
||||
private final StringWriter httpOutput = new StringWriter();
|
||||
private final FakeClock clock = new FakeClock(DateTime.parse("2014-01-01TZ"));
|
||||
private DnsTestComponent component;
|
||||
private DnsQueue dnsQueue;
|
||||
|
||||
@Before
|
||||
public void setUp() throws Exception {
|
||||
inject.setStaticField(Ofy.class, "clock", clock);
|
||||
when(rsp.getWriter()).thenReturn(new PrintWriter(httpOutput));
|
||||
component = DaggerDnsTestComponent.builder()
|
||||
.requestModule(new RequestModule(req, rsp))
|
||||
.build();
|
||||
dnsQueue = component.dnsQueue();
|
||||
createTld("lol");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testReadDnsQueueAction_injectsAndWorks() {
|
||||
persistActiveSubordinateHost("ns1.example.lol", persistActiveDomain("example.lol"));
|
||||
clock.advanceOneMilli();
|
||||
dnsQueue.addDomainRefreshTask("example.lol");
|
||||
when(req.getParameter("tld")).thenReturn("lol");
|
||||
component.readDnsQueueAction().run();
|
||||
assertNoDnsTasksEnqueued();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRefreshDns_domain_injectsAndWorks() {
|
||||
persistActiveDomain("example.lol");
|
||||
when(req.getParameter("type")).thenReturn("domain");
|
||||
when(req.getParameter("name")).thenReturn("example.lol");
|
||||
component.refreshDns().run();
|
||||
assertDnsTasksEnqueued("example.lol");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRefreshDns_missingDomain_throwsNotFound() {
|
||||
when(req.getParameter("type")).thenReturn("domain");
|
||||
when(req.getParameter("name")).thenReturn("example.lol");
|
||||
NotFoundException thrown =
|
||||
assertThrows(NotFoundException.class, () -> component.refreshDns().run());
|
||||
assertThat(thrown).hasMessageThat().contains("domain example.lol not found");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRefreshDns_host_injectsAndWorks() {
|
||||
persistActiveSubordinateHost("ns1.example.lol", persistActiveDomain("example.lol"));
|
||||
when(req.getParameter("type")).thenReturn("host");
|
||||
when(req.getParameter("name")).thenReturn("ns1.example.lol");
|
||||
component.refreshDns().run();
|
||||
assertDnsTasksEnqueued("ns1.example.lol");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRefreshDns_missingHost_throwsNotFound() {
|
||||
when(req.getParameter("type")).thenReturn("host");
|
||||
when(req.getParameter("name")).thenReturn("ns1.example.lol");
|
||||
NotFoundException thrown =
|
||||
assertThrows(NotFoundException.class, () -> component.refreshDns().run());
|
||||
assertThat(thrown).hasMessageThat().contains("host ns1.example.lol not found");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
// Copyright 2017 The Nomulus Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package google.registry.dns;
|
||||
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
import static google.registry.testing.DatastoreHelper.createTld;
|
||||
import static google.registry.testing.JUnitBackports.assertThrows;
|
||||
import static google.registry.testing.TaskQueueHelper.assertNoTasksEnqueued;
|
||||
import static google.registry.testing.TaskQueueHelper.assertTasksEnqueued;
|
||||
|
||||
import google.registry.testing.AppEngineRule;
|
||||
import google.registry.testing.FakeClock;
|
||||
import google.registry.testing.TaskQueueHelper.TaskMatcher;
|
||||
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;
|
||||
|
||||
/** Unit tests for {@link DnsQueue}. */
|
||||
@RunWith(JUnit4.class)
|
||||
public class DnsQueueTest {
|
||||
|
||||
@Rule
|
||||
public final AppEngineRule appEngine = AppEngineRule.builder()
|
||||
.withDatastore()
|
||||
.withTaskQueue()
|
||||
.build();
|
||||
private DnsQueue dnsQueue;
|
||||
private final FakeClock clock = new FakeClock(DateTime.parse("2010-01-01T10:00:00Z"));
|
||||
|
||||
@Before
|
||||
public void init() {
|
||||
dnsQueue = DnsQueue.createForTesting(clock);
|
||||
dnsQueue.leaseTasksBatchSize = 10;
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test_addHostRefreshTask_success() {
|
||||
createTld("tld");
|
||||
dnsQueue.addHostRefreshTask("octopus.tld");
|
||||
assertTasksEnqueued(
|
||||
"dns-pull",
|
||||
new TaskMatcher()
|
||||
.param("Target-Type", "HOST")
|
||||
.param("Target-Name", "octopus.tld")
|
||||
.param("Create-Time", "2010-01-01T10:00:00.000Z")
|
||||
.param("tld", "tld"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test_addHostRefreshTask_failsOnUnknownTld() {
|
||||
IllegalArgumentException thrown =
|
||||
assertThrows(
|
||||
IllegalArgumentException.class,
|
||||
() -> {
|
||||
try {
|
||||
dnsQueue.addHostRefreshTask("octopus.notatld");
|
||||
} finally {
|
||||
assertNoTasksEnqueued("dns-pull");
|
||||
}
|
||||
});
|
||||
assertThat(thrown)
|
||||
.hasMessageThat()
|
||||
.contains("octopus.notatld is not a subordinate host to a known tld");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test_addDomainRefreshTask_success() {
|
||||
createTld("tld");
|
||||
dnsQueue.addDomainRefreshTask("octopus.tld");
|
||||
assertTasksEnqueued(
|
||||
"dns-pull",
|
||||
new TaskMatcher()
|
||||
.param("Target-Type", "DOMAIN")
|
||||
.param("Target-Name", "octopus.tld")
|
||||
.param("Create-Time", "2010-01-01T10:00:00.000Z")
|
||||
.param("tld", "tld"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test_addDomainRefreshTask_failsOnUnknownTld() {
|
||||
IllegalArgumentException thrown =
|
||||
assertThrows(
|
||||
IllegalArgumentException.class,
|
||||
() -> {
|
||||
try {
|
||||
dnsQueue.addDomainRefreshTask("fake.notatld");
|
||||
} finally {
|
||||
assertNoTasksEnqueued("dns-pull");
|
||||
}
|
||||
});
|
||||
assertThat(thrown).hasMessageThat().contains("TLD notatld does not exist");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
// Copyright 2017 The Nomulus Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package google.registry.dns;
|
||||
|
||||
import dagger.Component;
|
||||
import google.registry.config.RegistryConfig.ConfigModule;
|
||||
import google.registry.cron.CronModule;
|
||||
import google.registry.dns.writer.VoidDnsWriterModule;
|
||||
import google.registry.module.backend.BackendModule;
|
||||
import google.registry.request.RequestModule;
|
||||
import google.registry.util.UtilsModule;
|
||||
import javax.inject.Singleton;
|
||||
|
||||
@Singleton
|
||||
@Component(
|
||||
modules = {
|
||||
BackendModule.class,
|
||||
ConfigModule.class,
|
||||
CronModule.class,
|
||||
DnsModule.class,
|
||||
RequestModule.class,
|
||||
UtilsModule.class,
|
||||
VoidDnsWriterModule.class,
|
||||
})
|
||||
interface DnsTestComponent {
|
||||
DnsQueue dnsQueue();
|
||||
RefreshDnsAction refreshDns();
|
||||
ReadDnsQueueAction readDnsQueueAction();
|
||||
}
|
||||
@@ -0,0 +1,375 @@
|
||||
// Copyright 2017 The Nomulus Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package google.registry.dns;
|
||||
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
import static google.registry.testing.DatastoreHelper.createTld;
|
||||
import static google.registry.testing.DatastoreHelper.persistActiveDomain;
|
||||
import static google.registry.testing.DatastoreHelper.persistActiveSubordinateHost;
|
||||
import static google.registry.testing.DatastoreHelper.persistResource;
|
||||
import static google.registry.testing.JUnitBackports.assertThrows;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.doThrow;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.verifyNoMoreInteractions;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import com.google.common.collect.ImmutableMap;
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
import google.registry.dns.DnsMetrics.ActionStatus;
|
||||
import google.registry.dns.DnsMetrics.CommitStatus;
|
||||
import google.registry.dns.DnsMetrics.PublishStatus;
|
||||
import google.registry.dns.writer.DnsWriter;
|
||||
import google.registry.model.domain.DomainBase;
|
||||
import google.registry.model.ofy.Ofy;
|
||||
import google.registry.model.registry.Registry;
|
||||
import google.registry.request.HttpException.ServiceUnavailableException;
|
||||
import google.registry.request.lock.LockHandler;
|
||||
import google.registry.testing.AppEngineRule;
|
||||
import google.registry.testing.FakeClock;
|
||||
import google.registry.testing.FakeLockHandler;
|
||||
import google.registry.testing.InjectRule;
|
||||
import org.joda.time.DateTime;
|
||||
import org.joda.time.Duration;
|
||||
import org.junit.Before;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.junit.runners.JUnit4;
|
||||
|
||||
/** Unit tests for {@link PublishDnsUpdatesAction}. */
|
||||
@RunWith(JUnit4.class)
|
||||
public class PublishDnsUpdatesActionTest {
|
||||
|
||||
@Rule
|
||||
public final AppEngineRule appEngine = AppEngineRule.builder()
|
||||
.withDatastore()
|
||||
.withTaskQueue()
|
||||
.build();
|
||||
|
||||
@Rule
|
||||
public final InjectRule inject = new InjectRule();
|
||||
private final FakeClock clock = new FakeClock(DateTime.parse("1971-01-01TZ"));
|
||||
private final FakeLockHandler lockHandler = new FakeLockHandler(true);
|
||||
private final DnsWriter dnsWriter = mock(DnsWriter.class);
|
||||
private final DnsMetrics dnsMetrics = mock(DnsMetrics.class);
|
||||
private final DnsQueue dnsQueue = mock(DnsQueue.class);
|
||||
private PublishDnsUpdatesAction action;
|
||||
|
||||
@Before
|
||||
public void setUp() {
|
||||
inject.setStaticField(Ofy.class, "clock", clock);
|
||||
createTld("xn--q9jyb4c");
|
||||
persistResource(
|
||||
Registry.get("xn--q9jyb4c")
|
||||
.asBuilder()
|
||||
.setDnsWriters(ImmutableSet.of("correctWriter"))
|
||||
.build());
|
||||
DomainBase domain1 = persistActiveDomain("example.xn--q9jyb4c");
|
||||
persistActiveSubordinateHost("ns1.example.xn--q9jyb4c", domain1);
|
||||
persistActiveSubordinateHost("ns2.example.xn--q9jyb4c", domain1);
|
||||
DomainBase domain2 = persistActiveDomain("example2.xn--q9jyb4c");
|
||||
persistActiveSubordinateHost("ns1.example.xn--q9jyb4c", domain2);
|
||||
clock.advanceOneMilli();
|
||||
}
|
||||
|
||||
private PublishDnsUpdatesAction createAction(String tld) {
|
||||
PublishDnsUpdatesAction action = new PublishDnsUpdatesAction();
|
||||
action.timeout = Duration.standardSeconds(10);
|
||||
action.tld = tld;
|
||||
action.hosts = ImmutableSet.of();
|
||||
action.domains = ImmutableSet.of();
|
||||
action.itemsCreateTime = clock.nowUtc().minusHours(2);
|
||||
action.enqueuedTime = clock.nowUtc().minusHours(1);
|
||||
action.dnsWriter = "correctWriter";
|
||||
action.dnsWriterProxy = new DnsWriterProxy(ImmutableMap.of("correctWriter", dnsWriter));
|
||||
action.dnsMetrics = dnsMetrics;
|
||||
action.dnsQueue = dnsQueue;
|
||||
action.lockIndex = 1;
|
||||
action.numPublishLocks = 1;
|
||||
action.lockHandler = lockHandler;
|
||||
action.clock = clock;
|
||||
return action;
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testHost_published() {
|
||||
action = createAction("xn--q9jyb4c");
|
||||
action.hosts = ImmutableSet.of("ns1.example.xn--q9jyb4c");
|
||||
|
||||
action.run();
|
||||
|
||||
verify(dnsWriter).publishHost("ns1.example.xn--q9jyb4c");
|
||||
verify(dnsWriter).commit();
|
||||
verifyNoMoreInteractions(dnsWriter);
|
||||
verify(dnsMetrics).incrementPublishDomainRequests("xn--q9jyb4c", 0, PublishStatus.ACCEPTED);
|
||||
verify(dnsMetrics).incrementPublishDomainRequests("xn--q9jyb4c", 0, PublishStatus.REJECTED);
|
||||
verify(dnsMetrics).incrementPublishHostRequests("xn--q9jyb4c", 1, PublishStatus.ACCEPTED);
|
||||
verify(dnsMetrics).incrementPublishHostRequests("xn--q9jyb4c", 0, PublishStatus.REJECTED);
|
||||
verify(dnsMetrics)
|
||||
.recordCommit("xn--q9jyb4c", "correctWriter", CommitStatus.SUCCESS, Duration.ZERO, 0, 1);
|
||||
verify(dnsMetrics)
|
||||
.recordActionResult(
|
||||
"xn--q9jyb4c",
|
||||
"correctWriter",
|
||||
ActionStatus.SUCCESS,
|
||||
1,
|
||||
Duration.standardHours(2),
|
||||
Duration.standardHours(1));
|
||||
verifyNoMoreInteractions(dnsMetrics);
|
||||
verifyNoMoreInteractions(dnsQueue);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDomain_published() {
|
||||
action = createAction("xn--q9jyb4c");
|
||||
action.domains = ImmutableSet.of("example.xn--q9jyb4c");
|
||||
|
||||
action.run();
|
||||
|
||||
verify(dnsWriter).publishDomain("example.xn--q9jyb4c");
|
||||
verify(dnsWriter).commit();
|
||||
verifyNoMoreInteractions(dnsWriter);
|
||||
verify(dnsMetrics).incrementPublishDomainRequests("xn--q9jyb4c", 1, PublishStatus.ACCEPTED);
|
||||
verify(dnsMetrics).incrementPublishDomainRequests("xn--q9jyb4c", 0, PublishStatus.REJECTED);
|
||||
verify(dnsMetrics).incrementPublishHostRequests("xn--q9jyb4c", 0, PublishStatus.ACCEPTED);
|
||||
verify(dnsMetrics).incrementPublishHostRequests("xn--q9jyb4c", 0, PublishStatus.REJECTED);
|
||||
verify(dnsMetrics)
|
||||
.recordCommit("xn--q9jyb4c", "correctWriter", CommitStatus.SUCCESS, Duration.ZERO, 1, 0);
|
||||
verify(dnsMetrics)
|
||||
.recordActionResult(
|
||||
"xn--q9jyb4c",
|
||||
"correctWriter",
|
||||
ActionStatus.SUCCESS,
|
||||
1,
|
||||
Duration.standardHours(2),
|
||||
Duration.standardHours(1));
|
||||
verifyNoMoreInteractions(dnsMetrics);
|
||||
verifyNoMoreInteractions(dnsQueue);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAction_acquiresCorrectLock() {
|
||||
persistResource(Registry.get("xn--q9jyb4c").asBuilder().setNumDnsPublishLocks(4).build());
|
||||
action = createAction("xn--q9jyb4c");
|
||||
action.lockIndex = 2;
|
||||
action.numPublishLocks = 4;
|
||||
action.domains = ImmutableSet.of("example.xn--q9jyb4c");
|
||||
LockHandler mockLockHandler = mock(LockHandler.class);
|
||||
when(mockLockHandler.executeWithLocks(any(), any(), any(), any())).thenReturn(true);
|
||||
action.lockHandler = mockLockHandler;
|
||||
|
||||
action.run();
|
||||
|
||||
verify(mockLockHandler)
|
||||
.executeWithLocks(
|
||||
action, "xn--q9jyb4c", Duration.standardSeconds(10), "DNS updates-lock 2 of 4");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testPublish_commitFails() {
|
||||
action = createAction("xn--q9jyb4c");
|
||||
action.domains = ImmutableSet.of("example.xn--q9jyb4c", "example2.xn--q9jyb4c");
|
||||
action.hosts =
|
||||
ImmutableSet.of(
|
||||
"ns1.example.xn--q9jyb4c", "ns2.example.xn--q9jyb4c", "ns1.example2.xn--q9jyb4c");
|
||||
doThrow(new RuntimeException()).when(dnsWriter).commit();
|
||||
|
||||
assertThrows(RuntimeException.class, action::run);
|
||||
|
||||
verify(dnsMetrics).incrementPublishDomainRequests("xn--q9jyb4c", 2, PublishStatus.ACCEPTED);
|
||||
verify(dnsMetrics).incrementPublishDomainRequests("xn--q9jyb4c", 0, PublishStatus.REJECTED);
|
||||
verify(dnsMetrics).incrementPublishHostRequests("xn--q9jyb4c", 3, PublishStatus.ACCEPTED);
|
||||
verify(dnsMetrics).incrementPublishHostRequests("xn--q9jyb4c", 0, PublishStatus.REJECTED);
|
||||
verify(dnsMetrics)
|
||||
.recordCommit("xn--q9jyb4c", "correctWriter", CommitStatus.FAILURE, Duration.ZERO, 2, 3);
|
||||
verify(dnsMetrics)
|
||||
.recordActionResult(
|
||||
"xn--q9jyb4c",
|
||||
"correctWriter",
|
||||
ActionStatus.COMMIT_FAILURE,
|
||||
5,
|
||||
Duration.standardHours(2),
|
||||
Duration.standardHours(1));
|
||||
verifyNoMoreInteractions(dnsMetrics);
|
||||
verifyNoMoreInteractions(dnsQueue);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testHostAndDomain_published() {
|
||||
action = createAction("xn--q9jyb4c");
|
||||
action.domains = ImmutableSet.of("example.xn--q9jyb4c", "example2.xn--q9jyb4c");
|
||||
action.hosts = ImmutableSet.of(
|
||||
"ns1.example.xn--q9jyb4c", "ns2.example.xn--q9jyb4c", "ns1.example2.xn--q9jyb4c");
|
||||
|
||||
action.run();
|
||||
|
||||
verify(dnsWriter).publishDomain("example.xn--q9jyb4c");
|
||||
verify(dnsWriter).publishDomain("example2.xn--q9jyb4c");
|
||||
verify(dnsWriter).publishHost("ns1.example.xn--q9jyb4c");
|
||||
verify(dnsWriter).publishHost("ns2.example.xn--q9jyb4c");
|
||||
verify(dnsWriter).publishHost("ns1.example2.xn--q9jyb4c");
|
||||
verify(dnsWriter).commit();
|
||||
verifyNoMoreInteractions(dnsWriter);
|
||||
verify(dnsMetrics).incrementPublishDomainRequests("xn--q9jyb4c", 2, PublishStatus.ACCEPTED);
|
||||
verify(dnsMetrics).incrementPublishDomainRequests("xn--q9jyb4c", 0, PublishStatus.REJECTED);
|
||||
verify(dnsMetrics).incrementPublishHostRequests("xn--q9jyb4c", 3, PublishStatus.ACCEPTED);
|
||||
verify(dnsMetrics).incrementPublishHostRequests("xn--q9jyb4c", 0, PublishStatus.REJECTED);
|
||||
verify(dnsMetrics)
|
||||
.recordCommit("xn--q9jyb4c", "correctWriter", CommitStatus.SUCCESS, Duration.ZERO, 2, 3);
|
||||
verify(dnsMetrics)
|
||||
.recordActionResult(
|
||||
"xn--q9jyb4c",
|
||||
"correctWriter",
|
||||
ActionStatus.SUCCESS,
|
||||
5,
|
||||
Duration.standardHours(2),
|
||||
Duration.standardHours(1));
|
||||
verifyNoMoreInteractions(dnsMetrics);
|
||||
verifyNoMoreInteractions(dnsQueue);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testWrongTld_notPublished() {
|
||||
action = createAction("xn--q9jyb4c");
|
||||
action.domains = ImmutableSet.of("example.com", "example2.com");
|
||||
action.hosts = ImmutableSet.of("ns1.example.com", "ns2.example.com", "ns1.example2.com");
|
||||
|
||||
action.run();
|
||||
|
||||
verify(dnsWriter).commit();
|
||||
verifyNoMoreInteractions(dnsWriter);
|
||||
verify(dnsMetrics).incrementPublishDomainRequests("xn--q9jyb4c", 0, PublishStatus.ACCEPTED);
|
||||
verify(dnsMetrics).incrementPublishDomainRequests("xn--q9jyb4c", 2, PublishStatus.REJECTED);
|
||||
verify(dnsMetrics).incrementPublishHostRequests("xn--q9jyb4c", 0, PublishStatus.ACCEPTED);
|
||||
verify(dnsMetrics).incrementPublishHostRequests("xn--q9jyb4c", 3, PublishStatus.REJECTED);
|
||||
verify(dnsMetrics)
|
||||
.recordCommit("xn--q9jyb4c", "correctWriter", CommitStatus.SUCCESS, Duration.ZERO, 0, 0);
|
||||
verify(dnsMetrics)
|
||||
.recordActionResult(
|
||||
"xn--q9jyb4c",
|
||||
"correctWriter",
|
||||
ActionStatus.SUCCESS,
|
||||
5,
|
||||
Duration.standardHours(2),
|
||||
Duration.standardHours(1));
|
||||
verifyNoMoreInteractions(dnsMetrics);
|
||||
verifyNoMoreInteractions(dnsQueue);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testLockIsntAvailable() {
|
||||
action = createAction("xn--q9jyb4c");
|
||||
action.domains = ImmutableSet.of("example.com", "example2.com");
|
||||
action.hosts = ImmutableSet.of("ns1.example.com", "ns2.example.com", "ns1.example2.com");
|
||||
action.lockHandler = new FakeLockHandler(false);
|
||||
|
||||
ServiceUnavailableException thrown =
|
||||
assertThrows(ServiceUnavailableException.class, action::run);
|
||||
|
||||
assertThat(thrown).hasMessageThat().contains("Lock failure");
|
||||
verifyNoMoreInteractions(dnsWriter);
|
||||
verify(dnsMetrics)
|
||||
.recordActionResult(
|
||||
"xn--q9jyb4c",
|
||||
"correctWriter",
|
||||
ActionStatus.LOCK_FAILURE,
|
||||
5,
|
||||
Duration.standardHours(2),
|
||||
Duration.standardHours(1));
|
||||
verifyNoMoreInteractions(dnsMetrics);
|
||||
verifyNoMoreInteractions(dnsQueue);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testParam_invalidLockIndex() {
|
||||
persistResource(Registry.get("xn--q9jyb4c").asBuilder().setNumDnsPublishLocks(4).build());
|
||||
action = createAction("xn--q9jyb4c");
|
||||
action.domains = ImmutableSet.of("example.com");
|
||||
action.hosts = ImmutableSet.of("ns1.example.com");
|
||||
action.lockIndex = 5;
|
||||
action.numPublishLocks = 4;
|
||||
|
||||
action.run();
|
||||
|
||||
verifyNoMoreInteractions(dnsWriter);
|
||||
verify(dnsMetrics)
|
||||
.recordActionResult(
|
||||
"xn--q9jyb4c",
|
||||
"correctWriter",
|
||||
ActionStatus.BAD_LOCK_INDEX,
|
||||
2,
|
||||
Duration.standardHours(2),
|
||||
Duration.standardHours(1));
|
||||
verifyNoMoreInteractions(dnsMetrics);
|
||||
verify(dnsQueue).addDomainRefreshTask("example.com");
|
||||
verify(dnsQueue).addHostRefreshTask("ns1.example.com");
|
||||
verifyNoMoreInteractions(dnsQueue);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRegistryParam_mismatchedMaxLocks() {
|
||||
persistResource(Registry.get("xn--q9jyb4c").asBuilder().setNumDnsPublishLocks(4).build());
|
||||
action = createAction("xn--q9jyb4c");
|
||||
action.domains = ImmutableSet.of("example.com");
|
||||
action.hosts = ImmutableSet.of("ns1.example.com");
|
||||
action.lockIndex = 3;
|
||||
action.numPublishLocks = 5;
|
||||
|
||||
action.run();
|
||||
|
||||
verifyNoMoreInteractions(dnsWriter);
|
||||
verify(dnsMetrics)
|
||||
.recordActionResult(
|
||||
"xn--q9jyb4c",
|
||||
"correctWriter",
|
||||
ActionStatus.BAD_LOCK_INDEX,
|
||||
2,
|
||||
Duration.standardHours(2),
|
||||
Duration.standardHours(1));
|
||||
verifyNoMoreInteractions(dnsMetrics);
|
||||
verify(dnsQueue).addDomainRefreshTask("example.com");
|
||||
verify(dnsQueue).addHostRefreshTask("ns1.example.com");
|
||||
verifyNoMoreInteractions(dnsQueue);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testWrongDnsWriter() {
|
||||
action = createAction("xn--q9jyb4c");
|
||||
action.domains = ImmutableSet.of("example.com", "example2.com");
|
||||
action.hosts = ImmutableSet.of("ns1.example.com", "ns2.example.com", "ns1.example2.com");
|
||||
action.dnsWriter = "wrongWriter";
|
||||
|
||||
action.run();
|
||||
|
||||
verifyNoMoreInteractions(dnsWriter);
|
||||
verify(dnsMetrics)
|
||||
.recordActionResult(
|
||||
"xn--q9jyb4c",
|
||||
"wrongWriter",
|
||||
ActionStatus.BAD_WRITER,
|
||||
5,
|
||||
Duration.standardHours(2),
|
||||
Duration.standardHours(1));
|
||||
verifyNoMoreInteractions(dnsMetrics);
|
||||
verify(dnsQueue).addDomainRefreshTask("example.com");
|
||||
verify(dnsQueue).addDomainRefreshTask("example2.com");
|
||||
verify(dnsQueue).addHostRefreshTask("ns1.example.com");
|
||||
verify(dnsQueue).addHostRefreshTask("ns2.example.com");
|
||||
verify(dnsQueue).addHostRefreshTask("ns1.example2.com");
|
||||
verifyNoMoreInteractions(dnsQueue);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,524 @@
|
||||
// Copyright 2017 The Nomulus Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package google.registry.dns;
|
||||
|
||||
import static com.google.appengine.api.taskqueue.QueueFactory.getQueue;
|
||||
import static com.google.common.collect.ImmutableList.toImmutableList;
|
||||
import static com.google.common.collect.Lists.transform;
|
||||
import static com.google.common.collect.MoreCollectors.onlyElement;
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
import static com.google.common.truth.Truth8.assertThat;
|
||||
import static google.registry.dns.DnsConstants.DNS_PUBLISH_PUSH_QUEUE_NAME;
|
||||
import static google.registry.dns.DnsConstants.DNS_PULL_QUEUE_NAME;
|
||||
import static google.registry.dns.DnsConstants.DNS_TARGET_CREATE_TIME_PARAM;
|
||||
import static google.registry.dns.DnsConstants.DNS_TARGET_NAME_PARAM;
|
||||
import static google.registry.dns.DnsConstants.DNS_TARGET_TYPE_PARAM;
|
||||
import static google.registry.request.RequestParameters.PARAM_TLD;
|
||||
import static google.registry.testing.DatastoreHelper.createTlds;
|
||||
import static google.registry.testing.DatastoreHelper.persistResource;
|
||||
import static google.registry.testing.TaskQueueHelper.assertNoTasksEnqueued;
|
||||
import static google.registry.testing.TaskQueueHelper.assertTasksEnqueued;
|
||||
import static google.registry.testing.TaskQueueHelper.getQueuedParams;
|
||||
|
||||
import com.google.appengine.api.taskqueue.QueueFactory;
|
||||
import com.google.appengine.api.taskqueue.TaskOptions;
|
||||
import com.google.appengine.api.taskqueue.TaskOptions.Method;
|
||||
import com.google.common.base.Joiner;
|
||||
import com.google.common.base.Splitter;
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.google.common.collect.ImmutableMultimap;
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
import com.google.common.hash.Hashing;
|
||||
import com.google.common.net.InternetDomainName;
|
||||
import google.registry.dns.DnsConstants.TargetType;
|
||||
import google.registry.model.registry.Registry;
|
||||
import google.registry.model.registry.Registry.TldType;
|
||||
import google.registry.testing.AppEngineRule;
|
||||
import google.registry.testing.FakeClock;
|
||||
import google.registry.testing.TaskQueueHelper.TaskMatcher;
|
||||
import google.registry.util.Retrier;
|
||||
import google.registry.util.TaskQueueUtils;
|
||||
import java.util.Map.Entry;
|
||||
import java.util.Optional;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.IntStream;
|
||||
import org.joda.time.DateTime;
|
||||
import org.joda.time.Duration;
|
||||
import org.junit.Before;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.junit.runners.JUnit4;
|
||||
|
||||
/** Unit tests for {@link ReadDnsQueueAction}. */
|
||||
@RunWith(JUnit4.class)
|
||||
public class ReadDnsQueueActionTest {
|
||||
|
||||
private static final int TEST_TLD_UPDATE_BATCH_SIZE = 100;
|
||||
private DnsQueue dnsQueue;
|
||||
// Because of a bug in the queue test environment - b/73372999 - we must set the fake date of the
|
||||
// test in the future. Set to year 3000 so it'll remain in the future for a very long time.
|
||||
private FakeClock clock = new FakeClock(DateTime.parse("3000-01-01TZ"));
|
||||
|
||||
@Rule
|
||||
public final AppEngineRule appEngine = AppEngineRule.builder()
|
||||
.withDatastore()
|
||||
.withTaskQueue(Joiner.on('\n').join(
|
||||
"<?xml version=\"1.0\" encoding=\"UTF-8\"?>",
|
||||
"<queue-entries>",
|
||||
" <queue>",
|
||||
" <name>dns-publish</name>",
|
||||
" <rate>1/s</rate>",
|
||||
" </queue>",
|
||||
" <queue>",
|
||||
" <name>dns-pull</name>",
|
||||
" <mode>pull</mode>",
|
||||
" </queue>",
|
||||
"</queue-entries>"))
|
||||
.withClock(clock)
|
||||
.build();
|
||||
|
||||
@Before
|
||||
public void before() {
|
||||
// Because of b/73372999 - the FakeClock can't be in the past, or the TaskQueues stop working.
|
||||
// To make sure it's never in the past, we set the date far-far into the future
|
||||
clock.setTo(DateTime.parse("3000-01-01TZ"));
|
||||
createTlds("com", "net", "example", "multilock.uk");
|
||||
persistResource(
|
||||
Registry.get("com").asBuilder().setDnsWriters(ImmutableSet.of("comWriter")).build());
|
||||
persistResource(
|
||||
Registry.get("net").asBuilder().setDnsWriters(ImmutableSet.of("netWriter")).build());
|
||||
persistResource(
|
||||
Registry.get("example")
|
||||
.asBuilder()
|
||||
.setTldType(TldType.TEST)
|
||||
.setDnsWriters(ImmutableSet.of("exampleWriter"))
|
||||
.build());
|
||||
persistResource(
|
||||
Registry.get("multilock.uk")
|
||||
.asBuilder()
|
||||
.setNumDnsPublishLocks(1000)
|
||||
.setDnsWriters(ImmutableSet.of("multilockWriter"))
|
||||
.build());
|
||||
dnsQueue = DnsQueue.createForTesting(clock);
|
||||
}
|
||||
|
||||
private void run() {
|
||||
ReadDnsQueueAction action = new ReadDnsQueueAction();
|
||||
action.tldUpdateBatchSize = TEST_TLD_UPDATE_BATCH_SIZE;
|
||||
action.requestedMaximumDuration = Duration.standardSeconds(10);
|
||||
action.clock = clock;
|
||||
action.dnsQueue = dnsQueue;
|
||||
action.dnsPublishPushQueue = QueueFactory.getQueue(DNS_PUBLISH_PUSH_QUEUE_NAME);
|
||||
action.hashFunction = Hashing.murmur3_32();
|
||||
action.taskQueueUtils = new TaskQueueUtils(new Retrier(null, 1));
|
||||
action.jitterSeconds = Optional.empty();
|
||||
// Advance the time a little, to ensure that leaseTasks() returns all tasks.
|
||||
clock.advanceBy(Duration.standardHours(1));
|
||||
|
||||
action.run();
|
||||
}
|
||||
|
||||
private static TaskOptions createRefreshTask(String name, TargetType type) {
|
||||
TaskOptions options =
|
||||
TaskOptions.Builder.withMethod(Method.PULL)
|
||||
.param(DNS_TARGET_TYPE_PARAM, type.toString())
|
||||
.param(DNS_TARGET_NAME_PARAM, name)
|
||||
.param(DNS_TARGET_CREATE_TIME_PARAM, "3000-01-01TZ");
|
||||
String tld = InternetDomainName.from(name).parts().reverse().get(0);
|
||||
return options.param("tld", tld);
|
||||
}
|
||||
|
||||
private static TaskMatcher createDomainRefreshTaskMatcher(String name) {
|
||||
return new TaskMatcher()
|
||||
.param(DNS_TARGET_NAME_PARAM, name)
|
||||
.param(DNS_TARGET_TYPE_PARAM, TargetType.DOMAIN.toString());
|
||||
}
|
||||
|
||||
private void assertTldsEnqueuedInPushQueue(ImmutableMultimap<String, String> tldsToDnsWriters) {
|
||||
// By default, the publishDnsUpdates tasks will be enqueued one hour after the update items were
|
||||
// created in the pull queue. This is because of the clock.advanceBy in run()
|
||||
assertTasksEnqueued(
|
||||
DNS_PUBLISH_PUSH_QUEUE_NAME,
|
||||
transform(
|
||||
tldsToDnsWriters.entries().asList(),
|
||||
(Entry<String, String> tldToDnsWriter) ->
|
||||
new TaskMatcher()
|
||||
.url(PublishDnsUpdatesAction.PATH)
|
||||
.param("tld", tldToDnsWriter.getKey())
|
||||
.param("dnsWriter", tldToDnsWriter.getValue())
|
||||
.param("itemsCreated", "3000-01-01T00:00:00.000Z")
|
||||
.param("enqueued", "3000-01-01T01:00:00.000Z")
|
||||
// Single-lock TLDs should use lock 1 of 1 by default
|
||||
.param("lockIndex", "1")
|
||||
.param("numPublishLocks", "1")
|
||||
.header("content-type", "application/x-www-form-urlencoded")));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSuccess_methodPostIsDefault() {
|
||||
dnsQueue.addDomainRefreshTask("domain.com");
|
||||
dnsQueue.addDomainRefreshTask("domain.net");
|
||||
dnsQueue.addDomainRefreshTask("domain.example");
|
||||
|
||||
run();
|
||||
|
||||
assertNoTasksEnqueued(DNS_PULL_QUEUE_NAME);
|
||||
assertTasksEnqueued(
|
||||
DNS_PUBLISH_PUSH_QUEUE_NAME,
|
||||
new TaskMatcher().method("POST"),
|
||||
new TaskMatcher().method("POST"),
|
||||
new TaskMatcher().method("POST"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSuccess_allSingleLockTlds() {
|
||||
dnsQueue.addDomainRefreshTask("domain.com");
|
||||
dnsQueue.addDomainRefreshTask("domain.net");
|
||||
dnsQueue.addDomainRefreshTask("domain.example");
|
||||
|
||||
run();
|
||||
|
||||
assertNoTasksEnqueued(DNS_PULL_QUEUE_NAME);
|
||||
assertTldsEnqueuedInPushQueue(
|
||||
ImmutableMultimap.of("com", "comWriter", "net", "netWriter", "example", "exampleWriter"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSuccess_moreUpdatesThanQueueBatchSize() {
|
||||
// The task queue has a batch size of 1000 (that's the maximum number of items you can lease at
|
||||
// once).
|
||||
ImmutableList<String> domains =
|
||||
IntStream.range(0, 1500)
|
||||
.mapToObj(i -> String.format("domain_%04d.com", i))
|
||||
.collect(toImmutableList());
|
||||
domains.forEach(dnsQueue::addDomainRefreshTask);
|
||||
|
||||
run();
|
||||
|
||||
assertNoTasksEnqueued(DNS_PULL_QUEUE_NAME);
|
||||
ImmutableList<ImmutableMultimap<String, String>> queuedParams =
|
||||
getQueuedParams(DNS_PUBLISH_PUSH_QUEUE_NAME);
|
||||
// ReadDnsQueueAction batches items per TLD in batches of size 100.
|
||||
// So for 1500 items in the DNS queue, we expect 15 items in the push queue
|
||||
assertThat(queuedParams).hasSize(15);
|
||||
// Check all the expected domains are indeed enqueued
|
||||
assertThat(
|
||||
queuedParams
|
||||
.stream()
|
||||
.map(params -> params.get("domains").stream().collect(onlyElement()))
|
||||
.flatMap(values -> Splitter.on(',').splitToList(values).stream()))
|
||||
.containsExactlyElementsIn(domains);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSuccess_twoDnsWriters() {
|
||||
persistResource(
|
||||
Registry.get("com")
|
||||
.asBuilder()
|
||||
.setDnsWriters(ImmutableSet.of("comWriter", "otherWriter"))
|
||||
.build());
|
||||
dnsQueue.addDomainRefreshTask("domain.com");
|
||||
|
||||
run();
|
||||
|
||||
assertNoTasksEnqueued(DNS_PULL_QUEUE_NAME);
|
||||
assertTldsEnqueuedInPushQueue(ImmutableMultimap.of("com", "comWriter", "com", "otherWriter"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSuccess_differentUpdateTimes_usesMinimum() {
|
||||
clock.setTo(DateTime.parse("3000-02-03TZ"));
|
||||
dnsQueue.addDomainRefreshTask("domain1.com");
|
||||
clock.setTo(DateTime.parse("3000-02-04TZ"));
|
||||
dnsQueue.addDomainRefreshTask("domain2.com");
|
||||
clock.setTo(DateTime.parse("3000-02-05TZ"));
|
||||
dnsQueue.addDomainRefreshTask("domain3.com");
|
||||
|
||||
run();
|
||||
|
||||
assertNoTasksEnqueued(DNS_PULL_QUEUE_NAME);
|
||||
assertThat(getQueuedParams(DNS_PUBLISH_PUSH_QUEUE_NAME)).hasSize(1);
|
||||
assertThat(getQueuedParams(DNS_PUBLISH_PUSH_QUEUE_NAME).get(0))
|
||||
.containsExactly(
|
||||
"enqueued", "3000-02-05T01:00:00.000Z",
|
||||
"itemsCreated", "3000-02-03T00:00:00.000Z",
|
||||
"tld", "com",
|
||||
"dnsWriter", "comWriter",
|
||||
"domains", "domain1.com,domain2.com,domain3.com",
|
||||
"hosts", "",
|
||||
"lockIndex", "1",
|
||||
"numPublishLocks", "1");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSuccess_oneTldPaused_returnedToQueue() {
|
||||
persistResource(Registry.get("net").asBuilder().setDnsPaused(true).build());
|
||||
dnsQueue.addDomainRefreshTask("domain.com");
|
||||
dnsQueue.addDomainRefreshTask("domain.net");
|
||||
dnsQueue.addDomainRefreshTask("domain.example");
|
||||
|
||||
run();
|
||||
|
||||
assertTasksEnqueued(DNS_PULL_QUEUE_NAME, createDomainRefreshTaskMatcher("domain.net"));
|
||||
assertTldsEnqueuedInPushQueue(
|
||||
ImmutableMultimap.of("com", "comWriter", "example", "exampleWriter"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSuccess_oneTldUnknown_returnedToQueue() {
|
||||
dnsQueue.addDomainRefreshTask("domain.com");
|
||||
dnsQueue.addDomainRefreshTask("domain.example");
|
||||
QueueFactory.getQueue(DNS_PULL_QUEUE_NAME)
|
||||
.add(
|
||||
TaskOptions.Builder.withDefaults()
|
||||
.method(Method.PULL)
|
||||
.param(DNS_TARGET_TYPE_PARAM, TargetType.DOMAIN.toString())
|
||||
.param(DNS_TARGET_NAME_PARAM, "domain.unknown")
|
||||
.param(DNS_TARGET_CREATE_TIME_PARAM, "3000-01-01TZ")
|
||||
.param(PARAM_TLD, "unknown"));
|
||||
|
||||
run();
|
||||
|
||||
assertTasksEnqueued(DNS_PULL_QUEUE_NAME, createDomainRefreshTaskMatcher("domain.unknown"));
|
||||
assertTldsEnqueuedInPushQueue(
|
||||
ImmutableMultimap.of("com", "comWriter", "example", "exampleWriter"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSuccess_corruptTaskTldMismatch_published() {
|
||||
// TODO(mcilwain): what's the correct action to take in this case?
|
||||
dnsQueue.addDomainRefreshTask("domain.com");
|
||||
dnsQueue.addDomainRefreshTask("domain.example");
|
||||
QueueFactory.getQueue(DNS_PULL_QUEUE_NAME)
|
||||
.add(
|
||||
TaskOptions.Builder.withDefaults()
|
||||
.method(Method.PULL)
|
||||
.param(DNS_TARGET_TYPE_PARAM, TargetType.DOMAIN.toString())
|
||||
.param(DNS_TARGET_NAME_PARAM, "domain.wrongtld")
|
||||
.param(DNS_TARGET_CREATE_TIME_PARAM, "3000-01-01TZ")
|
||||
.param(PARAM_TLD, "net"));
|
||||
|
||||
run();
|
||||
|
||||
assertNoTasksEnqueued(DNS_PULL_QUEUE_NAME);
|
||||
assertTldsEnqueuedInPushQueue(
|
||||
ImmutableMultimap.of("com", "comWriter", "example", "exampleWriter", "net", "netWriter"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSuccess_corruptTaskNoTld_discarded() {
|
||||
dnsQueue.addDomainRefreshTask("domain.com");
|
||||
dnsQueue.addDomainRefreshTask("domain.example");
|
||||
QueueFactory.getQueue(DNS_PULL_QUEUE_NAME)
|
||||
.add(
|
||||
TaskOptions.Builder.withDefaults()
|
||||
.method(Method.PULL)
|
||||
.param(DNS_TARGET_TYPE_PARAM, TargetType.DOMAIN.toString())
|
||||
.param(DNS_TARGET_NAME_PARAM, "domain.net"));
|
||||
|
||||
run();
|
||||
|
||||
// The corrupt task isn't in the pull queue, but also isn't in the push queue
|
||||
assertNoTasksEnqueued(DNS_PULL_QUEUE_NAME);
|
||||
assertTldsEnqueuedInPushQueue(
|
||||
ImmutableMultimap.of("com", "comWriter", "example", "exampleWriter"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSuccess_corruptTaskNoName_discarded() {
|
||||
dnsQueue.addDomainRefreshTask("domain.com");
|
||||
dnsQueue.addDomainRefreshTask("domain.example");
|
||||
QueueFactory.getQueue(DNS_PULL_QUEUE_NAME)
|
||||
.add(
|
||||
TaskOptions.Builder.withDefaults()
|
||||
.method(Method.PULL)
|
||||
.param(DNS_TARGET_TYPE_PARAM, TargetType.DOMAIN.toString())
|
||||
.param(PARAM_TLD, "net"));
|
||||
|
||||
run();
|
||||
|
||||
// The corrupt task isn't in the pull queue, but also isn't in the push queue
|
||||
assertNoTasksEnqueued(DNS_PULL_QUEUE_NAME);
|
||||
assertTldsEnqueuedInPushQueue(
|
||||
ImmutableMultimap.of("com", "comWriter", "example", "exampleWriter"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSuccess_corruptTaskNoType_discarded() {
|
||||
dnsQueue.addDomainRefreshTask("domain.com");
|
||||
dnsQueue.addDomainRefreshTask("domain.example");
|
||||
QueueFactory.getQueue(DNS_PULL_QUEUE_NAME)
|
||||
.add(
|
||||
TaskOptions.Builder.withDefaults()
|
||||
.method(Method.PULL)
|
||||
.param(DNS_TARGET_NAME_PARAM, "domain.net")
|
||||
.param(PARAM_TLD, "net"));
|
||||
|
||||
run();
|
||||
|
||||
// The corrupt task isn't in the pull queue, but also isn't in the push queue
|
||||
assertNoTasksEnqueued(DNS_PULL_QUEUE_NAME);
|
||||
assertTldsEnqueuedInPushQueue(
|
||||
ImmutableMultimap.of("com", "comWriter", "example", "exampleWriter"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSuccess_corruptTaskWrongType_discarded() {
|
||||
dnsQueue.addDomainRefreshTask("domain.com");
|
||||
dnsQueue.addDomainRefreshTask("domain.example");
|
||||
QueueFactory.getQueue(DNS_PULL_QUEUE_NAME)
|
||||
.add(
|
||||
TaskOptions.Builder.withDefaults()
|
||||
.method(Method.PULL)
|
||||
.param(DNS_TARGET_TYPE_PARAM, "Wrong type")
|
||||
.param(DNS_TARGET_NAME_PARAM, "domain.net")
|
||||
.param(PARAM_TLD, "net"));
|
||||
|
||||
run();
|
||||
|
||||
// The corrupt task isn't in the pull queue, but also isn't in the push queue
|
||||
assertNoTasksEnqueued(DNS_PULL_QUEUE_NAME);
|
||||
assertTldsEnqueuedInPushQueue(
|
||||
ImmutableMultimap.of("com", "comWriter", "example", "exampleWriter"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSuccess_zone_getsIgnored() {
|
||||
dnsQueue.addHostRefreshTask("ns1.domain.com");
|
||||
dnsQueue.addDomainRefreshTask("domain.net");
|
||||
dnsQueue.addZoneRefreshTask("example");
|
||||
|
||||
run();
|
||||
|
||||
assertNoTasksEnqueued(DNS_PULL_QUEUE_NAME);
|
||||
assertTasksEnqueued(
|
||||
DNS_PUBLISH_PUSH_QUEUE_NAME,
|
||||
new TaskMatcher().url(PublishDnsUpdatesAction.PATH).param("domains", "domain.net"),
|
||||
new TaskMatcher().url(PublishDnsUpdatesAction.PATH).param("hosts", "ns1.domain.com"));
|
||||
}
|
||||
|
||||
private static String makeCommaSeparatedRange(int from, int to, String format) {
|
||||
return IntStream.range(from, to)
|
||||
.mapToObj(i -> String.format(format, i))
|
||||
.collect(Collectors.joining(","));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSuccess_manyDomainsAndHosts() {
|
||||
for (int i = 0; i < 150; i++) {
|
||||
// 0: domain; 1: host 1; 2: host 2
|
||||
for (int thingType = 0; thingType < 3; thingType++) {
|
||||
for (String tld : ImmutableList.of("com", "net")) {
|
||||
String domainName = String.format("domain%04d.%s", i, tld);
|
||||
switch (thingType) {
|
||||
case 1:
|
||||
getQueue(DNS_PULL_QUEUE_NAME)
|
||||
.add(createRefreshTask("ns1." + domainName, TargetType.HOST));
|
||||
break;
|
||||
case 2:
|
||||
getQueue(DNS_PULL_QUEUE_NAME)
|
||||
.add(createRefreshTask("ns2." + domainName, TargetType.HOST));
|
||||
break;
|
||||
default:
|
||||
dnsQueue.addDomainRefreshTask(domainName);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
run();
|
||||
|
||||
assertNoTasksEnqueued(DNS_PULL_QUEUE_NAME);
|
||||
assertTasksEnqueued(
|
||||
DNS_PUBLISH_PUSH_QUEUE_NAME,
|
||||
new TaskMatcher()
|
||||
.url(PublishDnsUpdatesAction.PATH)
|
||||
.param("domains", makeCommaSeparatedRange(0, 100, "domain%04d.com"))
|
||||
.param("hosts", ""),
|
||||
new TaskMatcher()
|
||||
.url(PublishDnsUpdatesAction.PATH)
|
||||
.param("domains", makeCommaSeparatedRange(100, 150, "domain%04d.com"))
|
||||
.param("hosts", makeCommaSeparatedRange(0, 50, "ns1.domain%04d.com")),
|
||||
new TaskMatcher()
|
||||
.url(PublishDnsUpdatesAction.PATH)
|
||||
.param("domains", "")
|
||||
.param("hosts", makeCommaSeparatedRange(50, 150, "ns1.domain%04d.com")),
|
||||
new TaskMatcher()
|
||||
.url(PublishDnsUpdatesAction.PATH)
|
||||
.param("domains", "")
|
||||
.param("hosts", makeCommaSeparatedRange(0, 100, "ns2.domain%04d.com")),
|
||||
new TaskMatcher()
|
||||
.url(PublishDnsUpdatesAction.PATH)
|
||||
.param("domains", "")
|
||||
.param("hosts", makeCommaSeparatedRange(100, 150, "ns2.domain%04d.com")),
|
||||
new TaskMatcher()
|
||||
.url(PublishDnsUpdatesAction.PATH)
|
||||
.param("domains", makeCommaSeparatedRange(0, 100, "domain%04d.net"))
|
||||
.param("hosts", ""),
|
||||
new TaskMatcher()
|
||||
.url(PublishDnsUpdatesAction.PATH)
|
||||
.param("domains", makeCommaSeparatedRange(100, 150, "domain%04d.net"))
|
||||
.param("hosts", makeCommaSeparatedRange(0, 50, "ns1.domain%04d.net")),
|
||||
new TaskMatcher()
|
||||
.url(PublishDnsUpdatesAction.PATH)
|
||||
.param("domains", "")
|
||||
.param("hosts", makeCommaSeparatedRange(50, 150, "ns1.domain%04d.net")),
|
||||
new TaskMatcher()
|
||||
.url(PublishDnsUpdatesAction.PATH)
|
||||
.param("domains", "")
|
||||
.param("hosts", makeCommaSeparatedRange(0, 100, "ns2.domain%04d.net")),
|
||||
new TaskMatcher()
|
||||
.url(PublishDnsUpdatesAction.PATH)
|
||||
.param("domains", "")
|
||||
.param("hosts", makeCommaSeparatedRange(100, 150, "ns2.domain%04d.net")));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSuccess_lockGroupsHostBySuperordinateDomain() {
|
||||
dnsQueue.addDomainRefreshTask("hello.multilock.uk");
|
||||
dnsQueue.addHostRefreshTask("ns1.abc.hello.multilock.uk");
|
||||
dnsQueue.addHostRefreshTask("ns2.hello.multilock.uk");
|
||||
dnsQueue.addDomainRefreshTask("another.multilock.uk");
|
||||
dnsQueue.addHostRefreshTask("ns3.def.another.multilock.uk");
|
||||
dnsQueue.addHostRefreshTask("ns4.another.multilock.uk");
|
||||
|
||||
run();
|
||||
|
||||
assertNoTasksEnqueued(DNS_PULL_QUEUE_NAME);
|
||||
// Expect two different groups; in-balliwick hosts are locked with their superordinate domains.
|
||||
assertTasksEnqueued(
|
||||
DNS_PUBLISH_PUSH_QUEUE_NAME,
|
||||
new TaskMatcher()
|
||||
.url(PublishDnsUpdatesAction.PATH)
|
||||
.param("tld", "multilock.uk")
|
||||
.param("dnsWriter", "multilockWriter")
|
||||
.param("itemsCreated", "3000-01-01T00:00:00.000Z")
|
||||
.param("enqueued", "3000-01-01T01:00:00.000Z")
|
||||
.param("domains", "hello.multilock.uk")
|
||||
.param("hosts", "ns1.abc.hello.multilock.uk,ns2.hello.multilock.uk")
|
||||
.header("content-type", "application/x-www-form-urlencoded"),
|
||||
new TaskMatcher()
|
||||
.url(PublishDnsUpdatesAction.PATH)
|
||||
.param("tld", "multilock.uk")
|
||||
.param("dnsWriter", "multilockWriter")
|
||||
.param("itemsCreated", "3000-01-01T00:00:00.000Z")
|
||||
.param("enqueued", "3000-01-01T01:00:00.000Z")
|
||||
.param("domains", "another.multilock.uk")
|
||||
.param("hosts", "ns3.def.another.multilock.uk,ns4.another.multilock.uk")
|
||||
.header("content-type", "application/x-www-form-urlencoded"));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
// Copyright 2017 The Nomulus Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package google.registry.dns;
|
||||
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
import static google.registry.testing.DatastoreHelper.createTld;
|
||||
import static google.registry.testing.DatastoreHelper.persistActiveDomain;
|
||||
import static google.registry.testing.DatastoreHelper.persistActiveHost;
|
||||
import static google.registry.testing.DatastoreHelper.persistActiveSubordinateHost;
|
||||
import static google.registry.testing.JUnitBackports.assertThrows;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.verifyNoMoreInteractions;
|
||||
|
||||
import google.registry.dns.DnsConstants.TargetType;
|
||||
import google.registry.model.domain.DomainBase;
|
||||
import google.registry.request.HttpException.BadRequestException;
|
||||
import google.registry.request.HttpException.NotFoundException;
|
||||
import google.registry.testing.AppEngineRule;
|
||||
import google.registry.testing.FakeClock;
|
||||
import org.junit.Before;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.junit.runners.JUnit4;
|
||||
|
||||
/** Unit tests for {@link RefreshDnsAction}. */
|
||||
@RunWith(JUnit4.class)
|
||||
public class RefreshDnsActionTest {
|
||||
|
||||
@Rule
|
||||
public final AppEngineRule appEngine =
|
||||
AppEngineRule.builder().withDatastore().withTaskQueue().build();
|
||||
|
||||
private final DnsQueue dnsQueue = mock(DnsQueue.class);
|
||||
private final FakeClock clock = new FakeClock();
|
||||
|
||||
private void run(TargetType type, String name) {
|
||||
new RefreshDnsAction(name, type, clock, dnsQueue).run();
|
||||
}
|
||||
|
||||
@Before
|
||||
public void before() {
|
||||
createTld("xn--q9jyb4c");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSuccess_host() {
|
||||
DomainBase domain = persistActiveDomain("example.xn--q9jyb4c");
|
||||
persistActiveSubordinateHost("ns1.example.xn--q9jyb4c", domain);
|
||||
run(TargetType.HOST, "ns1.example.xn--q9jyb4c");
|
||||
verify(dnsQueue).addHostRefreshTask("ns1.example.xn--q9jyb4c");
|
||||
verifyNoMoreInteractions(dnsQueue);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSuccess_externalHostNotEnqueued() {
|
||||
persistActiveDomain("example.xn--q9jyb4c");
|
||||
persistActiveHost("ns1.example.xn--q9jyb4c");
|
||||
BadRequestException thrown =
|
||||
assertThrows(
|
||||
BadRequestException.class,
|
||||
() -> {
|
||||
try {
|
||||
run(TargetType.HOST, "ns1.example.xn--q9jyb4c");
|
||||
} finally {
|
||||
verifyNoMoreInteractions(dnsQueue);
|
||||
}
|
||||
});
|
||||
assertThat(thrown)
|
||||
.hasMessageThat()
|
||||
.contains("ns1.example.xn--q9jyb4c isn't a subordinate hostname");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSuccess_domain() {
|
||||
persistActiveDomain("example.xn--q9jyb4c");
|
||||
run(TargetType.DOMAIN, "example.xn--q9jyb4c");
|
||||
verify(dnsQueue).addDomainRefreshTask("example.xn--q9jyb4c");
|
||||
verifyNoMoreInteractions(dnsQueue);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testFailure_unqualifiedName() {
|
||||
assertThrows(BadRequestException.class, () -> run(TargetType.DOMAIN, "example"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testFailure_hostDoesNotExist() {
|
||||
assertThrows(NotFoundException.class, () -> run(TargetType.HOST, "ns1.example.xn--q9jyb4c"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testFailure_domainDoesNotExist() {
|
||||
assertThrows(NotFoundException.class, () -> run(TargetType.DOMAIN, "example.xn--q9jyb4c"));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
// Copyright 2017 The Nomulus Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package google.registry.dns.writer;
|
||||
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
import static google.registry.testing.JUnitBackports.assertThrows;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.junit.runners.JUnit4;
|
||||
|
||||
/** Unit tests for {@link BaseDnsWriter}. */
|
||||
@RunWith(JUnit4.class)
|
||||
public class BaseDnsWriterTest {
|
||||
|
||||
static class StubDnsWriter extends BaseDnsWriter {
|
||||
|
||||
int commitCallCount = 0;
|
||||
|
||||
@Override
|
||||
protected void commitUnchecked() {
|
||||
commitCallCount++;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void publishDomain(String domainName) {
|
||||
// No op
|
||||
}
|
||||
|
||||
@Override
|
||||
public void publishHost(String hostName) {
|
||||
// No op
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test_cannotBeCalledTwice() {
|
||||
StubDnsWriter writer = new StubDnsWriter();
|
||||
assertThat(writer.commitCallCount).isEqualTo(0);
|
||||
writer.commit();
|
||||
assertThat(writer.commitCallCount).isEqualTo(1);
|
||||
IllegalStateException thrown = assertThrows(IllegalStateException.class, writer::commit);
|
||||
assertThat(thrown).hasMessageThat().isEqualTo("commit() has already been called");
|
||||
assertThat(writer.commitCallCount).isEqualTo(1);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
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 = "clouddns",
|
||||
srcs = glob(["*Test.java"]),
|
||||
deps = [
|
||||
"//java/google/registry/dns/writer/clouddns",
|
||||
"//java/google/registry/model",
|
||||
"//java/google/registry/util",
|
||||
"//javatests/google/registry/testing",
|
||||
"//third_party/objectify:objectify-v4_1",
|
||||
"@com_google_apis_google_api_services_dns",
|
||||
"@com_google_guava",
|
||||
"@com_google_truth",
|
||||
"@com_google_truth_extensions_truth_java8_extension",
|
||||
"@joda_time",
|
||||
"@junit",
|
||||
"@org_mockito_core",
|
||||
],
|
||||
)
|
||||
|
||||
GenTestRules(
|
||||
name = "GeneratedTestRules",
|
||||
test_files = glob(["*Test.java"]),
|
||||
deps = [":clouddns"],
|
||||
)
|
||||
@@ -0,0 +1,524 @@
|
||||
// Copyright 2017 The Nomulus Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package google.registry.dns.writer.clouddns;
|
||||
|
||||
import static com.google.common.collect.ImmutableList.toImmutableList;
|
||||
import static com.google.common.io.BaseEncoding.base16;
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
import static google.registry.testing.DatastoreHelper.createTld;
|
||||
import static google.registry.testing.DatastoreHelper.newDomainBase;
|
||||
import static google.registry.testing.DatastoreHelper.newHostResource;
|
||||
import static google.registry.testing.DatastoreHelper.persistResource;
|
||||
import static org.mockito.ArgumentMatchers.anyString;
|
||||
import static org.mockito.Mockito.doThrow;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.spy;
|
||||
import static org.mockito.Mockito.times;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import com.google.api.services.dns.Dns;
|
||||
import com.google.api.services.dns.model.Change;
|
||||
import com.google.api.services.dns.model.ResourceRecordSet;
|
||||
import com.google.api.services.dns.model.ResourceRecordSetsListResponse;
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
import com.google.common.collect.Sets;
|
||||
import com.google.common.net.InetAddresses;
|
||||
import com.google.common.util.concurrent.RateLimiter;
|
||||
import com.googlecode.objectify.Key;
|
||||
import google.registry.dns.writer.clouddns.CloudDnsWriter.ZoneStateException;
|
||||
import google.registry.model.domain.DomainBase;
|
||||
import google.registry.model.domain.secdns.DelegationSignerData;
|
||||
import google.registry.model.eppcommon.StatusValue;
|
||||
import google.registry.model.host.HostResource;
|
||||
import google.registry.testing.AppEngineRule;
|
||||
import google.registry.util.Retrier;
|
||||
import google.registry.util.SystemClock;
|
||||
import google.registry.util.SystemSleeper;
|
||||
import java.io.IOException;
|
||||
import java.net.Inet4Address;
|
||||
import java.net.Inet6Address;
|
||||
import java.net.InetAddress;
|
||||
import org.joda.time.Duration;
|
||||
import org.junit.Before;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.junit.runners.JUnit4;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.mockito.ArgumentMatchers;
|
||||
import org.mockito.Captor;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.MockitoJUnit;
|
||||
import org.mockito.junit.MockitoRule;
|
||||
|
||||
/** Test case for {@link CloudDnsWriter}. */
|
||||
@RunWith(JUnit4.class)
|
||||
public class CloudDnsWriterTest {
|
||||
|
||||
@Rule public final AppEngineRule appEngine = AppEngineRule.builder().withDatastore().build();
|
||||
@Rule public final MockitoRule mocks = MockitoJUnit.rule();
|
||||
|
||||
private static final Inet4Address IPv4 = (Inet4Address) InetAddresses.forString("127.0.0.1");
|
||||
private static final Inet6Address IPv6 = (Inet6Address) InetAddresses.forString("::1");
|
||||
private static final Duration DEFAULT_A_TTL = Duration.standardSeconds(11);
|
||||
private static final Duration DEFAULT_NS_TTL = Duration.standardSeconds(222);
|
||||
private static final Duration DEFAULT_DS_TTL = Duration.standardSeconds(3333);
|
||||
|
||||
@Mock private Dns dnsConnection;
|
||||
@Mock private Dns.ResourceRecordSets resourceRecordSets;
|
||||
@Mock private Dns.Changes changes;
|
||||
@Mock private Dns.Changes.Create createChangeRequest;
|
||||
@Captor ArgumentCaptor<String> zoneNameCaptor;
|
||||
@Captor ArgumentCaptor<Change> changeCaptor;
|
||||
|
||||
private CloudDnsWriter writer;
|
||||
private ImmutableSet<ResourceRecordSet> stubZone;
|
||||
|
||||
/*
|
||||
* Because of multi-threading in the CloudDnsWriter, we need to return a different instance of
|
||||
* List for every request, with its own ArgumentCaptor. Otherwise, we can't separate the arguments
|
||||
* of the various Lists
|
||||
*/
|
||||
private Dns.ResourceRecordSets.List newListResourceRecordSetsRequestMock() throws Exception {
|
||||
Dns.ResourceRecordSets.List listResourceRecordSetsRequest =
|
||||
mock(Dns.ResourceRecordSets.List.class);
|
||||
ArgumentCaptor<String> recordNameCaptor = ArgumentCaptor.forClass(String.class);
|
||||
when(listResourceRecordSetsRequest.setName(recordNameCaptor.capture()))
|
||||
.thenReturn(listResourceRecordSetsRequest);
|
||||
// Return records from our stub zone when a request to list the records is executed
|
||||
when(listResourceRecordSetsRequest.execute())
|
||||
.thenAnswer(
|
||||
invocationOnMock ->
|
||||
new ResourceRecordSetsListResponse()
|
||||
.setRrsets(
|
||||
stubZone
|
||||
.stream()
|
||||
.filter(
|
||||
rs ->
|
||||
rs != null && rs.getName().equals(recordNameCaptor.getValue()))
|
||||
.collect(toImmutableList())));
|
||||
return listResourceRecordSetsRequest;
|
||||
}
|
||||
|
||||
@Before
|
||||
public void setUp() throws Exception {
|
||||
createTld("tld");
|
||||
writer =
|
||||
new CloudDnsWriter(
|
||||
dnsConnection,
|
||||
"projectId",
|
||||
"triple.secret.tld", // used by testInvalidZoneNames()
|
||||
DEFAULT_A_TTL,
|
||||
DEFAULT_NS_TTL,
|
||||
DEFAULT_DS_TTL,
|
||||
RateLimiter.create(20),
|
||||
10, // max num threads
|
||||
new SystemClock(),
|
||||
new Retrier(new SystemSleeper(), 5));
|
||||
|
||||
// Create an empty zone.
|
||||
stubZone = ImmutableSet.of();
|
||||
|
||||
when(dnsConnection.changes()).thenReturn(changes);
|
||||
when(dnsConnection.resourceRecordSets()).thenReturn(resourceRecordSets);
|
||||
when(resourceRecordSets.list(anyString(), anyString()))
|
||||
.thenAnswer(invocationOnMock -> newListResourceRecordSetsRequestMock());
|
||||
when(changes.create(anyString(), zoneNameCaptor.capture(), changeCaptor.capture()))
|
||||
.thenReturn(createChangeRequest);
|
||||
// Change our stub zone when a request to change the records is executed
|
||||
when(createChangeRequest.execute())
|
||||
.thenAnswer(
|
||||
invocationOnMock -> {
|
||||
Change requestedChange = changeCaptor.getValue();
|
||||
ImmutableSet<ResourceRecordSet> toDelete =
|
||||
ImmutableSet.copyOf(requestedChange.getDeletions());
|
||||
ImmutableSet<ResourceRecordSet> toAdd =
|
||||
ImmutableSet.copyOf(requestedChange.getAdditions());
|
||||
// Fail if the records to delete has records that aren't in the stub zone.
|
||||
// This matches documented Google Cloud DNS behavior.
|
||||
if (!Sets.difference(toDelete, stubZone).isEmpty()) {
|
||||
throw new IOException();
|
||||
}
|
||||
stubZone =
|
||||
Sets.union(Sets.difference(stubZone, toDelete).immutableCopy(), toAdd)
|
||||
.immutableCopy();
|
||||
return requestedChange;
|
||||
});
|
||||
}
|
||||
|
||||
private void verifyZone(ImmutableSet<ResourceRecordSet> expectedRecords) {
|
||||
// Trigger zone changes
|
||||
writer.commit();
|
||||
|
||||
assertThat(stubZone).containsExactlyElementsIn(expectedRecords);
|
||||
}
|
||||
|
||||
/** Returns a a zone cut with records for a domain and given nameservers, with no glue records. */
|
||||
private static ImmutableSet<ResourceRecordSet> fakeDomainRecords(
|
||||
String domainName, String... nameservers) {
|
||||
ImmutableSet.Builder<ResourceRecordSet> recordSetBuilder = new ImmutableSet.Builder<>();
|
||||
if (nameservers.length > 0) {
|
||||
recordSetBuilder.add(
|
||||
new ResourceRecordSet()
|
||||
.setKind("dns#resourceRecordSet")
|
||||
.setType("NS")
|
||||
.setName(domainName + ".")
|
||||
.setTtl(222)
|
||||
.setRrdatas(ImmutableList.copyOf(nameservers)));
|
||||
}
|
||||
return recordSetBuilder.build();
|
||||
}
|
||||
|
||||
/** Returns a a zone cut with records for a domain */
|
||||
private static ImmutableSet<ResourceRecordSet> fakeDomainRecords(
|
||||
String domainName,
|
||||
int v4InBailiwickNameservers,
|
||||
int v6InBailiwickNameservers,
|
||||
int externalNameservers,
|
||||
int dsRecords) {
|
||||
ImmutableSet.Builder<ResourceRecordSet> recordSetBuilder = new ImmutableSet.Builder<>();
|
||||
|
||||
// Add IPv4 in-bailiwick nameservers
|
||||
if (v4InBailiwickNameservers > 0) {
|
||||
ImmutableList.Builder<String> nameserverHostnames = new ImmutableList.Builder<>();
|
||||
for (int i = 0; i < v4InBailiwickNameservers; i++) {
|
||||
nameserverHostnames.add(i + ".ip4." + domainName + ".");
|
||||
}
|
||||
|
||||
recordSetBuilder.add(
|
||||
new ResourceRecordSet()
|
||||
.setKind("dns#resourceRecordSet")
|
||||
.setType("NS")
|
||||
.setName(domainName + ".")
|
||||
.setTtl(222)
|
||||
.setRrdatas(nameserverHostnames.build()));
|
||||
|
||||
// Add glue for IPv4 in-bailiwick nameservers
|
||||
for (int i = 0; i < v4InBailiwickNameservers; i++) {
|
||||
recordSetBuilder.add(
|
||||
new ResourceRecordSet()
|
||||
.setKind("dns#resourceRecordSet")
|
||||
.setType("A")
|
||||
.setName(i + ".ip4." + domainName + ".")
|
||||
.setTtl(11)
|
||||
.setRrdatas(ImmutableList.of("127.0.0.1")));
|
||||
}
|
||||
}
|
||||
|
||||
// Add IPv6 in-bailiwick nameservers
|
||||
if (v6InBailiwickNameservers > 0) {
|
||||
ImmutableList.Builder<String> nameserverHostnames = new ImmutableList.Builder<>();
|
||||
for (int i = 0; i < v6InBailiwickNameservers; i++) {
|
||||
nameserverHostnames.add(i + ".ip6." + domainName + ".");
|
||||
}
|
||||
|
||||
recordSetBuilder.add(
|
||||
new ResourceRecordSet()
|
||||
.setKind("dns#resourceRecordSet")
|
||||
.setType("NS")
|
||||
.setName(domainName + ".")
|
||||
.setTtl(222)
|
||||
.setRrdatas(nameserverHostnames.build()));
|
||||
// Add glue for IPv6 in-bailiwick nameservers
|
||||
for (int i = 0; i < v6InBailiwickNameservers; i++) {
|
||||
recordSetBuilder.add(
|
||||
new ResourceRecordSet()
|
||||
.setKind("dns#resourceRecordSet")
|
||||
.setType("AAAA")
|
||||
.setName(i + ".ip6." + domainName + ".")
|
||||
.setTtl(11)
|
||||
.setRrdatas(ImmutableList.of("0:0:0:0:0:0:0:1")));
|
||||
}
|
||||
}
|
||||
|
||||
// Add external nameservers
|
||||
if (externalNameservers > 0) {
|
||||
ImmutableList.Builder<String> nameserverHostnames = new ImmutableList.Builder<>();
|
||||
for (int i = 0; i < externalNameservers; i++) {
|
||||
nameserverHostnames.add(i + ".external.");
|
||||
}
|
||||
|
||||
recordSetBuilder.add(
|
||||
new ResourceRecordSet()
|
||||
.setKind("dns#resourceRecordSet")
|
||||
.setType("NS")
|
||||
.setName(domainName + ".")
|
||||
.setTtl(222)
|
||||
.setRrdatas(nameserverHostnames.build()));
|
||||
}
|
||||
|
||||
// Add DS records
|
||||
if (dsRecords > 0) {
|
||||
ImmutableList.Builder<String> dsRecordData = new ImmutableList.Builder<>();
|
||||
|
||||
for (int i = 0; i < dsRecords; i++) {
|
||||
dsRecordData.add(
|
||||
DelegationSignerData.create(i, 3, 1, base16().decode("1234567890ABCDEF")).toRrData());
|
||||
}
|
||||
recordSetBuilder.add(
|
||||
new ResourceRecordSet()
|
||||
.setKind("dns#resourceRecordSet")
|
||||
.setType("DS")
|
||||
.setName(domainName + ".")
|
||||
.setTtl(3333)
|
||||
.setRrdatas(dsRecordData.build()));
|
||||
}
|
||||
|
||||
return recordSetBuilder.build();
|
||||
}
|
||||
|
||||
/** Returns a domain to be persisted in Datastore. */
|
||||
private static DomainBase fakeDomain(
|
||||
String domainName, ImmutableSet<HostResource> nameservers, int numDsRecords) {
|
||||
ImmutableSet.Builder<DelegationSignerData> dsDataBuilder = new ImmutableSet.Builder<>();
|
||||
|
||||
for (int i = 0; i < numDsRecords; i++) {
|
||||
dsDataBuilder.add(DelegationSignerData.create(i, 3, 1, base16().decode("1234567890ABCDEF")));
|
||||
}
|
||||
|
||||
ImmutableSet.Builder<Key<HostResource>> hostResourceRefBuilder = new ImmutableSet.Builder<>();
|
||||
for (HostResource nameserver : nameservers) {
|
||||
hostResourceRefBuilder.add(Key.create(nameserver));
|
||||
}
|
||||
|
||||
return newDomainBase(domainName)
|
||||
.asBuilder()
|
||||
.setNameservers(hostResourceRefBuilder.build())
|
||||
.setDsData(dsDataBuilder.build())
|
||||
.build();
|
||||
}
|
||||
|
||||
/** Returns a nameserver used for its NS record. */
|
||||
private static HostResource fakeHost(String nameserver, InetAddress... addresses) {
|
||||
return newHostResource(nameserver)
|
||||
.asBuilder()
|
||||
.setInetAddresses(ImmutableSet.copyOf(addresses))
|
||||
.build();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testLoadDomain_nonExistentDomain() {
|
||||
writer.publishDomain("example.tld");
|
||||
|
||||
verifyZone(ImmutableSet.of());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testLoadDomain_noDsDataOrNameservers() {
|
||||
persistResource(fakeDomain("example.tld", ImmutableSet.of(), 0));
|
||||
writer.publishDomain("example.tld");
|
||||
|
||||
verifyZone(fakeDomainRecords("example.tld", 0, 0, 0, 0));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testLoadDomain_deleteOldData() {
|
||||
stubZone = fakeDomainRecords("example.tld", 2, 2, 2, 2);
|
||||
persistResource(fakeDomain("example.tld", ImmutableSet.of(), 0));
|
||||
writer.publishDomain("example.tld");
|
||||
|
||||
verifyZone(fakeDomainRecords("example.tld", 0, 0, 0, 0));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testLoadDomain_withExternalNs() {
|
||||
persistResource(
|
||||
fakeDomain("example.tld", ImmutableSet.of(persistResource(fakeHost("0.external"))), 0));
|
||||
writer.publishDomain("example.tld");
|
||||
|
||||
verifyZone(fakeDomainRecords("example.tld", 0, 0, 1, 0));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testLoadDomain_withDsData() {
|
||||
persistResource(
|
||||
fakeDomain("example.tld", ImmutableSet.of(persistResource(fakeHost("0.external"))), 1));
|
||||
writer.publishDomain("example.tld");
|
||||
|
||||
verifyZone(fakeDomainRecords("example.tld", 0, 0, 1, 1));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testLoadDomain_withInBailiwickNs_IPv4() {
|
||||
persistResource(
|
||||
fakeDomain(
|
||||
"example.tld",
|
||||
ImmutableSet.of(persistResource(fakeHost("0.ip4.example.tld", IPv4))),
|
||||
0)
|
||||
.asBuilder()
|
||||
.addSubordinateHost("0.ip4.example.tld")
|
||||
.build());
|
||||
writer.publishDomain("example.tld");
|
||||
|
||||
verifyZone(fakeDomainRecords("example.tld", 1, 0, 0, 0));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testLoadDomain_withInBailiwickNs_IPv6() {
|
||||
persistResource(
|
||||
fakeDomain(
|
||||
"example.tld",
|
||||
ImmutableSet.of(persistResource(fakeHost("0.ip6.example.tld", IPv6))),
|
||||
0)
|
||||
.asBuilder()
|
||||
.addSubordinateHost("0.ip6.example.tld")
|
||||
.build());
|
||||
writer.publishDomain("example.tld");
|
||||
|
||||
verifyZone(fakeDomainRecords("example.tld", 0, 1, 0, 0));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testLoadDomain_withNameserveThatEndsWithDomainName() {
|
||||
persistResource(
|
||||
fakeDomain(
|
||||
"example.tld",
|
||||
ImmutableSet.of(persistResource(fakeHost("ns.another-example.tld", IPv4))),
|
||||
0));
|
||||
writer.publishDomain("example.tld");
|
||||
|
||||
verifyZone(fakeDomainRecords("example.tld", "ns.another-example.tld."));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testLoadHost_externalHost() {
|
||||
writer.publishHost("ns1.example.com");
|
||||
|
||||
// external hosts should not be published in our zone
|
||||
verifyZone(ImmutableSet.of());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testLoadHost_removeStaleNsRecords() {
|
||||
// Initialize the zone with both NS records
|
||||
stubZone = fakeDomainRecords("example.tld", 2, 0, 0, 0);
|
||||
|
||||
// Model the domain with only one NS record -- this is equivalent to creating it
|
||||
// with two NS records and then deleting one
|
||||
persistResource(
|
||||
fakeDomain(
|
||||
"example.tld",
|
||||
ImmutableSet.of(persistResource(fakeHost("0.ip4.example.tld", IPv4))),
|
||||
0)
|
||||
.asBuilder()
|
||||
.addSubordinateHost("0.ip4.example.tld")
|
||||
.build());
|
||||
|
||||
// Ask the writer to delete the deleted NS record and glue
|
||||
writer.publishHost("1.ip4.example.tld");
|
||||
|
||||
verifyZone(fakeDomainRecords("example.tld", 1, 0, 0, 0));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void retryMutateZoneOnError() {
|
||||
CloudDnsWriter spyWriter = spy(writer);
|
||||
// First call - throw. Second call - do nothing.
|
||||
doThrow(ZoneStateException.class)
|
||||
.doNothing()
|
||||
.when(spyWriter)
|
||||
.mutateZone(ArgumentMatchers.any());
|
||||
spyWriter.commit();
|
||||
|
||||
verify(spyWriter, times(2)).mutateZone(ArgumentMatchers.any());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testLoadDomain_withClientHold() {
|
||||
persistResource(
|
||||
fakeDomain(
|
||||
"example.tld",
|
||||
ImmutableSet.of(persistResource(fakeHost("0.ip4.example.tld", IPv4))),
|
||||
0)
|
||||
.asBuilder()
|
||||
.addStatusValue(StatusValue.CLIENT_HOLD)
|
||||
.build());
|
||||
writer.publishDomain("example.tld");
|
||||
|
||||
verifyZone(ImmutableSet.of());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testLoadDomain_withServerHold() {
|
||||
persistResource(
|
||||
fakeDomain(
|
||||
"example.tld",
|
||||
ImmutableSet.of(persistResource(fakeHost("0.ip4.example.tld", IPv4))),
|
||||
0)
|
||||
.asBuilder()
|
||||
.addStatusValue(StatusValue.SERVER_HOLD)
|
||||
.build());
|
||||
|
||||
writer.publishDomain("example.tld");
|
||||
|
||||
verifyZone(ImmutableSet.of());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testLoadDomain_withPendingDelete() {
|
||||
persistResource(
|
||||
fakeDomain(
|
||||
"example.tld",
|
||||
ImmutableSet.of(persistResource(fakeHost("0.ip4.example.tld", IPv4))),
|
||||
0)
|
||||
.asBuilder()
|
||||
.addStatusValue(StatusValue.PENDING_DELETE)
|
||||
.build());
|
||||
writer.publishDomain("example.tld");
|
||||
|
||||
verifyZone(ImmutableSet.of());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDuplicateRecords() {
|
||||
// In publishing DNS records, we can end up publishing information on the same host twice
|
||||
// (through a domain change and a host change), so this scenario needs to work.
|
||||
persistResource(
|
||||
fakeDomain(
|
||||
"example.tld",
|
||||
ImmutableSet.of(persistResource(fakeHost("0.ip4.example.tld", IPv4))),
|
||||
0)
|
||||
.asBuilder()
|
||||
.addSubordinateHost("0.ip4.example.tld")
|
||||
.build());
|
||||
writer.publishDomain("example.tld");
|
||||
writer.publishHost("0.ip4.example.tld");
|
||||
|
||||
verifyZone(fakeDomainRecords("example.tld", 1, 0, 0, 0));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testInvalidZoneNames() {
|
||||
createTld("triple.secret.tld");
|
||||
persistResource(
|
||||
fakeDomain(
|
||||
"example.triple.secret.tld",
|
||||
ImmutableSet.of(persistResource(fakeHost("0.ip4.example.tld", IPv4))),
|
||||
0)
|
||||
.asBuilder()
|
||||
.build());
|
||||
writer.publishDomain("example.triple.secret.tld");
|
||||
writer.commit();
|
||||
assertThat(zoneNameCaptor.getValue()).isEqualTo("triple-secret-tld");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testEmptyCommit() {
|
||||
writer.commit();
|
||||
verify(dnsConnection, times(0)).changes();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
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 = "dnsupdate",
|
||||
srcs = glob(["*.java"]),
|
||||
deps = [
|
||||
"//java/google/registry/dns/writer/dnsupdate",
|
||||
"//java/google/registry/model",
|
||||
"//javatests/google/registry/testing",
|
||||
"//third_party/objectify:objectify-v4_1",
|
||||
"@com_google_dagger",
|
||||
"@com_google_guava",
|
||||
"@com_google_truth",
|
||||
"@com_google_truth_extensions_truth_java8_extension",
|
||||
"@dnsjava",
|
||||
"@joda_time",
|
||||
"@junit",
|
||||
"@org_mockito_core",
|
||||
],
|
||||
)
|
||||
|
||||
GenTestRules(
|
||||
name = "GeneratedTestRules",
|
||||
test_files = glob(["*Test.java"]),
|
||||
deps = [":dnsupdate"],
|
||||
)
|
||||
@@ -0,0 +1,190 @@
|
||||
// Copyright 2017 The Nomulus Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package google.registry.dns.writer.dnsupdate;
|
||||
|
||||
import static com.google.common.io.BaseEncoding.base16;
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
import static google.registry.testing.JUnitBackports.assertThrows;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import com.google.common.base.VerifyException;
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.EOFException;
|
||||
import java.io.InputStream;
|
||||
import java.net.InetAddress;
|
||||
import java.net.Socket;
|
||||
import java.net.SocketTimeoutException;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.Arrays;
|
||||
import javax.net.SocketFactory;
|
||||
import org.joda.time.Duration;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.junit.runners.JUnit4;
|
||||
import org.xbill.DNS.ARecord;
|
||||
import org.xbill.DNS.DClass;
|
||||
import org.xbill.DNS.Flags;
|
||||
import org.xbill.DNS.Message;
|
||||
import org.xbill.DNS.Name;
|
||||
import org.xbill.DNS.Opcode;
|
||||
import org.xbill.DNS.Rcode;
|
||||
import org.xbill.DNS.Record;
|
||||
import org.xbill.DNS.Type;
|
||||
import org.xbill.DNS.Update;
|
||||
|
||||
/** Unit tests for {@link DnsMessageTransport}. */
|
||||
@RunWith(JUnit4.class)
|
||||
public class DnsMessageTransportTest {
|
||||
|
||||
private static final String UPDATE_HOST = "127.0.0.1";
|
||||
|
||||
private final SocketFactory mockFactory = mock(SocketFactory.class);
|
||||
private final Socket mockSocket = mock(Socket.class);
|
||||
|
||||
private Message simpleQuery;
|
||||
private Message expectedResponse;
|
||||
private DnsMessageTransport resolver;
|
||||
@Before
|
||||
public void before() throws Exception {
|
||||
simpleQuery =
|
||||
Message.newQuery(Record.newRecord(Name.fromString("example.com."), Type.A, DClass.IN));
|
||||
expectedResponse = responseMessageWithCode(simpleQuery, Rcode.NOERROR);
|
||||
when(mockFactory.createSocket(InetAddress.getByName(UPDATE_HOST), DnsMessageTransport.DNS_PORT))
|
||||
.thenReturn(mockSocket);
|
||||
resolver = new DnsMessageTransport(mockFactory, UPDATE_HOST, Duration.ZERO);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSentMessageHasCorrectLengthAndContent() throws Exception {
|
||||
ByteArrayInputStream inputStream =
|
||||
new ByteArrayInputStream(messageToBytesWithLength(expectedResponse));
|
||||
when(mockSocket.getInputStream()).thenReturn(inputStream);
|
||||
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
|
||||
when(mockSocket.getOutputStream()).thenReturn(outputStream);
|
||||
|
||||
resolver.send(simpleQuery);
|
||||
|
||||
ByteBuffer sentMessage = ByteBuffer.wrap(outputStream.toByteArray());
|
||||
int messageLength = sentMessage.getShort();
|
||||
byte[] messageData = new byte[messageLength];
|
||||
sentMessage.get(messageData);
|
||||
assertThat(messageLength).isEqualTo(simpleQuery.toWire().length);
|
||||
assertThat(base16().encode(messageData)).isEqualTo(base16().encode(simpleQuery.toWire()));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testReceivedMessageWithLengthHasCorrectContent() throws Exception {
|
||||
ByteArrayInputStream inputStream =
|
||||
new ByteArrayInputStream(messageToBytesWithLength(expectedResponse));
|
||||
when(mockSocket.getInputStream()).thenReturn(inputStream);
|
||||
when(mockSocket.getOutputStream()).thenReturn(new ByteArrayOutputStream());
|
||||
|
||||
Message actualResponse = resolver.send(simpleQuery);
|
||||
|
||||
assertThat(base16().encode(actualResponse.toWire()))
|
||||
.isEqualTo(base16().encode(expectedResponse.toWire()));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testEofReceivingResponse() throws Exception {
|
||||
byte[] messageBytes = messageToBytesWithLength(expectedResponse);
|
||||
ByteArrayInputStream inputStream =
|
||||
new ByteArrayInputStream(Arrays.copyOf(messageBytes, messageBytes.length - 1));
|
||||
when(mockSocket.getInputStream()).thenReturn(inputStream);
|
||||
when(mockSocket.getOutputStream()).thenReturn(new ByteArrayOutputStream());
|
||||
assertThrows(EOFException.class, () -> resolver.send(new Message()));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testTimeoutReceivingResponse() throws Exception {
|
||||
InputStream mockInputStream = mock(InputStream.class);
|
||||
when(mockInputStream.read()).thenThrow(new SocketTimeoutException("testing"));
|
||||
when(mockSocket.getInputStream()).thenReturn(mockInputStream);
|
||||
when(mockSocket.getOutputStream()).thenReturn(new ByteArrayOutputStream());
|
||||
|
||||
Duration testTimeout = Duration.standardSeconds(1);
|
||||
DnsMessageTransport resolver = new DnsMessageTransport(mockFactory, UPDATE_HOST, testTimeout);
|
||||
Message expectedQuery = new Message();
|
||||
assertThrows(SocketTimeoutException.class, () -> resolver.send(expectedQuery));
|
||||
verify(mockSocket).setSoTimeout((int) testTimeout.getMillis());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSentMessageTooLongThrowsException() throws Exception {
|
||||
Update oversize = new Update(Name.fromString("tld", Name.root));
|
||||
for (int i = 0; i < 2000; i++) {
|
||||
oversize.add(
|
||||
ARecord.newRecord(
|
||||
Name.fromString("test-extremely-long-name-" + i + ".tld", Name.root),
|
||||
Type.A,
|
||||
DClass.IN));
|
||||
}
|
||||
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
|
||||
when(mockSocket.getOutputStream()).thenReturn(outputStream);
|
||||
IllegalArgumentException thrown =
|
||||
assertThrows(IllegalArgumentException.class, () -> resolver.send(oversize));
|
||||
assertThat(thrown).hasMessageThat().contains("message larger than maximum");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testResponseIdMismatchThrowsExeption() throws Exception {
|
||||
expectedResponse.getHeader().setID(1 + simpleQuery.getHeader().getID());
|
||||
when(mockSocket.getInputStream())
|
||||
.thenReturn(new ByteArrayInputStream(messageToBytesWithLength(expectedResponse)));
|
||||
when(mockSocket.getOutputStream()).thenReturn(new ByteArrayOutputStream());
|
||||
VerifyException thrown = assertThrows(VerifyException.class, () -> resolver.send(simpleQuery));
|
||||
assertThat(thrown)
|
||||
.hasMessageThat()
|
||||
.contains(
|
||||
"response ID "
|
||||
+ expectedResponse.getHeader().getID()
|
||||
+ " does not match query ID "
|
||||
+ simpleQuery.getHeader().getID());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testResponseOpcodeMismatchThrowsException() throws Exception {
|
||||
simpleQuery.getHeader().setOpcode(Opcode.QUERY);
|
||||
expectedResponse.getHeader().setOpcode(Opcode.STATUS);
|
||||
when(mockSocket.getInputStream())
|
||||
.thenReturn(new ByteArrayInputStream(messageToBytesWithLength(expectedResponse)));
|
||||
when(mockSocket.getOutputStream()).thenReturn(new ByteArrayOutputStream());
|
||||
VerifyException thrown = assertThrows(VerifyException.class, () -> resolver.send(simpleQuery));
|
||||
assertThat(thrown)
|
||||
.hasMessageThat()
|
||||
.contains("response opcode 'STATUS' does not match query opcode 'QUERY'");
|
||||
}
|
||||
|
||||
private Message responseMessageWithCode(Message query, int responseCode) {
|
||||
Message message = new Message(query.getHeader().getID());
|
||||
message.getHeader().setOpcode(query.getHeader().getOpcode());
|
||||
message.getHeader().setFlag(Flags.QR);
|
||||
message.getHeader().setRcode(responseCode);
|
||||
return message;
|
||||
}
|
||||
|
||||
private byte[] messageToBytesWithLength(Message message) {
|
||||
byte[] bytes = message.toWire();
|
||||
ByteBuffer buffer =
|
||||
ByteBuffer.allocate(bytes.length + DnsMessageTransport.MESSAGE_LENGTH_FIELD_BYTES);
|
||||
buffer.putShort((short) bytes.length);
|
||||
buffer.put(bytes);
|
||||
return buffer.array();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,471 @@
|
||||
// Copyright 2017 The Nomulus Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package google.registry.dns.writer.dnsupdate;
|
||||
|
||||
import static com.google.common.io.BaseEncoding.base16;
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
import static com.google.common.truth.Truth.assert_;
|
||||
import static google.registry.testing.DatastoreHelper.createTld;
|
||||
import static google.registry.testing.DatastoreHelper.newDomainBase;
|
||||
import static google.registry.testing.DatastoreHelper.newHostResource;
|
||||
import static google.registry.testing.DatastoreHelper.persistActiveDomain;
|
||||
import static google.registry.testing.DatastoreHelper.persistActiveHost;
|
||||
import static google.registry.testing.DatastoreHelper.persistActiveSubordinateHost;
|
||||
import static google.registry.testing.DatastoreHelper.persistDeletedDomain;
|
||||
import static google.registry.testing.DatastoreHelper.persistDeletedHost;
|
||||
import static google.registry.testing.DatastoreHelper.persistResource;
|
||||
import static google.registry.testing.JUnitBackports.assertThrows;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.verifyZeroInteractions;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import com.google.common.base.VerifyException;
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
import com.google.common.net.InetAddresses;
|
||||
import com.googlecode.objectify.Key;
|
||||
import google.registry.model.domain.DomainBase;
|
||||
import google.registry.model.domain.secdns.DelegationSignerData;
|
||||
import google.registry.model.eppcommon.StatusValue;
|
||||
import google.registry.model.host.HostResource;
|
||||
import google.registry.model.ofy.Ofy;
|
||||
import google.registry.testing.AppEngineRule;
|
||||
import google.registry.testing.FakeClock;
|
||||
import google.registry.testing.InjectRule;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Iterator;
|
||||
import org.joda.time.DateTime;
|
||||
import org.joda.time.Duration;
|
||||
import org.junit.Before;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.junit.runners.JUnit4;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.mockito.Captor;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.MockitoJUnit;
|
||||
import org.mockito.junit.MockitoRule;
|
||||
import org.xbill.DNS.Flags;
|
||||
import org.xbill.DNS.Message;
|
||||
import org.xbill.DNS.Opcode;
|
||||
import org.xbill.DNS.RRset;
|
||||
import org.xbill.DNS.Rcode;
|
||||
import org.xbill.DNS.Record;
|
||||
import org.xbill.DNS.Section;
|
||||
import org.xbill.DNS.Type;
|
||||
import org.xbill.DNS.Update;
|
||||
|
||||
/** Unit tests for {@link DnsUpdateWriter}. */
|
||||
@RunWith(JUnit4.class)
|
||||
public class DnsUpdateWriterTest {
|
||||
|
||||
@Rule
|
||||
public final AppEngineRule appEngine =
|
||||
AppEngineRule.builder().withDatastore().withTaskQueue().build();
|
||||
|
||||
@Rule public final MockitoRule mocks = MockitoJUnit.rule();
|
||||
@Rule public final InjectRule inject = new InjectRule();
|
||||
|
||||
@Mock private DnsMessageTransport mockResolver;
|
||||
@Captor private ArgumentCaptor<Update> updateCaptor;
|
||||
|
||||
private final FakeClock clock = new FakeClock(DateTime.parse("1971-01-01TZ"));
|
||||
|
||||
private DnsUpdateWriter writer;
|
||||
|
||||
@Before
|
||||
public void setUp() throws Exception {
|
||||
inject.setStaticField(Ofy.class, "clock", clock);
|
||||
|
||||
createTld("tld");
|
||||
when(mockResolver.send(any(Update.class))).thenReturn(messageWithResponseCode(Rcode.NOERROR));
|
||||
|
||||
writer = new DnsUpdateWriter(
|
||||
"tld", Duration.ZERO, Duration.ZERO, Duration.ZERO, mockResolver, clock);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testPublishDomainCreate_publishesNameServers() throws Exception {
|
||||
HostResource host1 = persistActiveHost("ns1.example.tld");
|
||||
HostResource host2 = persistActiveHost("ns2.example.tld");
|
||||
DomainBase domain =
|
||||
persistActiveDomain("example.tld")
|
||||
.asBuilder()
|
||||
.setNameservers(ImmutableSet.of(Key.create(host1), Key.create(host2)))
|
||||
.build();
|
||||
persistResource(domain);
|
||||
|
||||
writer.publishDomain("example.tld");
|
||||
writer.commit();
|
||||
|
||||
verify(mockResolver).send(updateCaptor.capture());
|
||||
Update update = updateCaptor.getValue();
|
||||
assertThatUpdatedZoneIs(update, "tld.");
|
||||
assertThatUpdateDeletes(update, "example.tld.", Type.ANY);
|
||||
assertThatUpdateAdds(update, "example.tld.", Type.NS, "ns1.example.tld.", "ns2.example.tld.");
|
||||
assertThatTotalUpdateSetsIs(update, 2); // The delete and NS sets
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testPublishAtomic_noCommit() {
|
||||
HostResource host1 = persistActiveHost("ns.example1.tld");
|
||||
DomainBase domain1 =
|
||||
persistActiveDomain("example1.tld")
|
||||
.asBuilder()
|
||||
.setNameservers(ImmutableSet.of(Key.create(host1)))
|
||||
.build();
|
||||
persistResource(domain1);
|
||||
|
||||
HostResource host2 = persistActiveHost("ns.example2.tld");
|
||||
DomainBase domain2 =
|
||||
persistActiveDomain("example2.tld")
|
||||
.asBuilder()
|
||||
.setNameservers(ImmutableSet.of(Key.create(host2)))
|
||||
.build();
|
||||
persistResource(domain2);
|
||||
|
||||
writer.publishDomain("example1.tld");
|
||||
writer.publishDomain("example2.tld");
|
||||
|
||||
verifyZeroInteractions(mockResolver);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testPublishAtomic_oneUpdate() throws Exception {
|
||||
HostResource host1 = persistActiveHost("ns.example1.tld");
|
||||
DomainBase domain1 =
|
||||
persistActiveDomain("example1.tld")
|
||||
.asBuilder()
|
||||
.setNameservers(ImmutableSet.of(Key.create(host1)))
|
||||
.build();
|
||||
persistResource(domain1);
|
||||
|
||||
HostResource host2 = persistActiveHost("ns.example2.tld");
|
||||
DomainBase domain2 =
|
||||
persistActiveDomain("example2.tld")
|
||||
.asBuilder()
|
||||
.setNameservers(ImmutableSet.of(Key.create(host2)))
|
||||
.build();
|
||||
persistResource(domain2);
|
||||
|
||||
writer.publishDomain("example1.tld");
|
||||
writer.publishDomain("example2.tld");
|
||||
writer.commit();
|
||||
|
||||
verify(mockResolver).send(updateCaptor.capture());
|
||||
Update update = updateCaptor.getValue();
|
||||
assertThatUpdatedZoneIs(update, "tld.");
|
||||
assertThatUpdateDeletes(update, "example1.tld.", Type.ANY);
|
||||
assertThatUpdateDeletes(update, "example2.tld.", Type.ANY);
|
||||
assertThatUpdateAdds(update, "example1.tld.", Type.NS, "ns.example1.tld.");
|
||||
assertThatUpdateAdds(update, "example2.tld.", Type.NS, "ns.example2.tld.");
|
||||
assertThatTotalUpdateSetsIs(update, 4); // The delete and NS sets for each TLD
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testPublishDomainCreate_publishesDelegationSigner() throws Exception {
|
||||
DomainBase domain =
|
||||
persistActiveDomain("example.tld")
|
||||
.asBuilder()
|
||||
.setNameservers(ImmutableSet.of(Key.create(persistActiveHost("ns1.example.tld"))))
|
||||
.setDsData(
|
||||
ImmutableSet.of(
|
||||
DelegationSignerData.create(1, 3, 1, base16().decode("0123456789ABCDEF"))))
|
||||
.build();
|
||||
persistResource(domain);
|
||||
|
||||
writer.publishDomain("example.tld");
|
||||
writer.commit();
|
||||
|
||||
verify(mockResolver).send(updateCaptor.capture());
|
||||
Update update = updateCaptor.getValue();
|
||||
assertThatUpdatedZoneIs(update, "tld.");
|
||||
assertThatUpdateDeletes(update, "example.tld.", Type.ANY);
|
||||
assertThatUpdateAdds(update, "example.tld.", Type.NS, "ns1.example.tld.");
|
||||
assertThatUpdateAdds(update, "example.tld.", Type.DS, "1 3 1 0123456789ABCDEF");
|
||||
assertThatTotalUpdateSetsIs(update, 3); // The delete, the NS, and DS sets
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testPublishDomainWhenNotActive_removesDnsRecords() throws Exception {
|
||||
DomainBase domain =
|
||||
persistActiveDomain("example.tld")
|
||||
.asBuilder()
|
||||
.addStatusValue(StatusValue.SERVER_HOLD)
|
||||
.setNameservers(ImmutableSet.of(Key.create(persistActiveHost("ns1.example.tld"))))
|
||||
.build();
|
||||
persistResource(domain);
|
||||
|
||||
writer.publishDomain("example.tld");
|
||||
writer.commit();
|
||||
|
||||
verify(mockResolver).send(updateCaptor.capture());
|
||||
Update update = updateCaptor.getValue();
|
||||
assertThatUpdatedZoneIs(update, "tld.");
|
||||
assertThatUpdateDeletes(update, "example.tld.", Type.ANY);
|
||||
assertThatTotalUpdateSetsIs(update, 1); // Just the delete set
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testPublishDomainDelete_removesDnsRecords() throws Exception {
|
||||
persistDeletedDomain("example.tld", clock.nowUtc().minusDays(1));
|
||||
|
||||
writer.publishDomain("example.tld");
|
||||
writer.commit();
|
||||
|
||||
verify(mockResolver).send(updateCaptor.capture());
|
||||
Update update = updateCaptor.getValue();
|
||||
assertThatUpdatedZoneIs(update, "tld.");
|
||||
assertThatUpdateDeletes(update, "example.tld.", Type.ANY);
|
||||
assertThatTotalUpdateSetsIs(update, 1); // Just the delete set
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testPublishHostCreate_publishesAddressRecords() throws Exception {
|
||||
HostResource host =
|
||||
persistResource(
|
||||
newHostResource("ns1.example.tld")
|
||||
.asBuilder()
|
||||
.setInetAddresses(
|
||||
ImmutableSet.of(
|
||||
InetAddresses.forString("10.0.0.1"),
|
||||
InetAddresses.forString("10.1.0.1"),
|
||||
InetAddresses.forString("fd0e:a5c8:6dfb:6a5e:0:0:0:1")))
|
||||
.build());
|
||||
persistResource(
|
||||
newDomainBase("example.tld")
|
||||
.asBuilder()
|
||||
.addSubordinateHost("ns1.example.tld")
|
||||
.addNameserver(Key.create(host))
|
||||
.build());
|
||||
|
||||
writer.publishHost("ns1.example.tld");
|
||||
writer.commit();
|
||||
|
||||
verify(mockResolver).send(updateCaptor.capture());
|
||||
Update update = updateCaptor.getValue();
|
||||
assertThatUpdatedZoneIs(update, "tld.");
|
||||
assertThatUpdateDeletes(update, "example.tld.", Type.ANY);
|
||||
assertThatUpdateDeletes(update, "ns1.example.tld.", Type.ANY);
|
||||
assertThatUpdateAdds(update, "ns1.example.tld.", Type.A, "10.0.0.1", "10.1.0.1");
|
||||
assertThatUpdateAdds(update, "ns1.example.tld.", Type.AAAA, "fd0e:a5c8:6dfb:6a5e:0:0:0:1");
|
||||
assertThatUpdateAdds(update, "example.tld.", Type.NS, "ns1.example.tld.");
|
||||
assertThatTotalUpdateSetsIs(update, 5);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testPublishHostDelete_removesDnsRecords() throws Exception {
|
||||
persistDeletedHost("ns1.example.tld", clock.nowUtc().minusDays(1));
|
||||
persistActiveDomain("example.tld");
|
||||
|
||||
writer.publishHost("ns1.example.tld");
|
||||
writer.commit();
|
||||
|
||||
verify(mockResolver).send(updateCaptor.capture());
|
||||
Update update = updateCaptor.getValue();
|
||||
assertThatUpdatedZoneIs(update, "tld.");
|
||||
assertThatUpdateDeletes(update, "example.tld.", Type.ANY);
|
||||
assertThatUpdateDeletes(update, "ns1.example.tld.", Type.ANY);
|
||||
assertThatTotalUpdateSetsIs(update, 2); // Just the delete set
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testPublishHostDelete_removesGlueRecords() throws Exception {
|
||||
persistDeletedHost("ns1.example.tld", clock.nowUtc().minusDays(1));
|
||||
persistResource(
|
||||
persistActiveDomain("example.tld")
|
||||
.asBuilder()
|
||||
.setNameservers(ImmutableSet.of(Key.create(persistActiveHost("ns1.example.com"))))
|
||||
.build());
|
||||
|
||||
writer.publishHost("ns1.example.tld");
|
||||
writer.commit();
|
||||
|
||||
verify(mockResolver).send(updateCaptor.capture());
|
||||
Update update = updateCaptor.getValue();
|
||||
assertThatUpdatedZoneIs(update, "tld.");
|
||||
assertThatUpdateDeletes(update, "example.tld.", Type.ANY);
|
||||
assertThatUpdateDeletes(update, "ns1.example.tld.", Type.ANY);
|
||||
assertThatUpdateAdds(update, "example.tld.", Type.NS, "ns1.example.com.");
|
||||
assertThatTotalUpdateSetsIs(update, 3);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testPublishDomainExternalAndInBailiwickNameServer() throws Exception {
|
||||
HostResource externalNameserver = persistResource(newHostResource("ns1.example.com"));
|
||||
HostResource inBailiwickNameserver =
|
||||
persistResource(
|
||||
newHostResource("ns1.example.tld")
|
||||
.asBuilder()
|
||||
.setInetAddresses(
|
||||
ImmutableSet.of(
|
||||
InetAddresses.forString("10.0.0.1"),
|
||||
InetAddresses.forString("10.1.0.1"),
|
||||
InetAddresses.forString("fd0e:a5c8:6dfb:6a5e:0:0:0:1")))
|
||||
.build());
|
||||
|
||||
persistResource(
|
||||
newDomainBase("example.tld")
|
||||
.asBuilder()
|
||||
.addSubordinateHost("ns1.example.tld")
|
||||
.addNameservers(
|
||||
ImmutableSet.of(Key.create(externalNameserver), Key.create(inBailiwickNameserver)))
|
||||
.build());
|
||||
|
||||
writer.publishDomain("example.tld");
|
||||
writer.commit();
|
||||
|
||||
verify(mockResolver).send(updateCaptor.capture());
|
||||
Update update = updateCaptor.getValue();
|
||||
assertThatUpdatedZoneIs(update, "tld.");
|
||||
assertThatUpdateDeletes(update, "example.tld.", Type.ANY);
|
||||
assertThatUpdateDeletes(update, "ns1.example.tld.", Type.ANY);
|
||||
assertThatUpdateAdds(update, "example.tld.", Type.NS, "ns1.example.com.", "ns1.example.tld.");
|
||||
assertThatUpdateAdds(update, "ns1.example.tld.", Type.A, "10.0.0.1", "10.1.0.1");
|
||||
assertThatUpdateAdds(update, "ns1.example.tld.", Type.AAAA, "fd0e:a5c8:6dfb:6a5e:0:0:0:1");
|
||||
assertThatTotalUpdateSetsIs(update, 5);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testPublishDomainDeleteOrphanGlues() throws Exception {
|
||||
HostResource inBailiwickNameserver =
|
||||
persistResource(
|
||||
newHostResource("ns1.example.tld")
|
||||
.asBuilder()
|
||||
.setInetAddresses(
|
||||
ImmutableSet.of(
|
||||
InetAddresses.forString("10.0.0.1"),
|
||||
InetAddresses.forString("10.1.0.1"),
|
||||
InetAddresses.forString("fd0e:a5c8:6dfb:6a5e:0:0:0:1")))
|
||||
.build());
|
||||
|
||||
persistResource(
|
||||
newDomainBase("example.tld")
|
||||
.asBuilder()
|
||||
.addSubordinateHost("ns1.example.tld")
|
||||
.addSubordinateHost("foo.example.tld")
|
||||
.addNameserver(Key.create(inBailiwickNameserver))
|
||||
.build());
|
||||
|
||||
writer.publishDomain("example.tld");
|
||||
writer.commit();
|
||||
|
||||
verify(mockResolver).send(updateCaptor.capture());
|
||||
Update update = updateCaptor.getValue();
|
||||
assertThatUpdatedZoneIs(update, "tld.");
|
||||
assertThatUpdateDeletes(update, "example.tld.", Type.ANY);
|
||||
assertThatUpdateDeletes(update, "ns1.example.tld.", Type.ANY);
|
||||
assertThatUpdateDeletes(update, "foo.example.tld.", Type.ANY);
|
||||
assertThatUpdateAdds(update, "example.tld.", Type.NS, "ns1.example.tld.");
|
||||
assertThatUpdateAdds(update, "ns1.example.tld.", Type.A, "10.0.0.1", "10.1.0.1");
|
||||
assertThatUpdateAdds(update, "ns1.example.tld.", Type.AAAA, "fd0e:a5c8:6dfb:6a5e:0:0:0:1");
|
||||
assertThatTotalUpdateSetsIs(update, 6);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testPublishDomainFails_whenDnsUpdateReturnsError() throws Exception {
|
||||
DomainBase domain =
|
||||
persistActiveDomain("example.tld")
|
||||
.asBuilder()
|
||||
.setNameservers(ImmutableSet.of(Key.create(persistActiveHost("ns1.example.tld"))))
|
||||
.build();
|
||||
persistResource(domain);
|
||||
when(mockResolver.send(any(Message.class))).thenReturn(messageWithResponseCode(Rcode.SERVFAIL));
|
||||
VerifyException thrown =
|
||||
assertThrows(
|
||||
VerifyException.class,
|
||||
() -> {
|
||||
writer.publishDomain("example.tld");
|
||||
writer.commit();
|
||||
});
|
||||
assertThat(thrown).hasMessageThat().contains("SERVFAIL");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testPublishHostFails_whenDnsUpdateReturnsError() throws Exception {
|
||||
HostResource host =
|
||||
persistActiveSubordinateHost("ns1.example.tld", persistActiveDomain("example.tld"))
|
||||
.asBuilder()
|
||||
.setInetAddresses(ImmutableSet.of(InetAddresses.forString("10.0.0.1")))
|
||||
.build();
|
||||
persistResource(host);
|
||||
when(mockResolver.send(any(Message.class))).thenReturn(messageWithResponseCode(Rcode.SERVFAIL));
|
||||
VerifyException thrown =
|
||||
assertThrows(
|
||||
VerifyException.class,
|
||||
() -> {
|
||||
writer.publishHost("ns1.example.tld");
|
||||
writer.commit();
|
||||
});
|
||||
assertThat(thrown).hasMessageThat().contains("SERVFAIL");
|
||||
}
|
||||
|
||||
private void assertThatUpdatedZoneIs(Update update, String zoneName) {
|
||||
Record[] zoneRecords = update.getSectionArray(Section.ZONE);
|
||||
assertThat(zoneRecords[0].getName().toString()).isEqualTo(zoneName);
|
||||
}
|
||||
|
||||
private void assertThatTotalUpdateSetsIs(Update update, int count) {
|
||||
assertThat(update.getSectionRRsets(Section.UPDATE)).hasLength(count);
|
||||
}
|
||||
|
||||
private void assertThatUpdateDeletes(Update update, String resourceName, int recordType) {
|
||||
ImmutableList<Record> deleted = findUpdateRecords(update, resourceName, recordType);
|
||||
// There's only an empty (i.e. "delete") record.
|
||||
assertThat(deleted.get(0).rdataToString()).hasLength(0);
|
||||
assertThat(deleted).hasSize(1);
|
||||
}
|
||||
|
||||
private void assertThatUpdateAdds(
|
||||
Update update, String resourceName, int recordType, String... resourceData) {
|
||||
ArrayList<String> expectedData = new ArrayList<>();
|
||||
Collections.addAll(expectedData, resourceData);
|
||||
|
||||
ArrayList<String> actualData = new ArrayList<>();
|
||||
for (Record record : findUpdateRecords(update, resourceName, recordType)) {
|
||||
actualData.add(record.rdataToString());
|
||||
}
|
||||
assertThat(actualData).containsExactlyElementsIn(expectedData);
|
||||
}
|
||||
|
||||
private ImmutableList<Record> findUpdateRecords(
|
||||
Update update, String resourceName, int recordType) {
|
||||
for (RRset set : update.getSectionRRsets(Section.UPDATE)) {
|
||||
if (set.getName().toString().equals(resourceName) && set.getType() == recordType) {
|
||||
return fixIterator(Record.class, set.rrs());
|
||||
}
|
||||
}
|
||||
assert_().fail(
|
||||
"No record set found for resource '%s' type '%s'",
|
||||
resourceName, Type.string(recordType));
|
||||
throw new AssertionError();
|
||||
}
|
||||
|
||||
@SuppressWarnings({"unchecked", "unused"})
|
||||
private static <T> ImmutableList<T> fixIterator(Class<T> clazz, final Iterator<?> iterator) {
|
||||
return ImmutableList.copyOf((Iterator<T>) iterator);
|
||||
}
|
||||
|
||||
private Message messageWithResponseCode(int responseCode) {
|
||||
Message message = new Message();
|
||||
message.getHeader().setOpcode(Opcode.UPDATE);
|
||||
message.getHeader().setFlag(Flags.QR);
|
||||
message.getHeader().setRcode(responseCode);
|
||||
return message;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,268 @@
|
||||
// Copyright 2017 The Nomulus Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package google.registry.documentation;
|
||||
|
||||
import static com.google.common.base.Preconditions.checkArgument;
|
||||
import static com.google.common.base.Preconditions.checkState;
|
||||
import static com.google.common.collect.ImmutableSet.toImmutableSet;
|
||||
import static com.google.common.collect.MoreCollectors.onlyElement;
|
||||
import static google.registry.util.BuildPathUtils.getProjectRoot;
|
||||
import static java.util.stream.Collectors.joining;
|
||||
|
||||
import com.google.common.base.Ascii;
|
||||
import com.google.common.collect.ImmutableMultimap;
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
import com.google.common.collect.MultimapBuilder;
|
||||
import com.google.common.collect.SetMultimap;
|
||||
import com.google.common.collect.Sets;
|
||||
import com.google.common.collect.Streams;
|
||||
import com.thoughtworks.qdox.JavaDocBuilder;
|
||||
import com.thoughtworks.qdox.model.JavaSource;
|
||||
import google.registry.documentation.FlowDocumentation.ErrorCase;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.DirectoryStream;
|
||||
import java.nio.file.FileSystems;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* Stores the context for a flow and computes exception mismatches between javadoc and tests.
|
||||
*
|
||||
* <p>This class uses the flow_docs library built for the documentation generator tool to pull out
|
||||
* the set of flow exceptions documented by custom javadoc tags on the specified flow. It then
|
||||
* derives the corresponding test files for that flow and pulls out the imported names from those
|
||||
* files, checking against a set of all possible flow exceptions to determine those used by this
|
||||
* particular test. The set of javadoc-based exceptions and the set of imported exceptions should
|
||||
* be identical, ensuring a correspondence between error cases listed in the documentation and
|
||||
* those tested in the flow unit tests.
|
||||
*
|
||||
* <p>If the two sets are not identical, the getMismatchedExceptions() method on this class will
|
||||
* return a non-empty string containing messages about what the mismatches were and which lines
|
||||
* need to be added or removed in which files in order to satisfy the correspondence condition.
|
||||
*/
|
||||
public class FlowContext {
|
||||
/** Represents one of the two possible places an exception may be referenced from. */
|
||||
// TODO(b/19124943): This enum is only used in ErrorCaseMismatch and ideally belongs there, but
|
||||
// can't go in the inner class because it's not a static inner class. At some point it might
|
||||
// be worth refactoring so that this enum can be properly scoped.
|
||||
private enum SourceType { JAVADOC, IMPORT }
|
||||
|
||||
/** The package in which this flow resides. */
|
||||
final String packageName;
|
||||
|
||||
/** The source file for this flow, used for help messages. */
|
||||
final String sourceFilename;
|
||||
|
||||
/** The test files for this flow, used for help messages and extracting imported exceptions. */
|
||||
final Set<String> testFilenames;
|
||||
|
||||
/** The set of all possible exceptions that could be error cases for a flow. */
|
||||
final Set<ErrorCase> possibleExceptions;
|
||||
|
||||
/** The set of exceptions referenced from the javadoc on this flow. */
|
||||
final Set<ErrorCase> javadocExceptions;
|
||||
|
||||
/** Maps exceptions imported by the test files for this flow to the files in which they occur. */
|
||||
final SetMultimap<ErrorCase, String> importExceptionsToFilenames;
|
||||
|
||||
/**
|
||||
* Creates a FlowContext from a FlowDocumentation object and a set of all possible exceptions.
|
||||
* The latter parameter is needed in order to filter imported names in the flow test file.
|
||||
*/
|
||||
public FlowContext(FlowDocumentation flowDoc, Set<ErrorCase> possibleExceptions)
|
||||
throws IOException {
|
||||
packageName = flowDoc.getPackageName();
|
||||
// Assume the standard filename conventions for locating the flow class's source file.
|
||||
sourceFilename = "java/" + flowDoc.getQualifiedName().replace('.', '/') + ".java";
|
||||
testFilenames = getTestFilenames(flowDoc.getQualifiedName());
|
||||
checkState(testFilenames.size() >= 1, "No test files found for %s.", flowDoc.getName());
|
||||
this.possibleExceptions = possibleExceptions;
|
||||
javadocExceptions = Sets.newHashSet(flowDoc.getErrors());
|
||||
importExceptionsToFilenames = getImportExceptions();
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper to locate test files for this flow by looking in javatests/ for all files with the
|
||||
* exact same relative filename as the flow file, but with a "*Test{,Case}.java" suffix.
|
||||
*/
|
||||
private static Set<String> getTestFilenames(String flowName) throws IOException {
|
||||
String commonPrefix =
|
||||
getProjectRoot().resolve("core/src/test/java").resolve(flowName.replace('.', '/'))
|
||||
.toString();
|
||||
return Sets.union(
|
||||
getFilenamesMatchingGlob(commonPrefix + "*Test.java"),
|
||||
getFilenamesMatchingGlob(commonPrefix + "*TestCase.java"));
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper to return the set of filenames matching the given glob. The glob should only have
|
||||
* asterisks in the portion following the last slash (if there is one).
|
||||
*/
|
||||
private static Set<String> getFilenamesMatchingGlob(String fullGlob) throws IOException {
|
||||
Path globPath = FileSystems.getDefault().getPath(fullGlob);
|
||||
Path dirPath = globPath.getParent();
|
||||
String glob = globPath.getFileName().toString();
|
||||
try (DirectoryStream<Path> dirStream = Files.newDirectoryStream(dirPath, glob)) {
|
||||
return Streams.stream(dirStream).map(Object::toString).collect(toImmutableSet());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a multimap mapping each exception imported in test files for this flow to the set of
|
||||
* filenames for files that import that exception.
|
||||
*/
|
||||
private SetMultimap<ErrorCase, String> getImportExceptions() throws IOException {
|
||||
ImmutableMultimap.Builder<String, ErrorCase> builder = new ImmutableMultimap.Builder<>();
|
||||
for (String testFileName : testFilenames) {
|
||||
builder.putAll(testFileName, getImportExceptionsFromFile(testFileName));
|
||||
}
|
||||
// Invert the mapping so that later we can easily map exceptions to where they were imported.
|
||||
return MultimapBuilder.hashKeys().hashSetValues().build(builder.build().inverse());
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the set of exceptions imported in this test file. First extracts the set of
|
||||
* all names imported by the test file, and then uses these to filter a global list of possible
|
||||
* exceptions, so that the additional exception information available via the global list objects
|
||||
* (which are ErrorCases wrapping exception names) can be preserved.
|
||||
*/
|
||||
private Set<ErrorCase> getImportExceptionsFromFile(String filename) throws IOException {
|
||||
JavaDocBuilder builder = new JavaDocBuilder();
|
||||
JavaSource src = builder.addSource(new File(filename));
|
||||
final Set<String> importedNames = Sets.newHashSet(src.getImports());
|
||||
return possibleExceptions
|
||||
.stream()
|
||||
.filter(errorCase -> importedNames.contains(errorCase.getClassName()))
|
||||
.collect(toImmutableSet());
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Represents a mismatch in this flow for a specific error case and documents how to fix it.
|
||||
* A mismatch occurs when the exception for this error case appears in either the source file
|
||||
* javadoc or at least one matching test file, but not in both.
|
||||
*/
|
||||
private class ErrorCaseMismatch {
|
||||
|
||||
/** The format for an import statement for a given exception name. */
|
||||
static final String IMPORT_FORMAT = "import %s;";
|
||||
|
||||
/** The format for a javadoc tag referencing a given exception name. */
|
||||
static final String JAVADOC_FORMAT = "@error {@link %s}";
|
||||
|
||||
// Template strings for printing output.
|
||||
static final String TEMPLATE_HEADER = "Extra %s for %s:\n";
|
||||
static final String TEMPLATE_ADD = " Add %s to %s:\n + %s\n";
|
||||
static final String TEMPLATE_ADD_MULTIPLE = " Add %s to one or more of:\n%s + %s\n";
|
||||
static final String TEMPLATE_REMOVE = " Or remove %s in %s:\n - %s\n";
|
||||
static final String TEMPLATE_REMOVE_MULTIPLE = " Or remove %ss in:\n%s - %s\n";
|
||||
static final String TEMPLATE_MULTIPLE_FILES = " * %s\n";
|
||||
|
||||
/** The error case for which the mismatch was detected. */
|
||||
final ErrorCase errorCase;
|
||||
|
||||
/** The source type where references could be added to fix the mismatch. */
|
||||
final SourceType addType;
|
||||
|
||||
/** The source type where references could be removed to fix the mismatch. */
|
||||
final SourceType removeType;
|
||||
|
||||
/**
|
||||
* Constructs an ErrorCaseMismatch for the given ErrorCase and SourceType. The latter parameter
|
||||
* indicates the source type this exception was referenced from.
|
||||
*/
|
||||
public ErrorCaseMismatch(ErrorCase errorCase, SourceType foundType) {
|
||||
this.errorCase = errorCase;
|
||||
// Effectively addType = !foundType.
|
||||
addType = (foundType == SourceType.IMPORT ? SourceType.JAVADOC : SourceType.IMPORT);
|
||||
removeType = foundType;
|
||||
}
|
||||
|
||||
/** Returns the line of code needed to refer to this exception from the given source type. */
|
||||
public String getCodeLineAs(SourceType sourceType) {
|
||||
return sourceType == SourceType.JAVADOC
|
||||
// Strip the flow package prefix from the exception class name if possible, for brevity.
|
||||
? String.format(JAVADOC_FORMAT, errorCase.getClassName().replace(packageName + ".", ""))
|
||||
: String.format(IMPORT_FORMAT, errorCase.getClassName());
|
||||
}
|
||||
|
||||
/** Helper to format a set of filenames for printing in a mismatch message. */
|
||||
private String formatMultipleFiles(Set<String> filenames) {
|
||||
checkArgument(filenames.size() >= 1, "Cannot format empty list of files.");
|
||||
if (filenames.size() == 1) {
|
||||
return filenames.stream().collect(onlyElement());
|
||||
}
|
||||
return filenames
|
||||
.stream()
|
||||
.map(filename -> String.format(TEMPLATE_MULTIPLE_FILES, filename))
|
||||
.collect(joining(""));
|
||||
}
|
||||
|
||||
/** Helper to format the section describing how to add references to fix the mismatch. */
|
||||
private String makeAddSection() {
|
||||
String addTypeString = Ascii.toLowerCase(addType.toString());
|
||||
String codeLine = getCodeLineAs(addType);
|
||||
Set<String> files = (addType == SourceType.JAVADOC
|
||||
? ImmutableSet.of(sourceFilename)
|
||||
: testFilenames);
|
||||
return (files.size() == 1
|
||||
? String.format(
|
||||
TEMPLATE_ADD, addTypeString, formatMultipleFiles(files), codeLine)
|
||||
: String.format(
|
||||
TEMPLATE_ADD_MULTIPLE, addTypeString, formatMultipleFiles(files), codeLine));
|
||||
}
|
||||
|
||||
/** Helper to format the section describing how to remove references to fix the mismatch. */
|
||||
// TODO(b/19124943): Repeating structure from makeAddSection() - would be nice to clean up.
|
||||
private String makeRemoveSection() {
|
||||
String removeTypeString = Ascii.toLowerCase(removeType.toString());
|
||||
String codeLine = getCodeLineAs(removeType);
|
||||
Set<String> files = (removeType == SourceType.JAVADOC
|
||||
? ImmutableSet.of(sourceFilename)
|
||||
: importExceptionsToFilenames.get(errorCase));
|
||||
return (files.size() == 1
|
||||
? String.format(
|
||||
TEMPLATE_REMOVE, removeTypeString, formatMultipleFiles(files), codeLine)
|
||||
: String.format(
|
||||
TEMPLATE_REMOVE_MULTIPLE, removeTypeString, formatMultipleFiles(files), codeLine));
|
||||
}
|
||||
|
||||
/** Returns a string describing the mismatch for this flow exception and how to fix it. */
|
||||
@Override
|
||||
public String toString() {
|
||||
String headerSection = String.format(
|
||||
TEMPLATE_HEADER, Ascii.toLowerCase(removeType.toString()), errorCase.getName());
|
||||
return headerSection + makeAddSection() + makeRemoveSection();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a single string describing all mismatched exceptions for this flow. An empty string
|
||||
* means no mismatched exceptions were found.
|
||||
*/
|
||||
public String getMismatchedExceptions() {
|
||||
Set<ErrorCase> importExceptions = importExceptionsToFilenames.keySet();
|
||||
StringBuilder builder = new StringBuilder();
|
||||
for (ErrorCase errorCase : Sets.difference(javadocExceptions, importExceptions)) {
|
||||
builder.append(new ErrorCaseMismatch(errorCase, SourceType.JAVADOC)).append("\n");
|
||||
}
|
||||
for (ErrorCase errorCase : Sets.difference(importExceptions, javadocExceptions)) {
|
||||
builder.append(new ErrorCaseMismatch(errorCase, SourceType.IMPORT)).append("\n");
|
||||
}
|
||||
return builder.toString();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
// Copyright 2017 The Nomulus Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package google.registry.documentation;
|
||||
|
||||
import static com.google.common.truth.Truth.assert_;
|
||||
import static google.registry.util.BuildPathUtils.getProjectRoot;
|
||||
import static java.nio.charset.StandardCharsets.UTF_8;
|
||||
|
||||
import com.google.common.base.Joiner;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.junit.runners.JUnit4;
|
||||
|
||||
/** Tests to ensure that generated flow documentation matches the expected documentation. */
|
||||
@RunWith(JUnit4.class)
|
||||
public class FlowDocumentationTest {
|
||||
private static final Path GOLDEN_MARKDOWN_FILEPATH = getProjectRoot().resolve("docs/flows.md");
|
||||
|
||||
private static final String UPDATE_COMMAND = "./gradlew :core:flowDocsTool";
|
||||
private static final String UPDATE_INSTRUCTIONS =
|
||||
Joiner.on('\n')
|
||||
.join(
|
||||
"",
|
||||
"-----------------------------------------------------------------------------------",
|
||||
"Your changes affect the flow API documentation output. To update the golden version "
|
||||
+ "of the documentation, run:",
|
||||
UPDATE_COMMAND,
|
||||
"");
|
||||
|
||||
@Test
|
||||
public void testGeneratedMatchesGolden() throws IOException {
|
||||
// Read the markdown file.
|
||||
Path goldenMarkdownPath =
|
||||
GOLDEN_MARKDOWN_FILEPATH;
|
||||
|
||||
String goldenMarkdown = new String(Files.readAllBytes(goldenMarkdownPath), UTF_8);
|
||||
|
||||
// Don't use Truth's isEqualTo() because the output is huge and unreadable for large files.
|
||||
DocumentationGenerator generator = new DocumentationGenerator();
|
||||
if (!generator.generateMarkdown().equals(goldenMarkdown)) {
|
||||
assert_().fail(UPDATE_INSTRUCTIONS);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
// Copyright 2017 The Nomulus Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package google.registry.documentation;
|
||||
|
||||
import static com.google.common.truth.Truth.assertWithMessage;
|
||||
|
||||
import com.google.common.collect.Sets;
|
||||
import com.google.common.flogger.FluentLogger;
|
||||
import google.registry.documentation.FlowDocumentation.ErrorCase;
|
||||
import java.io.IOException;
|
||||
import java.util.Set;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.junit.runners.JUnit4;
|
||||
|
||||
/**
|
||||
* Test to ensure accurate documentation of flow exceptions.
|
||||
*
|
||||
* <p>This test goes through each flow and ensures that the exceptions listed in custom javadoc
|
||||
* tags on the flow class match the import statements in the test case for that flow. Thus it
|
||||
* catches the case where someone adds a test case for an exception without updating the javadoc,
|
||||
* and the case where someone adds a javadoc tag to a flow without writing a test for this error
|
||||
* condition. For example, there should always be a matching pair of lines such as the following:
|
||||
*
|
||||
* <pre>
|
||||
* java/.../flows/session/LoginFlow.java:
|
||||
* @error {@link AlreadyLoggedInException}
|
||||
*
|
||||
* javatests/.../flows/session/LoginFlowTest.java:
|
||||
* import .....flows.session.LoginFlow.AlreadyLoggedInException;
|
||||
* </pre>
|
||||
*
|
||||
* If the first line is missing, this test fails and suggests adding the javadoc tag or removing
|
||||
* the import. If the second line is missing, this test fails and suggests adding the import or
|
||||
* removing the javadoc tag.
|
||||
*/
|
||||
@SuppressWarnings("javadoc")
|
||||
@RunWith(JUnit4.class)
|
||||
public class FlowExceptionsTest {
|
||||
|
||||
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
|
||||
|
||||
@Test
|
||||
public void testExceptionCorrespondence() throws IOException {
|
||||
DocumentationGenerator docGenerator = new DocumentationGenerator();
|
||||
Set<ErrorCase> possibleErrors = Sets.newHashSet(docGenerator.getAllErrors());
|
||||
Set<String> mismatchingFlows = Sets.newHashSet();
|
||||
for (FlowDocumentation flow : docGenerator.getFlowDocs()) {
|
||||
FlowContext context = new FlowContext(flow, possibleErrors);
|
||||
String mismatches = context.getMismatchedExceptions();
|
||||
if (!mismatches.isEmpty()) {
|
||||
logger.atWarning().log("%-40s FAIL\n\n%s", flow.getName(), mismatches);
|
||||
mismatchingFlows.add(flow.getName());
|
||||
} else {
|
||||
logger.atInfo().log("%-40s OK", flow.getName());
|
||||
}
|
||||
}
|
||||
assertWithMessage(
|
||||
"Mismatched exceptions between flow documentation and tests. See test log for full "
|
||||
+ "details. The set of failing flows follows.")
|
||||
.that(mismatchingFlows)
|
||||
.isEmpty();
|
||||
}
|
||||
}
|
||||
+92
@@ -0,0 +1,92 @@
|
||||
// Copyright 2017 The Nomulus Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package google.registry.documentation;
|
||||
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
import static google.registry.testing.JUnitBackports.assertThrows;
|
||||
|
||||
import java.util.Arrays;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.junit.runners.JUnit4;
|
||||
|
||||
/** Test conversion of javadocs to markdown. */
|
||||
@RunWith(JUnit4.class)
|
||||
public class MarkdownDocumentationFormatterTest {
|
||||
@Test
|
||||
public void testHtmlSanitization() {
|
||||
assertThat(
|
||||
MarkdownDocumentationFormatter.fixHtml(
|
||||
"First. <p>Second. < > & &squot; ""))
|
||||
.isEqualTo("First. Second. < > & ' \"");
|
||||
assertThat(MarkdownDocumentationFormatter.fixHtml("<p>Leading substitution."))
|
||||
.isEqualTo("Leading substitution.");
|
||||
assertThat(MarkdownDocumentationFormatter.fixHtml("No substitution."))
|
||||
.isEqualTo("No substitution.");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDedents() {
|
||||
assertThat(MarkdownDocumentationFormatter.fixHtml(
|
||||
"First line\n\n <p>Second line.\n Third line."))
|
||||
.isEqualTo("First line\n\nSecond line.\nThird line.");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testUnknownSequences() {
|
||||
assertThrows(
|
||||
IllegalArgumentException.class, () -> MarkdownDocumentationFormatter.fixHtml("&blech;"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testParagraphFormatting() {
|
||||
String[] words = {"first", "second", "third", "really-really-long-word", "more", "stuff"};
|
||||
String formatted = MarkdownDocumentationFormatter.formatParagraph(Arrays.asList(words), 16);
|
||||
assertThat(formatted).isEqualTo("first second\nthird\nreally-really-long-word\nmore stuff\n");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testReflow() {
|
||||
String input =
|
||||
"This is the very first line.\n"
|
||||
+ " \n" // add a little blank space to this line just to make things interesting.
|
||||
+ "This is the second paragraph. Aint\n"
|
||||
+ "it sweet?\n"
|
||||
+ "\n"
|
||||
+ "This is our third and final paragraph.\n"
|
||||
+ "It is multi-line and ends with no blank\n"
|
||||
+ "line.";
|
||||
|
||||
String expected =
|
||||
"This is the very\n"
|
||||
+ "first line.\n"
|
||||
+ "\n"
|
||||
+ "This is the\n"
|
||||
+ "second\n"
|
||||
+ "paragraph. Aint\n"
|
||||
+ "it sweet?\n"
|
||||
+ "\n"
|
||||
+ "This is our\n"
|
||||
+ "third and final\n"
|
||||
+ "paragraph. It is\n"
|
||||
+ "multi-line and\n"
|
||||
+ "ends with no\n"
|
||||
+ "blank line.\n";
|
||||
|
||||
assertThat(MarkdownDocumentationFormatter.reflow(input, 16)).isEqualTo(expected);
|
||||
}
|
||||
|
||||
public MarkdownDocumentationFormatterTest() {}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
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 = "export",
|
||||
srcs = glob(["*.java"]),
|
||||
resources = glob([
|
||||
"testdata/*.json",
|
||||
"backup_kinds.txt",
|
||||
"reporting_kinds.txt",
|
||||
]),
|
||||
deps = [
|
||||
"//java/google/registry/bigquery",
|
||||
"//java/google/registry/config",
|
||||
"//java/google/registry/export",
|
||||
"//java/google/registry/export/datastore",
|
||||
"//java/google/registry/groups",
|
||||
"//java/google/registry/model",
|
||||
"//java/google/registry/request",
|
||||
"//java/google/registry/storage/drive",
|
||||
"//java/google/registry/util",
|
||||
"//javatests/google/registry/testing",
|
||||
"//javatests/google/registry/testing/mapreduce",
|
||||
"//third_party/objectify:objectify-v4_1",
|
||||
"@com_google_api_client",
|
||||
"@com_google_apis_google_api_services_bigquery",
|
||||
"@com_google_apis_google_api_services_drive",
|
||||
"@com_google_appengine_api_1_0_sdk",
|
||||
"@com_google_appengine_api_stubs",
|
||||
"@com_google_appengine_tools_appengine_gcs_client",
|
||||
"@com_google_dagger",
|
||||
"@com_google_flogger",
|
||||
"@com_google_flogger_system_backend",
|
||||
"@com_google_guava",
|
||||
"@com_google_http_client",
|
||||
"@com_google_http_client_jackson2",
|
||||
"@com_google_re2j",
|
||||
"@com_google_truth",
|
||||
"@com_google_truth_extensions_truth_java8_extension",
|
||||
"@javax_servlet_api",
|
||||
"@joda_time",
|
||||
"@junit",
|
||||
"@org_mockito_core",
|
||||
],
|
||||
)
|
||||
|
||||
GenTestRules(
|
||||
name = "GeneratedTestRules",
|
||||
test_files = glob(["*Test.java"]),
|
||||
deps = [":export"],
|
||||
)
|
||||
@@ -0,0 +1,86 @@
|
||||
// Copyright 2018 The Nomulus Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package google.registry.export;
|
||||
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
import static google.registry.export.CheckBackupAction.CHECK_BACKUP_KINDS_TO_LOAD_PARAM;
|
||||
import static google.registry.export.CheckBackupAction.CHECK_BACKUP_NAME_PARAM;
|
||||
import static google.registry.testing.TaskQueueHelper.assertTasksEnqueued;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import com.google.common.base.Joiner;
|
||||
import google.registry.export.datastore.DatastoreAdmin;
|
||||
import google.registry.export.datastore.DatastoreAdmin.Export;
|
||||
import google.registry.export.datastore.Operation;
|
||||
import google.registry.testing.AppEngineRule;
|
||||
import google.registry.testing.FakeResponse;
|
||||
import google.registry.testing.TaskQueueHelper.TaskMatcher;
|
||||
import org.junit.Before;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.junit.runners.JUnit4;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.MockitoJUnit;
|
||||
import org.mockito.junit.MockitoRule;
|
||||
|
||||
/** Unit tests for {@link BackupDatastoreAction}. */
|
||||
@RunWith(JUnit4.class)
|
||||
public class BackupDatastoreActionTest {
|
||||
|
||||
@Rule public final AppEngineRule appEngine = AppEngineRule.builder().withTaskQueue().build();
|
||||
@Rule public final MockitoRule mocks = MockitoJUnit.rule();
|
||||
|
||||
@Mock private DatastoreAdmin datastoreAdmin;
|
||||
@Mock private Export exportRequest;
|
||||
@Mock private Operation backupOperation;
|
||||
|
||||
private final FakeResponse response = new FakeResponse();
|
||||
private final BackupDatastoreAction action = new BackupDatastoreAction();
|
||||
|
||||
@Before
|
||||
public void before() throws Exception {
|
||||
action.datastoreAdmin = datastoreAdmin;
|
||||
action.response = response;
|
||||
|
||||
when(datastoreAdmin.export(
|
||||
"gs://registry-project-id-datastore-backups", ExportConstants.getBackupKinds()))
|
||||
.thenReturn(exportRequest);
|
||||
when(exportRequest.execute()).thenReturn(backupOperation);
|
||||
when(backupOperation.getName())
|
||||
.thenReturn("projects/registry-project-id/operations/ASA1ODYwNjc");
|
||||
when(backupOperation.getExportFolderUrl())
|
||||
.thenReturn("gs://registry-project-id-datastore-backups/some-id");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testBackup_enqueuesPollTask() {
|
||||
action.run();
|
||||
assertTasksEnqueued(
|
||||
CheckBackupAction.QUEUE,
|
||||
new TaskMatcher()
|
||||
.url(CheckBackupAction.PATH)
|
||||
.param(CHECK_BACKUP_NAME_PARAM, "projects/registry-project-id/operations/ASA1ODYwNjc")
|
||||
.param(
|
||||
CHECK_BACKUP_KINDS_TO_LOAD_PARAM,
|
||||
Joiner.on(",").join(ExportConstants.getReportingKinds()))
|
||||
.method("POST"));
|
||||
assertThat(response.getPayload())
|
||||
.isEqualTo(
|
||||
"Datastore backup started with name: "
|
||||
+ "projects/registry-project-id/operations/ASA1ODYwNjc\n"
|
||||
+ "Saving to gs://registry-project-id-datastore-backups/some-id");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,201 @@
|
||||
// Copyright 2017 The Nomulus Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package google.registry.export;
|
||||
|
||||
import static com.google.appengine.api.taskqueue.QueueFactory.getQueue;
|
||||
import static com.google.common.collect.Iterables.getOnlyElement;
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
import static google.registry.testing.JUnitBackports.assertThrows;
|
||||
import static google.registry.testing.TaskQueueHelper.assertTasksEnqueued;
|
||||
import static google.registry.testing.TestLogHandlerUtils.assertLogMessage;
|
||||
import static java.nio.charset.StandardCharsets.UTF_8;
|
||||
import static java.util.logging.Level.INFO;
|
||||
import static java.util.logging.Level.SEVERE;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import com.google.api.services.bigquery.Bigquery;
|
||||
import com.google.api.services.bigquery.model.ErrorProto;
|
||||
import com.google.api.services.bigquery.model.Job;
|
||||
import com.google.api.services.bigquery.model.JobReference;
|
||||
import com.google.api.services.bigquery.model.JobStatus;
|
||||
import com.google.appengine.api.taskqueue.TaskOptions;
|
||||
import com.google.appengine.api.taskqueue.TaskOptions.Method;
|
||||
import com.google.appengine.api.taskqueue.dev.QueueStateInfo.TaskStateInfo;
|
||||
import com.google.common.flogger.LoggerConfig;
|
||||
import google.registry.export.BigqueryPollJobAction.BigqueryPollJobEnqueuer;
|
||||
import google.registry.request.HttpException.BadRequestException;
|
||||
import google.registry.request.HttpException.NotModifiedException;
|
||||
import google.registry.testing.AppEngineRule;
|
||||
import google.registry.testing.FakeClock;
|
||||
import google.registry.testing.FakeSleeper;
|
||||
import google.registry.testing.TaskQueueHelper;
|
||||
import google.registry.testing.TaskQueueHelper.TaskMatcher;
|
||||
import google.registry.util.CapturingLogHandler;
|
||||
import google.registry.util.Retrier;
|
||||
import google.registry.util.TaskQueueUtils;
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.ObjectInputStream;
|
||||
import java.io.ObjectOutputStream;
|
||||
import org.junit.Before;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.junit.runners.JUnit4;
|
||||
|
||||
/** Unit tests for {@link BigqueryPollJobAction}. */
|
||||
@RunWith(JUnit4.class)
|
||||
public class BigqueryPollJobActionTest {
|
||||
|
||||
@Rule
|
||||
public final AppEngineRule appEngine = AppEngineRule.builder()
|
||||
.withDatastore()
|
||||
.withTaskQueue()
|
||||
.build();
|
||||
private static final String PROJECT_ID = "project_id";
|
||||
private static final String JOB_ID = "job_id";
|
||||
private static final String CHAINED_QUEUE_NAME = UpdateSnapshotViewAction.QUEUE;
|
||||
private static final TaskQueueUtils TASK_QUEUE_UTILS =
|
||||
new TaskQueueUtils(new Retrier(new FakeSleeper(new FakeClock()), 1));
|
||||
|
||||
private final Bigquery bigquery = mock(Bigquery.class);
|
||||
private final Bigquery.Jobs bigqueryJobs = mock(Bigquery.Jobs.class);
|
||||
private final Bigquery.Jobs.Get bigqueryJobsGet = mock(Bigquery.Jobs.Get.class);
|
||||
|
||||
private final CapturingLogHandler logHandler = new CapturingLogHandler();
|
||||
private BigqueryPollJobAction action = new BigqueryPollJobAction();
|
||||
|
||||
@Before
|
||||
public void before() throws Exception {
|
||||
action.bigquery = bigquery;
|
||||
when(bigquery.jobs()).thenReturn(bigqueryJobs);
|
||||
when(bigqueryJobs.get(PROJECT_ID, JOB_ID)).thenReturn(bigqueryJobsGet);
|
||||
action.taskQueueUtils = TASK_QUEUE_UTILS;
|
||||
action.projectId = PROJECT_ID;
|
||||
action.jobId = JOB_ID;
|
||||
action.chainedQueueName = () -> CHAINED_QUEUE_NAME;
|
||||
LoggerConfig.getConfig(BigqueryPollJobAction.class).addHandler(logHandler);
|
||||
}
|
||||
|
||||
private static TaskMatcher newPollJobTaskMatcher(String method) {
|
||||
return new TaskMatcher()
|
||||
.method(method)
|
||||
.url(BigqueryPollJobAction.PATH)
|
||||
.header(BigqueryPollJobAction.PROJECT_ID_HEADER, PROJECT_ID)
|
||||
.header(BigqueryPollJobAction.JOB_ID_HEADER, JOB_ID);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSuccess_enqueuePollTask() {
|
||||
new BigqueryPollJobEnqueuer(TASK_QUEUE_UTILS).enqueuePollTask(
|
||||
new JobReference().setProjectId(PROJECT_ID).setJobId(JOB_ID));
|
||||
assertTasksEnqueued(BigqueryPollJobAction.QUEUE, newPollJobTaskMatcher("GET"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSuccess_enqueuePollTask_withChainedTask() throws Exception {
|
||||
TaskOptions chainedTask = TaskOptions.Builder
|
||||
.withUrl("/_dr/something")
|
||||
.method(Method.POST)
|
||||
.header("X-Testing", "foo")
|
||||
.param("testing", "bar");
|
||||
new BigqueryPollJobEnqueuer(TASK_QUEUE_UTILS).enqueuePollTask(
|
||||
new JobReference().setProjectId(PROJECT_ID).setJobId(JOB_ID),
|
||||
chainedTask,
|
||||
getQueue(CHAINED_QUEUE_NAME));
|
||||
assertTasksEnqueued(BigqueryPollJobAction.QUEUE, newPollJobTaskMatcher("POST"));
|
||||
TaskStateInfo taskInfo = getOnlyElement(
|
||||
TaskQueueHelper.getQueueInfo(BigqueryPollJobAction.QUEUE).getTaskInfo());
|
||||
ByteArrayInputStream taskBodyBytes = new ByteArrayInputStream(taskInfo.getBodyAsBytes());
|
||||
TaskOptions taskOptions = (TaskOptions) new ObjectInputStream(taskBodyBytes).readObject();
|
||||
assertThat(taskOptions).isEqualTo(chainedTask);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSuccess_jobCompletedSuccessfully() throws Exception {
|
||||
when(bigqueryJobsGet.execute()).thenReturn(
|
||||
new Job().setStatus(new JobStatus().setState("DONE")));
|
||||
action.run();
|
||||
assertLogMessage(
|
||||
logHandler, INFO, String.format("Bigquery job succeeded - %s:%s", PROJECT_ID, JOB_ID));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSuccess_chainedPayloadAndJobSucceeded_enqueuesChainedTask() throws Exception {
|
||||
when(bigqueryJobsGet.execute()).thenReturn(
|
||||
new Job().setStatus(new JobStatus().setState("DONE")));
|
||||
|
||||
TaskOptions chainedTask =
|
||||
TaskOptions.Builder.withUrl("/_dr/something")
|
||||
.method(Method.POST)
|
||||
.header("X-Testing", "foo")
|
||||
.param("testing", "bar")
|
||||
.taskName("my_task_name");
|
||||
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
|
||||
new ObjectOutputStream(bytes).writeObject(chainedTask);
|
||||
action.payload = bytes.toByteArray();
|
||||
|
||||
action.run();
|
||||
assertLogMessage(
|
||||
logHandler, INFO, String.format("Bigquery job succeeded - %s:%s", PROJECT_ID, JOB_ID));
|
||||
assertLogMessage(
|
||||
logHandler,
|
||||
INFO,
|
||||
"Added chained task my_task_name for /_dr/something to queue " + CHAINED_QUEUE_NAME);
|
||||
assertTasksEnqueued(
|
||||
CHAINED_QUEUE_NAME,
|
||||
new TaskMatcher()
|
||||
.url("/_dr/something")
|
||||
.method("POST")
|
||||
.header("X-Testing", "foo")
|
||||
.param("testing", "bar")
|
||||
.taskName("my_task_name"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testJobFailed() throws Exception {
|
||||
when(bigqueryJobsGet.execute()).thenReturn(new Job().setStatus(
|
||||
new JobStatus()
|
||||
.setState("DONE")
|
||||
.setErrorResult(new ErrorProto().setMessage("Job failed"))));
|
||||
action.run();
|
||||
assertLogMessage(
|
||||
logHandler, SEVERE, String.format("Bigquery job failed - %s:%s", PROJECT_ID, JOB_ID));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testJobPending() throws Exception {
|
||||
when(bigqueryJobsGet.execute()).thenReturn(
|
||||
new Job().setStatus(new JobStatus().setState("PENDING")));
|
||||
assertThrows(NotModifiedException.class, action::run);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testJobStatusUnreadable() throws Exception {
|
||||
when(bigqueryJobsGet.execute()).thenThrow(IOException.class);
|
||||
assertThrows(NotModifiedException.class, action::run);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testFailure_badChainedTaskPayload() throws Exception {
|
||||
when(bigqueryJobsGet.execute()).thenReturn(
|
||||
new Job().setStatus(new JobStatus().setState("DONE")));
|
||||
action.payload = "payload".getBytes(UTF_8);
|
||||
BadRequestException thrown = assertThrows(BadRequestException.class, action::run);
|
||||
assertThat(thrown).hasMessageThat().contains("Cannot deserialize task from payload");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,223 @@
|
||||
// Copyright 2018 The Nomulus Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package google.registry.export;
|
||||
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
import static google.registry.export.CheckBackupAction.CHECK_BACKUP_KINDS_TO_LOAD_PARAM;
|
||||
import static google.registry.export.CheckBackupAction.CHECK_BACKUP_NAME_PARAM;
|
||||
import static google.registry.testing.JUnitBackports.assertThrows;
|
||||
import static google.registry.testing.TaskQueueHelper.assertNoTasksEnqueued;
|
||||
import static google.registry.testing.TaskQueueHelper.assertTasksEnqueued;
|
||||
import static org.mockito.ArgumentMatchers.anyString;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import com.google.api.client.googleapis.json.GoogleJsonResponseException;
|
||||
import com.google.api.client.http.HttpHeaders;
|
||||
import com.google.api.client.json.JsonFactory;
|
||||
import com.google.api.client.json.jackson2.JacksonFactory;
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
import google.registry.export.datastore.DatastoreAdmin;
|
||||
import google.registry.export.datastore.DatastoreAdmin.Get;
|
||||
import google.registry.export.datastore.Operation;
|
||||
import google.registry.request.Action.Method;
|
||||
import google.registry.request.HttpException.BadRequestException;
|
||||
import google.registry.request.HttpException.NoContentException;
|
||||
import google.registry.request.HttpException.NotModifiedException;
|
||||
import google.registry.testing.AppEngineRule;
|
||||
import google.registry.testing.FakeClock;
|
||||
import google.registry.testing.FakeResponse;
|
||||
import google.registry.testing.TaskQueueHelper.TaskMatcher;
|
||||
import google.registry.testing.TestDataHelper;
|
||||
import org.joda.time.DateTime;
|
||||
import org.joda.time.Duration;
|
||||
import org.junit.Before;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.junit.runners.JUnit4;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.MockitoJUnit;
|
||||
import org.mockito.junit.MockitoRule;
|
||||
|
||||
/** Unit tests for {@link CheckBackupAction}. */
|
||||
@RunWith(JUnit4.class)
|
||||
public class CheckBackupActionTest {
|
||||
|
||||
static final DateTime START_TIME = DateTime.parse("2014-08-01T01:02:03Z");
|
||||
static final DateTime COMPLETE_TIME = START_TIME.plus(Duration.standardMinutes(30));
|
||||
|
||||
static final JsonFactory JSON_FACTORY = JacksonFactory.getDefaultInstance();
|
||||
|
||||
@Rule public final AppEngineRule appEngine = AppEngineRule.builder().withTaskQueue().build();
|
||||
@Rule public final MockitoRule mocks = MockitoJUnit.rule();
|
||||
|
||||
@Mock private DatastoreAdmin datastoreAdmin;
|
||||
@Mock private Get getNotFoundBackupProgressRequest;
|
||||
@Mock private Get getBackupProgressRequest;
|
||||
private Operation backupOperation;
|
||||
|
||||
private final FakeResponse response = new FakeResponse();
|
||||
private final FakeClock clock = new FakeClock(COMPLETE_TIME.plusMillis(1000));
|
||||
private final CheckBackupAction action = new CheckBackupAction();
|
||||
|
||||
@Before
|
||||
public void before() throws Exception {
|
||||
action.requestMethod = Method.POST;
|
||||
action.datastoreAdmin = datastoreAdmin;
|
||||
action.clock = clock;
|
||||
action.backupName = "some_backup";
|
||||
action.kindsToLoadParam = "one,two";
|
||||
action.response = response;
|
||||
|
||||
when(datastoreAdmin.get(anyString())).thenReturn(getBackupProgressRequest);
|
||||
when(getBackupProgressRequest.execute()).thenAnswer(arg -> backupOperation);
|
||||
}
|
||||
|
||||
private void setPendingBackup() throws Exception {
|
||||
backupOperation =
|
||||
JSON_FACTORY.fromString(
|
||||
TestDataHelper.loadFile(
|
||||
CheckBackupActionTest.class, "backup_operation_in_progress.json"),
|
||||
Operation.class);
|
||||
}
|
||||
|
||||
private void setCompleteBackup() throws Exception {
|
||||
backupOperation =
|
||||
JSON_FACTORY.fromString(
|
||||
TestDataHelper.loadFile(CheckBackupActionTest.class, "backup_operation_success.json"),
|
||||
Operation.class);
|
||||
}
|
||||
|
||||
private void setBackupNotFound() throws Exception {
|
||||
when(datastoreAdmin.get(anyString())).thenReturn(getNotFoundBackupProgressRequest);
|
||||
when(getNotFoundBackupProgressRequest.execute())
|
||||
.thenThrow(
|
||||
new GoogleJsonResponseException(
|
||||
new GoogleJsonResponseException.Builder(404, "NOT_FOUND", new HttpHeaders())
|
||||
.setMessage("No backup found"),
|
||||
null));
|
||||
}
|
||||
|
||||
private static void assertLoadTaskEnqueued(String id, String folder, String kinds) {
|
||||
assertTasksEnqueued(
|
||||
"export-snapshot",
|
||||
new TaskMatcher()
|
||||
.url("/_dr/task/uploadDatastoreBackup")
|
||||
.method("POST")
|
||||
.param("id", id)
|
||||
.param("folder", folder)
|
||||
.param("kinds", kinds));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSuccess_enqueuePollTask() {
|
||||
CheckBackupAction.enqueuePollTask("some_backup_name", ImmutableSet.of("one", "two", "three"));
|
||||
assertTasksEnqueued(
|
||||
CheckBackupAction.QUEUE,
|
||||
new TaskMatcher()
|
||||
.url(CheckBackupAction.PATH)
|
||||
.param(CHECK_BACKUP_NAME_PARAM, "some_backup_name")
|
||||
.param(CHECK_BACKUP_KINDS_TO_LOAD_PARAM, "one,two,three")
|
||||
.method("POST"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testPost_forPendingBackup_returnsNotModified() throws Exception {
|
||||
setPendingBackup();
|
||||
|
||||
NotModifiedException thrown = assertThrows(NotModifiedException.class, action::run);
|
||||
assertThat(thrown)
|
||||
.hasMessageThat()
|
||||
.contains("Datastore backup some_backup still in progress: Progress: N/A");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testPost_forStalePendingBackupBackup_returnsNoContent() throws Exception {
|
||||
setPendingBackup();
|
||||
clock.setTo(
|
||||
START_TIME
|
||||
.plus(Duration.standardHours(20))
|
||||
.plus(Duration.standardMinutes(3))
|
||||
.plus(Duration.millis(1234)));
|
||||
|
||||
NoContentException thrown = assertThrows(NoContentException.class, action::run);
|
||||
assertThat(thrown)
|
||||
.hasMessageThat()
|
||||
.contains(
|
||||
"Datastore backup some_backup abandoned - "
|
||||
+ "not complete after 20 hours, 3 minutes and 1 second. Progress: Progress: N/A");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testPost_forCompleteBackup_enqueuesLoadTask() throws Exception {
|
||||
setCompleteBackup();
|
||||
action.run();
|
||||
assertLoadTaskEnqueued(
|
||||
"2014-08-01T01:02:03_99364",
|
||||
"gs://registry-project-id-datastore-export-test/2014-08-01T01:02:03_99364",
|
||||
"one,two");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testPost_forCompleteBackup_withExtraKindsToLoad_enqueuesLoadTask() throws Exception {
|
||||
setCompleteBackup();
|
||||
action.kindsToLoadParam = "one,foo";
|
||||
|
||||
action.run();
|
||||
assertLoadTaskEnqueued(
|
||||
"2014-08-01T01:02:03_99364",
|
||||
"gs://registry-project-id-datastore-export-test/2014-08-01T01:02:03_99364",
|
||||
"one");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testPost_forCompleteBackup_withEmptyKindsToLoad_skipsLoadTask() throws Exception {
|
||||
setCompleteBackup();
|
||||
action.kindsToLoadParam = "";
|
||||
|
||||
action.run();
|
||||
assertNoTasksEnqueued("export-snapshot");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testPost_forBadBackup_returnsBadRequest() throws Exception {
|
||||
setBackupNotFound();
|
||||
|
||||
BadRequestException thrown = assertThrows(BadRequestException.class, action::run);
|
||||
assertThat(thrown).hasMessageThat().contains("Bad backup name some_backup: No backup found");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGet_returnsInformation() throws Exception {
|
||||
setCompleteBackup();
|
||||
action.requestMethod = Method.GET;
|
||||
|
||||
action.run();
|
||||
assertThat(response.getPayload())
|
||||
.isEqualTo(
|
||||
TestDataHelper.loadFile(
|
||||
CheckBackupActionTest.class, "pretty_printed_success_backup_operation.json")
|
||||
.trim());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGet_forBadBackup_returnsError() throws Exception {
|
||||
setBackupNotFound();
|
||||
action.requestMethod = Method.GET;
|
||||
|
||||
BadRequestException thrown = assertThrows(BadRequestException.class, action::run);
|
||||
assertThat(thrown).hasMessageThat().contains("Bad backup name some_backup: No backup found");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
// Copyright 2017 The Nomulus Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package google.registry.export;
|
||||
|
||||
import static com.google.common.base.Strings.repeat;
|
||||
import static com.google.common.collect.ImmutableList.toImmutableList;
|
||||
import static com.google.common.io.Resources.getResource;
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
import static com.google.common.truth.Truth.assertWithMessage;
|
||||
import static google.registry.export.ExportConstants.getBackupKinds;
|
||||
import static google.registry.export.ExportConstants.getReportingKinds;
|
||||
import static google.registry.util.ResourceUtils.readResourceUtf8;
|
||||
|
||||
import com.google.common.base.Joiner;
|
||||
import com.google.common.base.Splitter;
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
import com.google.common.collect.Streams;
|
||||
import com.google.re2j.Pattern;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.junit.runners.JUnit4;
|
||||
|
||||
/** Unit tests for {@link ExportConstants}. */
|
||||
@RunWith(JUnit4.class)
|
||||
public class ExportConstantsTest {
|
||||
|
||||
private static final String GOLDEN_BACKUP_KINDS_FILENAME = "backup_kinds.txt";
|
||||
|
||||
private static final String GOLDEN_REPORTING_KINDS_FILENAME = "reporting_kinds.txt";
|
||||
|
||||
private static final String UPDATE_INSTRUCTIONS_TEMPLATE = Joiner.on('\n').join(
|
||||
"",
|
||||
repeat("-", 80),
|
||||
"Your changes affect the list of %s kinds in the golden file:",
|
||||
" %s",
|
||||
"If these changes are desired, update the golden file with the following contents:",
|
||||
repeat("=", 80),
|
||||
"%s",
|
||||
repeat("=", 80),
|
||||
"");
|
||||
|
||||
@Test
|
||||
public void testBackupKinds_matchGoldenBackupKindsFile() {
|
||||
checkKindsMatchGoldenFile("backed-up", GOLDEN_BACKUP_KINDS_FILENAME, getBackupKinds());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testReportingKinds_matchGoldenReportingKindsFile() {
|
||||
checkKindsMatchGoldenFile("reporting", GOLDEN_REPORTING_KINDS_FILENAME, getReportingKinds());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testReportingKinds_areSubsetOfBackupKinds() {
|
||||
assertThat(getBackupKinds()).containsAtLeastElementsIn(getReportingKinds());
|
||||
}
|
||||
|
||||
private static void checkKindsMatchGoldenFile(
|
||||
String kindsName, String goldenFilename, ImmutableSet<String> actualKinds) {
|
||||
String updateInstructions =
|
||||
String.format(
|
||||
UPDATE_INSTRUCTIONS_TEMPLATE,
|
||||
kindsName,
|
||||
getResource(ExportConstantsTest.class, goldenFilename).toString(),
|
||||
Joiner.on('\n').join(actualKinds));
|
||||
assertWithMessage(updateInstructions)
|
||||
.that(actualKinds)
|
||||
.containsExactlyElementsIn(extractListFromFile(goldenFilename))
|
||||
.inOrder();
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method to extract list from file
|
||||
*
|
||||
* @param filename
|
||||
* @return ImmutableList<String>
|
||||
*/
|
||||
private static ImmutableList<String> extractListFromFile(String filename) {
|
||||
String fileContents = readResourceUtf8(ExportConstantsTest.class, filename);
|
||||
final Pattern stripComments = Pattern.compile("\\s*#.*$");
|
||||
return Streams.stream(Splitter.on('\n').split(fileContents.trim()))
|
||||
.map(line -> stripComments.matcher(line).replaceFirst(""))
|
||||
.collect(toImmutableList());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,164 @@
|
||||
// Copyright 2017 The Nomulus Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package google.registry.export;
|
||||
|
||||
import static com.google.appengine.tools.cloudstorage.GcsServiceFactory.createGcsService;
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
import static google.registry.export.ExportDomainListsAction.ExportDomainListsReducer.EXPORT_MIME_TYPE;
|
||||
import static google.registry.export.ExportDomainListsAction.ExportDomainListsReducer.REGISTERED_DOMAINS_FILENAME;
|
||||
import static google.registry.testing.DatastoreHelper.createTld;
|
||||
import static google.registry.testing.DatastoreHelper.persistActiveDomain;
|
||||
import static google.registry.testing.DatastoreHelper.persistDeletedDomain;
|
||||
import static google.registry.testing.DatastoreHelper.persistResource;
|
||||
import static google.registry.testing.GcsTestingUtils.readGcsFile;
|
||||
import static google.registry.testing.JUnitBackports.assertThrows;
|
||||
import static java.nio.charset.StandardCharsets.UTF_8;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.verifyNoMoreInteractions;
|
||||
|
||||
import com.google.appengine.tools.cloudstorage.GcsFilename;
|
||||
import com.google.appengine.tools.cloudstorage.GcsService;
|
||||
import com.google.appengine.tools.cloudstorage.ListOptions;
|
||||
import com.google.appengine.tools.cloudstorage.ListResult;
|
||||
import google.registry.export.ExportDomainListsAction.ExportDomainListsReducer;
|
||||
import google.registry.model.registry.Registry;
|
||||
import google.registry.model.registry.Registry.TldType;
|
||||
import google.registry.storage.drive.DriveConnection;
|
||||
import google.registry.testing.FakeResponse;
|
||||
import google.registry.testing.mapreduce.MapreduceTestCase;
|
||||
import java.io.FileNotFoundException;
|
||||
import org.joda.time.DateTime;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.junit.runners.JUnit4;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
|
||||
/** Unit tests for {@link ExportDomainListsAction}. */
|
||||
@RunWith(JUnit4.class)
|
||||
public class ExportDomainListsActionTest extends MapreduceTestCase<ExportDomainListsAction> {
|
||||
|
||||
private GcsService gcsService;
|
||||
private DriveConnection driveConnection = mock(DriveConnection.class);
|
||||
private ArgumentCaptor<byte[]> bytesExportedToDrive = ArgumentCaptor.forClass(byte[].class);
|
||||
private final FakeResponse response = new FakeResponse();
|
||||
|
||||
@Before
|
||||
public void init() {
|
||||
createTld("tld");
|
||||
createTld("testtld");
|
||||
persistResource(Registry.get("tld").asBuilder().setDriveFolderId("brouhaha").build());
|
||||
persistResource(Registry.get("testtld").asBuilder().setTldType(TldType.TEST).build());
|
||||
|
||||
ExportDomainListsReducer.setDriveConnectionForTesting(() -> driveConnection);
|
||||
|
||||
action = new ExportDomainListsAction();
|
||||
action.mrRunner = makeDefaultRunner();
|
||||
action.response = response;
|
||||
action.gcsBucket = "outputbucket";
|
||||
action.gcsBufferSize = 500;
|
||||
gcsService = createGcsService();
|
||||
}
|
||||
|
||||
private void runMapreduce() throws Exception {
|
||||
action.run();
|
||||
executeTasksUntilEmpty("mapreduce");
|
||||
}
|
||||
|
||||
private void verifyExportedToDrive(String folderId, String domains) throws Exception {
|
||||
verify(driveConnection)
|
||||
.createOrUpdateFile(
|
||||
eq(REGISTERED_DOMAINS_FILENAME),
|
||||
eq(EXPORT_MIME_TYPE),
|
||||
eq(folderId),
|
||||
bytesExportedToDrive.capture());
|
||||
assertThat(new String(bytesExportedToDrive.getValue(), UTF_8)).isEqualTo(domains);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test_writesLinkToMapreduceConsoleToResponse() throws Exception {
|
||||
runMapreduce();
|
||||
assertThat(response.getPayload())
|
||||
.startsWith(
|
||||
"Mapreduce console: https://backend-dot-projectid.appspot.com"
|
||||
+ "/_ah/pipeline/status.html?root=");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test_outputsOnlyActiveDomains() throws Exception {
|
||||
persistActiveDomain("onetwo.tld");
|
||||
persistActiveDomain("rudnitzky.tld");
|
||||
persistDeletedDomain("mortuary.tld", DateTime.parse("2001-03-14T10:11:12Z"));
|
||||
runMapreduce();
|
||||
GcsFilename existingFile = new GcsFilename("outputbucket", "tld.txt");
|
||||
String tlds = new String(readGcsFile(gcsService, existingFile), UTF_8);
|
||||
// Check that it only contains the active domains, not the dead one.
|
||||
assertThat(tlds).isEqualTo("onetwo.tld\nrudnitzky.tld");
|
||||
verifyExportedToDrive("brouhaha", "onetwo.tld\nrudnitzky.tld");
|
||||
verifyNoMoreInteractions(driveConnection);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test_outputsOnlyDomainsOnRealTlds() throws Exception {
|
||||
persistActiveDomain("onetwo.tld");
|
||||
persistActiveDomain("rudnitzky.tld");
|
||||
persistActiveDomain("wontgo.testtld");
|
||||
runMapreduce();
|
||||
GcsFilename existingFile = new GcsFilename("outputbucket", "tld.txt");
|
||||
String tlds = new String(readGcsFile(gcsService, existingFile), UTF_8).trim();
|
||||
// Check that it only contains the domains on the real TLD, and not the test one.
|
||||
assertThat(tlds).isEqualTo("onetwo.tld\nrudnitzky.tld");
|
||||
// Make sure that the test TLD file wasn't written out.
|
||||
GcsFilename nonexistentFile = new GcsFilename("outputbucket", "testtld.txt");
|
||||
assertThrows(FileNotFoundException.class, () -> readGcsFile(gcsService, nonexistentFile));
|
||||
ListResult ls = gcsService.list("outputbucket", ListOptions.DEFAULT);
|
||||
assertThat(ls.next().getName()).isEqualTo("tld.txt");
|
||||
// Make sure that no other files were written out.
|
||||
assertThat(ls.hasNext()).isFalse();
|
||||
verifyExportedToDrive("brouhaha", "onetwo.tld\nrudnitzky.tld");
|
||||
verifyNoMoreInteractions(driveConnection);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test_outputsDomainsFromDifferentTldsToMultipleFiles() throws Exception {
|
||||
createTld("tldtwo");
|
||||
persistResource(Registry.get("tldtwo").asBuilder().setDriveFolderId("hooray").build());
|
||||
|
||||
createTld("tldthree");
|
||||
// You'd think this test was written around Christmas, but it wasn't.
|
||||
persistActiveDomain("dasher.tld");
|
||||
persistActiveDomain("prancer.tld");
|
||||
persistActiveDomain("rudolph.tldtwo");
|
||||
persistActiveDomain("santa.tldtwo");
|
||||
persistActiveDomain("buddy.tldtwo");
|
||||
persistActiveDomain("cupid.tldthree");
|
||||
runMapreduce();
|
||||
GcsFilename firstTldFile = new GcsFilename("outputbucket", "tld.txt");
|
||||
String tlds = new String(readGcsFile(gcsService, firstTldFile), UTF_8).trim();
|
||||
assertThat(tlds).isEqualTo("dasher.tld\nprancer.tld");
|
||||
GcsFilename secondTldFile = new GcsFilename("outputbucket", "tldtwo.txt");
|
||||
String moreTlds = new String(readGcsFile(gcsService, secondTldFile), UTF_8).trim();
|
||||
assertThat(moreTlds).isEqualTo("buddy.tldtwo\nrudolph.tldtwo\nsanta.tldtwo");
|
||||
GcsFilename thirdTldFile = new GcsFilename("outputbucket", "tldthree.txt");
|
||||
String evenMoreTlds = new String(readGcsFile(gcsService, thirdTldFile), UTF_8).trim();
|
||||
assertThat(evenMoreTlds).isEqualTo("cupid.tldthree");
|
||||
verifyExportedToDrive("brouhaha", "dasher.tld\nprancer.tld");
|
||||
verifyExportedToDrive("hooray", "buddy.tldtwo\nrudolph.tldtwo\nsanta.tldtwo");
|
||||
// tldthree does not have a drive id, so no export to drive is performed.
|
||||
verifyNoMoreInteractions(driveConnection);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,195 @@
|
||||
// Copyright 2018 The Nomulus Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package google.registry.export;
|
||||
|
||||
import static com.google.common.net.MediaType.PLAIN_TEXT_UTF_8;
|
||||
import static google.registry.export.ExportPremiumTermsAction.EXPORT_MIME_TYPE;
|
||||
import static google.registry.export.ExportPremiumTermsAction.PREMIUM_TERMS_FILENAME;
|
||||
import static google.registry.model.registry.label.PremiumListUtils.deletePremiumList;
|
||||
import static google.registry.model.registry.label.PremiumListUtils.savePremiumListAndEntries;
|
||||
import static google.registry.testing.DatastoreHelper.createTld;
|
||||
import static google.registry.testing.DatastoreHelper.deleteTld;
|
||||
import static google.registry.testing.DatastoreHelper.persistResource;
|
||||
import static google.registry.testing.JUnitBackports.assertThrows;
|
||||
import static java.nio.charset.StandardCharsets.UTF_8;
|
||||
import static javax.servlet.http.HttpServletResponse.SC_INTERNAL_SERVER_ERROR;
|
||||
import static javax.servlet.http.HttpServletResponse.SC_OK;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyString;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.verifyNoMoreInteractions;
|
||||
import static org.mockito.Mockito.verifyZeroInteractions;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.google.common.net.MediaType;
|
||||
import google.registry.model.registry.Registry;
|
||||
import google.registry.model.registry.label.PremiumList;
|
||||
import google.registry.request.Response;
|
||||
import google.registry.storage.drive.DriveConnection;
|
||||
import google.registry.testing.AppEngineRule;
|
||||
import java.io.IOException;
|
||||
import org.junit.Before;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.junit.runners.JUnit4;
|
||||
import org.mockito.Matchers;
|
||||
|
||||
@RunWith(JUnit4.class)
|
||||
public class ExportPremiumTermsActionTest {
|
||||
|
||||
private static final String DISCLAIMER_WITH_NEWLINE = "# Premium Terms Export Disclaimer\n";
|
||||
private static final ImmutableList<String> PREMIUM_NAMES =
|
||||
ImmutableList.of("2048,USD 549", "0,USD 549");
|
||||
private static final String EXPECTED_FILE_CONTENT =
|
||||
DISCLAIMER_WITH_NEWLINE + "0,USD 549.00\n" + "2048,USD 549.00\n";
|
||||
|
||||
@Rule public final AppEngineRule appEngine = AppEngineRule.builder().withDatastore().build();
|
||||
|
||||
private final DriveConnection driveConnection = mock(DriveConnection.class);
|
||||
private final Response response = mock(Response.class);
|
||||
|
||||
private void runAction(String tld) {
|
||||
ExportPremiumTermsAction action = new ExportPremiumTermsAction();
|
||||
action.response = response;
|
||||
action.driveConnection = driveConnection;
|
||||
action.exportDisclaimer = DISCLAIMER_WITH_NEWLINE;
|
||||
action.tld = tld;
|
||||
action.run();
|
||||
}
|
||||
|
||||
@Before
|
||||
public void setup() throws Exception {
|
||||
createTld("tld");
|
||||
PremiumList pl = new PremiumList.Builder().setName("pl-name").build();
|
||||
savePremiumListAndEntries(pl, PREMIUM_NAMES);
|
||||
persistResource(
|
||||
Registry.get("tld").asBuilder().setPremiumList(pl).setDriveFolderId("folder_id").build());
|
||||
when(driveConnection.createOrUpdateFile(
|
||||
anyString(), any(MediaType.class), eq("folder_id"), any(byte[].class)))
|
||||
.thenReturn("file_id");
|
||||
when(driveConnection.createOrUpdateFile(
|
||||
anyString(), any(MediaType.class), eq("bad_folder_id"), any(byte[].class)))
|
||||
.thenThrow(new IOException());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test_exportPremiumTerms_success() throws IOException {
|
||||
runAction("tld");
|
||||
|
||||
verify(driveConnection)
|
||||
.createOrUpdateFile(
|
||||
PREMIUM_TERMS_FILENAME,
|
||||
EXPORT_MIME_TYPE,
|
||||
"folder_id",
|
||||
EXPECTED_FILE_CONTENT.getBytes(UTF_8));
|
||||
verifyNoMoreInteractions(driveConnection);
|
||||
|
||||
verify(response).setStatus(SC_OK);
|
||||
verify(response).setPayload("file_id");
|
||||
verify(response).setContentType(PLAIN_TEXT_UTF_8);
|
||||
verifyNoMoreInteractions(response);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test_exportPremiumTerms_success_emptyPremiumList() throws IOException {
|
||||
PremiumList pl = new PremiumList.Builder().setName("pl-name").build();
|
||||
savePremiumListAndEntries(pl, ImmutableList.of());
|
||||
runAction("tld");
|
||||
|
||||
verify(driveConnection)
|
||||
.createOrUpdateFile(
|
||||
PREMIUM_TERMS_FILENAME,
|
||||
EXPORT_MIME_TYPE,
|
||||
"folder_id",
|
||||
DISCLAIMER_WITH_NEWLINE.getBytes(UTF_8));
|
||||
verifyNoMoreInteractions(driveConnection);
|
||||
|
||||
verify(response).setStatus(SC_OK);
|
||||
verify(response).setPayload("file_id");
|
||||
verify(response).setContentType(PLAIN_TEXT_UTF_8);
|
||||
verifyNoMoreInteractions(response);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test_exportPremiumTerms_doNothing_listNotConfigured() {
|
||||
persistResource(Registry.get("tld").asBuilder().setPremiumList(null).build());
|
||||
runAction("tld");
|
||||
|
||||
verifyZeroInteractions(driveConnection);
|
||||
verify(response).setStatus(SC_OK);
|
||||
verify(response).setPayload("No premium lists configured");
|
||||
verify(response).setContentType(PLAIN_TEXT_UTF_8);
|
||||
verifyNoMoreInteractions(response);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testExportPremiumTerms_doNothing_driveIdNotConfiguredInTld() {
|
||||
persistResource(Registry.get("tld").asBuilder().setDriveFolderId(null).build());
|
||||
runAction("tld");
|
||||
|
||||
verifyZeroInteractions(driveConnection);
|
||||
verify(response).setStatus(SC_OK);
|
||||
verify(response)
|
||||
.setPayload("Skipping export because no Drive folder is associated with this TLD");
|
||||
verify(response).setContentType(PLAIN_TEXT_UTF_8);
|
||||
verifyNoMoreInteractions(response);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test_exportPremiumTerms_failure_noSuchTld() {
|
||||
deleteTld("tld");
|
||||
assertThrows(RuntimeException.class, () -> runAction("tld"));
|
||||
|
||||
verifyZeroInteractions(driveConnection);
|
||||
verify(response).setStatus(SC_INTERNAL_SERVER_ERROR);
|
||||
verify(response).setPayload(anyString());
|
||||
verify(response).setContentType(PLAIN_TEXT_UTF_8);
|
||||
verifyNoMoreInteractions(response);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test_exportPremiumTerms_failure_noPremiumList() {
|
||||
deletePremiumList(new PremiumList.Builder().setName("pl-name").build());
|
||||
assertThrows(RuntimeException.class, () -> runAction("tld"));
|
||||
|
||||
verifyZeroInteractions(driveConnection);
|
||||
verify(response).setStatus(SC_INTERNAL_SERVER_ERROR);
|
||||
verify(response).setPayload("Could not load premium list for " + "tld");
|
||||
verify(response).setContentType(PLAIN_TEXT_UTF_8);
|
||||
verifyNoMoreInteractions(response);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testExportPremiumTerms_failure_driveIdThrowsException() throws IOException {
|
||||
persistResource(Registry.get("tld").asBuilder().setDriveFolderId("bad_folder_id").build());
|
||||
assertThrows(RuntimeException.class, () -> runAction("tld"));
|
||||
|
||||
verify(driveConnection)
|
||||
.createOrUpdateFile(
|
||||
PREMIUM_TERMS_FILENAME,
|
||||
EXPORT_MIME_TYPE,
|
||||
"bad_folder_id",
|
||||
EXPECTED_FILE_CONTENT.getBytes(UTF_8));
|
||||
verifyNoMoreInteractions(driveConnection);
|
||||
verify(response).setStatus(SC_INTERNAL_SERVER_ERROR);
|
||||
verify(response).setPayload(Matchers.contains("Error exporting premium terms file to Drive."));
|
||||
verify(response).setContentType(PLAIN_TEXT_UTF_8);
|
||||
verifyNoMoreInteractions(response);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,138 @@
|
||||
// Copyright 2017 The Nomulus Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package google.registry.export;
|
||||
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
import static google.registry.export.ExportReservedTermsAction.EXPORT_MIME_TYPE;
|
||||
import static google.registry.export.ExportReservedTermsAction.RESERVED_TERMS_FILENAME;
|
||||
import static google.registry.testing.DatastoreHelper.createTld;
|
||||
import static google.registry.testing.DatastoreHelper.persistReservedList;
|
||||
import static google.registry.testing.DatastoreHelper.persistResource;
|
||||
import static google.registry.testing.JUnitBackports.assertThrows;
|
||||
import static java.nio.charset.StandardCharsets.UTF_8;
|
||||
import static javax.servlet.http.HttpServletResponse.SC_INTERNAL_SERVER_ERROR;
|
||||
import static javax.servlet.http.HttpServletResponse.SC_OK;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyString;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
import com.google.common.net.MediaType;
|
||||
import google.registry.model.registry.Registry;
|
||||
import google.registry.model.registry.label.ReservedList;
|
||||
import google.registry.request.Response;
|
||||
import google.registry.storage.drive.DriveConnection;
|
||||
import google.registry.testing.AppEngineRule;
|
||||
import java.io.IOException;
|
||||
import org.junit.Before;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.junit.runners.JUnit4;
|
||||
|
||||
/** Unit tests for {@link ExportReservedTermsAction}. */
|
||||
@RunWith(JUnit4.class)
|
||||
public class ExportReservedTermsActionTest {
|
||||
|
||||
@Rule
|
||||
public final AppEngineRule appEngine = AppEngineRule.builder()
|
||||
.withDatastore()
|
||||
.build();
|
||||
|
||||
private final DriveConnection driveConnection = mock(DriveConnection.class);
|
||||
private final Response response = mock(Response.class);
|
||||
|
||||
private void runAction(String tld) {
|
||||
ExportReservedTermsAction action = new ExportReservedTermsAction();
|
||||
action.response = response;
|
||||
action.driveConnection = driveConnection;
|
||||
action.exportUtils = new ExportUtils("# This is a disclaimer.");
|
||||
action.tld = tld;
|
||||
action.run();
|
||||
}
|
||||
|
||||
@Before
|
||||
public void init() throws Exception {
|
||||
ReservedList rl = persistReservedList(
|
||||
"tld-reserved",
|
||||
"lol,FULLY_BLOCKED",
|
||||
"cat,FULLY_BLOCKED");
|
||||
createTld("tld");
|
||||
persistResource(Registry.get("tld").asBuilder()
|
||||
.setReservedLists(rl)
|
||||
.setDriveFolderId("brouhaha").build());
|
||||
when(driveConnection.createOrUpdateFile(
|
||||
anyString(),
|
||||
any(MediaType.class),
|
||||
anyString(),
|
||||
any(byte[].class))).thenReturn("1001");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test_uploadFileToDrive_succeeds() throws Exception {
|
||||
runAction("tld");
|
||||
byte[] expected = "# This is a disclaimer.\ncat\nlol\n".getBytes(UTF_8);
|
||||
verify(driveConnection)
|
||||
.createOrUpdateFile(RESERVED_TERMS_FILENAME, EXPORT_MIME_TYPE, "brouhaha", expected);
|
||||
verify(response).setStatus(SC_OK);
|
||||
verify(response).setPayload("1001");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test_uploadFileToDrive_doesNothingIfReservedListsNotConfigured() {
|
||||
persistResource(
|
||||
Registry.get("tld")
|
||||
.asBuilder()
|
||||
.setReservedLists(ImmutableSet.of())
|
||||
.setDriveFolderId(null)
|
||||
.build());
|
||||
runAction("tld");
|
||||
verify(response).setStatus(SC_OK);
|
||||
verify(response).setPayload("No reserved lists configured");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test_uploadFileToDrive_doesNothingWhenDriveFolderIdIsNull() {
|
||||
persistResource(Registry.get("tld").asBuilder().setDriveFolderId(null).build());
|
||||
runAction("tld");
|
||||
verify(response).setStatus(SC_OK);
|
||||
verify(response)
|
||||
.setPayload("Skipping export because no Drive folder is associated with this TLD");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test_uploadFileToDrive_failsWhenDriveCannotBeReached() throws Exception {
|
||||
when(driveConnection.createOrUpdateFile(
|
||||
anyString(),
|
||||
any(MediaType.class),
|
||||
anyString(),
|
||||
any(byte[].class))).thenThrow(new IOException("errorMessage"));
|
||||
RuntimeException thrown = assertThrows(RuntimeException.class, () -> runAction("tld"));
|
||||
verify(response).setStatus(SC_INTERNAL_SERVER_ERROR);
|
||||
assertThat(thrown).hasCauseThat().hasMessageThat().isEqualTo("errorMessage");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test_uploadFileToDrive_failsWhenTldDoesntExist() {
|
||||
RuntimeException thrown = assertThrows(RuntimeException.class, () -> runAction("fakeTld"));
|
||||
verify(response).setStatus(SC_INTERNAL_SERVER_ERROR);
|
||||
assertThat(thrown)
|
||||
.hasCauseThat()
|
||||
.hasMessageThat()
|
||||
.isEqualTo("No registry object found for fakeTld");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
// Copyright 2017 The Nomulus Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package google.registry.export;
|
||||
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
import static google.registry.testing.DatastoreHelper.createTld;
|
||||
import static google.registry.testing.DatastoreHelper.persistReservedList;
|
||||
import static google.registry.testing.DatastoreHelper.persistResource;
|
||||
|
||||
import google.registry.model.registry.Registry;
|
||||
import google.registry.model.registry.label.ReservedList;
|
||||
import google.registry.testing.AppEngineRule;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.junit.runners.JUnit4;
|
||||
|
||||
/** Unit tests for {@link ExportUtils}. */
|
||||
@RunWith(JUnit4.class)
|
||||
public class ExportUtilsTest {
|
||||
|
||||
@Rule
|
||||
public final AppEngineRule appEngine = AppEngineRule.builder()
|
||||
.withDatastore()
|
||||
.build();
|
||||
|
||||
@Test
|
||||
public void test_exportReservedTerms() {
|
||||
ReservedList rl1 = persistReservedList(
|
||||
"tld-reserved1",
|
||||
"lol,FULLY_BLOCKED",
|
||||
"cat,FULLY_BLOCKED");
|
||||
ReservedList rl2 = persistReservedList(
|
||||
"tld-reserved2",
|
||||
"lol,NAME_COLLISION",
|
||||
"snow,FULLY_BLOCKED");
|
||||
ReservedList rl3 = persistReservedList(
|
||||
"tld-reserved3",
|
||||
false,
|
||||
"tine,FULLY_BLOCKED");
|
||||
createTld("tld");
|
||||
persistResource(Registry.get("tld").asBuilder().setReservedLists(rl1, rl2, rl3).build());
|
||||
// Should not contain jimmy, tine, or oval.
|
||||
assertThat(new ExportUtils("# This is a disclaimer.").exportReservedTerms(Registry.get("tld")))
|
||||
.isEqualTo("# This is a disclaimer.\ncat\nlol\nsnow\n");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,234 @@
|
||||
// Copyright 2017 The Nomulus Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package google.registry.export;
|
||||
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
import static google.registry.export.SyncGroupMembersAction.getGroupEmailAddressForContactType;
|
||||
import static google.registry.model.ofy.ObjectifyService.ofy;
|
||||
import static google.registry.model.registrar.RegistrarContact.Type.ADMIN;
|
||||
import static google.registry.model.registrar.RegistrarContact.Type.MARKETING;
|
||||
import static google.registry.model.registrar.RegistrarContact.Type.TECH;
|
||||
import static google.registry.testing.DatastoreHelper.loadRegistrar;
|
||||
import static google.registry.testing.DatastoreHelper.persistResource;
|
||||
import static javax.servlet.http.HttpServletResponse.SC_INTERNAL_SERVER_ERROR;
|
||||
import static javax.servlet.http.HttpServletResponse.SC_OK;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyString;
|
||||
import static org.mockito.Mockito.doThrow;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.times;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
import com.google.common.collect.Iterables;
|
||||
import google.registry.groups.DirectoryGroupsConnection;
|
||||
import google.registry.groups.GroupsConnection.Role;
|
||||
import google.registry.model.registrar.Registrar;
|
||||
import google.registry.model.registrar.RegistrarContact;
|
||||
import google.registry.request.Response;
|
||||
import google.registry.testing.AppEngineRule;
|
||||
import google.registry.testing.FakeClock;
|
||||
import google.registry.testing.FakeSleeper;
|
||||
import google.registry.testing.InjectRule;
|
||||
import google.registry.util.Retrier;
|
||||
import java.io.IOException;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.junit.runners.JUnit4;
|
||||
|
||||
/**
|
||||
* Unit tests for {@link SyncGroupMembersAction}.
|
||||
*
|
||||
* <p>Note that this relies on the registrars NewRegistrar and TheRegistrar created by default in
|
||||
* {@link AppEngineRule}.
|
||||
*/
|
||||
@RunWith(JUnit4.class)
|
||||
public class SyncGroupMembersActionTest {
|
||||
|
||||
@Rule
|
||||
public final AppEngineRule appEngine = AppEngineRule.builder()
|
||||
.withDatastore()
|
||||
.build();
|
||||
|
||||
@Rule
|
||||
public final InjectRule inject = new InjectRule();
|
||||
|
||||
private final DirectoryGroupsConnection connection = mock(DirectoryGroupsConnection.class);
|
||||
private final Response response = mock(Response.class);
|
||||
|
||||
private void runAction() {
|
||||
SyncGroupMembersAction action = new SyncGroupMembersAction();
|
||||
action.groupsConnection = connection;
|
||||
action.gSuiteDomainName = "domain-registry.example";
|
||||
action.response = response;
|
||||
action.retrier = new Retrier(new FakeSleeper(new FakeClock()), 3);
|
||||
action.run();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test_getGroupEmailAddressForContactType_convertsToLowercase() {
|
||||
assertThat(getGroupEmailAddressForContactType(
|
||||
"SomeRegistrar",
|
||||
RegistrarContact.Type.ADMIN,
|
||||
"domain-registry.example"))
|
||||
.isEqualTo("someregistrar-primary-contacts@domain-registry.example");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test_getGroupEmailAddressForContactType_convertsNonAlphanumericChars() {
|
||||
assertThat(getGroupEmailAddressForContactType(
|
||||
"Weird.ಠ_ಠRegistrar",
|
||||
MARKETING,
|
||||
"domain-registry.example"))
|
||||
.isEqualTo("weirdregistrar-marketing-contacts@domain-registry.example");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test_doPost_noneModified() {
|
||||
persistResource(
|
||||
loadRegistrar("NewRegistrar").asBuilder().setContactsRequireSyncing(false).build());
|
||||
persistResource(
|
||||
loadRegistrar("TheRegistrar").asBuilder().setContactsRequireSyncing(false).build());
|
||||
runAction();
|
||||
verify(response).setStatus(SC_OK);
|
||||
verify(response).setPayload("NOT_MODIFIED No registrar contacts have been updated "
|
||||
+ "since the last time servlet ran.\n");
|
||||
assertThat(loadRegistrar("NewRegistrar").getContactsRequireSyncing()).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test_doPost_syncsNewContact() throws Exception {
|
||||
runAction();
|
||||
verify(connection).addMemberToGroup(
|
||||
"newregistrar-primary-contacts@domain-registry.example",
|
||||
"janedoe@theregistrar.com",
|
||||
Role.MEMBER);
|
||||
verify(response).setStatus(SC_OK);
|
||||
verify(response).setPayload("OK Group memberships successfully updated.\n");
|
||||
assertThat(loadRegistrar("NewRegistrar").getContactsRequireSyncing()).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test_doPost_removesOldContact() throws Exception {
|
||||
when(connection.getMembersOfGroup("newregistrar-primary-contacts@domain-registry.example"))
|
||||
.thenReturn(ImmutableSet.of("defunct@example.com", "janedoe@theregistrar.com"));
|
||||
runAction();
|
||||
verify(connection).removeMemberFromGroup(
|
||||
"newregistrar-primary-contacts@domain-registry.example", "defunct@example.com");
|
||||
verify(response).setStatus(SC_OK);
|
||||
assertThat(loadRegistrar("NewRegistrar").getContactsRequireSyncing()).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test_doPost_removesAllContactsFromGroup() throws Exception {
|
||||
when(connection.getMembersOfGroup("newregistrar-primary-contacts@domain-registry.example"))
|
||||
.thenReturn(ImmutableSet.of("defunct@example.com", "janedoe@theregistrar.com"));
|
||||
ofy().deleteWithoutBackup()
|
||||
.entities(loadRegistrar("NewRegistrar").getContacts())
|
||||
.now();
|
||||
runAction();
|
||||
verify(connection).removeMemberFromGroup(
|
||||
"newregistrar-primary-contacts@domain-registry.example", "defunct@example.com");
|
||||
verify(connection).removeMemberFromGroup(
|
||||
"newregistrar-primary-contacts@domain-registry.example", "janedoe@theregistrar.com");
|
||||
verify(response).setStatus(SC_OK);
|
||||
assertThat(loadRegistrar("NewRegistrar").getContactsRequireSyncing()).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test_doPost_addsAndRemovesContacts_acrossMultipleRegistrars() throws Exception {
|
||||
when(connection.getMembersOfGroup("newregistrar-primary-contacts@domain-registry.example"))
|
||||
.thenReturn(ImmutableSet.of("defunct@example.com", "janedoe@theregistrar.com"));
|
||||
when(connection.getMembersOfGroup("newregistrar-marketing-contacts@domain-registry.example"))
|
||||
.thenReturn(ImmutableSet.of());
|
||||
when(connection.getMembersOfGroup("theregistrar-technical-contacts@domain-registry.example"))
|
||||
.thenReturn(ImmutableSet.of());
|
||||
when(connection.getMembersOfGroup("theregistrar-primary-contacts@domain-registry.example"))
|
||||
.thenReturn(ImmutableSet.of());
|
||||
persistResource(
|
||||
new RegistrarContact.Builder()
|
||||
.setParent(loadRegistrar("NewRegistrar"))
|
||||
.setName("Binary Star")
|
||||
.setEmailAddress("binarystar@example.tld")
|
||||
.setTypes(ImmutableSet.of(ADMIN, MARKETING))
|
||||
.build());
|
||||
persistResource(
|
||||
new RegistrarContact.Builder()
|
||||
.setParent(loadRegistrar("TheRegistrar"))
|
||||
.setName("Hexadecimal")
|
||||
.setEmailAddress("hexadecimal@snow.fall")
|
||||
.setTypes(ImmutableSet.of(TECH))
|
||||
.build());
|
||||
runAction();
|
||||
verify(connection).removeMemberFromGroup(
|
||||
"newregistrar-primary-contacts@domain-registry.example", "defunct@example.com");
|
||||
verify(connection).addMemberToGroup(
|
||||
"newregistrar-primary-contacts@domain-registry.example",
|
||||
"binarystar@example.tld",
|
||||
Role.MEMBER);
|
||||
verify(connection).addMemberToGroup(
|
||||
"newregistrar-marketing-contacts@domain-registry.example",
|
||||
"binarystar@example.tld",
|
||||
Role.MEMBER);
|
||||
verify(connection).addMemberToGroup(
|
||||
"theregistrar-primary-contacts@domain-registry.example",
|
||||
"johndoe@theregistrar.com",
|
||||
Role.MEMBER);
|
||||
verify(connection).addMemberToGroup(
|
||||
"theregistrar-technical-contacts@domain-registry.example",
|
||||
"hexadecimal@snow.fall",
|
||||
Role.MEMBER);
|
||||
verify(response).setStatus(SC_OK);
|
||||
assertThat(Iterables.filter(Registrar.loadAll(), Registrar::getContactsRequireSyncing))
|
||||
.isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test_doPost_gracefullyHandlesExceptionForSingleRegistrar() throws Exception {
|
||||
when(connection.getMembersOfGroup("newregistrar-primary-contacts@domain-registry.example"))
|
||||
.thenReturn(ImmutableSet.of());
|
||||
when(connection.getMembersOfGroup("theregistrar-primary-contacts@domain-registry.example"))
|
||||
.thenThrow(new IOException("Internet was deleted"));
|
||||
runAction();
|
||||
verify(connection).addMemberToGroup(
|
||||
"newregistrar-primary-contacts@domain-registry.example",
|
||||
"janedoe@theregistrar.com",
|
||||
Role.MEMBER);
|
||||
verify(connection, times(3))
|
||||
.getMembersOfGroup("theregistrar-primary-contacts@domain-registry.example");
|
||||
verify(response).setStatus(SC_INTERNAL_SERVER_ERROR);
|
||||
verify(response).setPayload("FAILED Error occurred while updating registrar contacts.\n");
|
||||
assertThat(loadRegistrar("NewRegistrar").getContactsRequireSyncing()).isFalse();
|
||||
assertThat(loadRegistrar("TheRegistrar").getContactsRequireSyncing()).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test_doPost_retriesOnTransientException() throws Exception {
|
||||
doThrow(IOException.class)
|
||||
.doNothing()
|
||||
.when(connection)
|
||||
.addMemberToGroup(anyString(), anyString(), any(Role.class));
|
||||
runAction();
|
||||
verify(connection, times(2)).addMemberToGroup(
|
||||
"newregistrar-primary-contacts@domain-registry.example",
|
||||
"janedoe@theregistrar.com",
|
||||
Role.MEMBER);
|
||||
verify(response).setStatus(SC_OK);
|
||||
verify(response).setPayload("OK Group memberships successfully updated.\n");
|
||||
assertThat(loadRegistrar("NewRegistrar").getContactsRequireSyncing()).isFalse();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,140 @@
|
||||
// Copyright 2017 The Nomulus Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package google.registry.export;
|
||||
|
||||
import static com.google.appengine.api.taskqueue.QueueFactory.getQueue;
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
import static google.registry.export.UpdateSnapshotViewAction.QUEUE;
|
||||
import static google.registry.export.UpdateSnapshotViewAction.UPDATE_SNAPSHOT_DATASET_ID_PARAM;
|
||||
import static google.registry.export.UpdateSnapshotViewAction.UPDATE_SNAPSHOT_KIND_PARAM;
|
||||
import static google.registry.export.UpdateSnapshotViewAction.UPDATE_SNAPSHOT_TABLE_ID_PARAM;
|
||||
import static google.registry.export.UpdateSnapshotViewAction.UPDATE_SNAPSHOT_VIEWNAME_PARAM;
|
||||
import static google.registry.export.UpdateSnapshotViewAction.createViewUpdateTask;
|
||||
import static google.registry.testing.JUnitBackports.assertThrows;
|
||||
import static google.registry.testing.TaskQueueHelper.assertTasksEnqueued;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyString;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.inOrder;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import com.google.api.services.bigquery.Bigquery;
|
||||
import com.google.api.services.bigquery.model.Dataset;
|
||||
import com.google.api.services.bigquery.model.Table;
|
||||
import com.google.common.collect.Iterables;
|
||||
import google.registry.bigquery.CheckedBigquery;
|
||||
import google.registry.request.HttpException.InternalServerErrorException;
|
||||
import google.registry.testing.AppEngineRule;
|
||||
import google.registry.testing.TaskQueueHelper.TaskMatcher;
|
||||
import java.io.IOException;
|
||||
import org.junit.Before;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.junit.runners.JUnit4;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.mockito.InOrder;
|
||||
|
||||
/** Unit tests for {@link UpdateSnapshotViewAction}. */
|
||||
@RunWith(JUnit4.class)
|
||||
public class UpdateSnapshotViewActionTest {
|
||||
|
||||
@Rule
|
||||
public final AppEngineRule appEngine = AppEngineRule.builder()
|
||||
.withTaskQueue()
|
||||
.build();
|
||||
private final CheckedBigquery checkedBigquery = mock(CheckedBigquery.class);
|
||||
private final Bigquery bigquery = mock(Bigquery.class);
|
||||
private final Bigquery.Datasets bigqueryDatasets = mock(Bigquery.Datasets.class);
|
||||
private final Bigquery.Datasets.Insert bigqueryDatasetsInsert =
|
||||
mock(Bigquery.Datasets.Insert.class);
|
||||
private final Bigquery.Tables bigqueryTables = mock(Bigquery.Tables.class);
|
||||
private final Bigquery.Tables.Update bigqueryTablesUpdate = mock(Bigquery.Tables.Update.class);
|
||||
|
||||
private UpdateSnapshotViewAction action;
|
||||
|
||||
@Before
|
||||
public void before() throws Exception {
|
||||
when(checkedBigquery.ensureDataSetExists(anyString(), anyString())).thenReturn(bigquery);
|
||||
when(bigquery.datasets()).thenReturn(bigqueryDatasets);
|
||||
when(bigqueryDatasets.insert(anyString(), any(Dataset.class)))
|
||||
.thenReturn(bigqueryDatasetsInsert);
|
||||
when(bigquery.tables()).thenReturn(bigqueryTables);
|
||||
when(bigqueryTables.update(anyString(), anyString(), anyString(), any(Table.class)))
|
||||
.thenReturn(bigqueryTablesUpdate);
|
||||
|
||||
action = new UpdateSnapshotViewAction();
|
||||
action.checkedBigquery = checkedBigquery;
|
||||
action.datasetId = "some_dataset";
|
||||
action.kindName = "fookind";
|
||||
action.viewName = "latest_datastore_export";
|
||||
action.projectId = "myproject";
|
||||
action.tableId = "12345_fookind";
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSuccess_createViewUpdateTask() {
|
||||
getQueue(QUEUE)
|
||||
.add(
|
||||
createViewUpdateTask(
|
||||
"some_dataset", "12345_fookind", "fookind", "latest_datastore_export"));
|
||||
assertTasksEnqueued(
|
||||
QUEUE,
|
||||
new TaskMatcher()
|
||||
.url(UpdateSnapshotViewAction.PATH)
|
||||
.method("POST")
|
||||
.param(UPDATE_SNAPSHOT_DATASET_ID_PARAM, "some_dataset")
|
||||
.param(UPDATE_SNAPSHOT_TABLE_ID_PARAM, "12345_fookind")
|
||||
.param(UPDATE_SNAPSHOT_KIND_PARAM, "fookind")
|
||||
.param(UPDATE_SNAPSHOT_VIEWNAME_PARAM, "latest_datastore_export"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSuccess_doPost() throws Exception {
|
||||
action.run();
|
||||
|
||||
InOrder factoryOrder = inOrder(checkedBigquery);
|
||||
// Check that the BigQuery factory was called in such a way that the dataset would be created
|
||||
// if it didn't already exist.
|
||||
factoryOrder
|
||||
.verify(checkedBigquery)
|
||||
.ensureDataSetExists("myproject", "latest_datastore_export");
|
||||
|
||||
// Check that we updated both views
|
||||
InOrder tableOrder = inOrder(bigqueryTables);
|
||||
ArgumentCaptor<Table> tableArg = ArgumentCaptor.forClass(Table.class);
|
||||
tableOrder
|
||||
.verify(bigqueryTables)
|
||||
.update(eq("myproject"), eq("latest_datastore_export"), eq("fookind"), tableArg.capture());
|
||||
Iterable<String> actualQueries =
|
||||
Iterables.transform(tableArg.getAllValues(), table -> table.getView().getQuery());
|
||||
assertThat(actualQueries)
|
||||
.containsExactly("#standardSQL\nSELECT * FROM `myproject.some_dataset.12345_fookind`");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testFailure_bigqueryConnectionThrowsError() throws Exception {
|
||||
when(bigqueryTables.update(anyString(), anyString(), anyString(), any(Table.class)))
|
||||
.thenThrow(new IOException("I'm sorry Dave, I can't let you do that"));
|
||||
InternalServerErrorException thrown =
|
||||
assertThrows(InternalServerErrorException.class, action::run);
|
||||
assertThat(thrown)
|
||||
.hasMessageThat()
|
||||
.isEqualTo(
|
||||
"Could not update snapshot view latest_datastore_export for table 12345_fookind");
|
||||
assertThat(thrown).hasCauseThat().hasMessageThat().contains("I'm sorry Dave");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,209 @@
|
||||
// Copyright 2017 The Nomulus Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package google.registry.export;
|
||||
|
||||
import static com.google.common.collect.Iterables.transform;
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
import static google.registry.export.UploadDatastoreBackupAction.BACKUP_DATASET;
|
||||
import static google.registry.export.UploadDatastoreBackupAction.LATEST_BACKUP_VIEW_NAME;
|
||||
import static google.registry.export.UploadDatastoreBackupAction.PATH;
|
||||
import static google.registry.export.UploadDatastoreBackupAction.QUEUE;
|
||||
import static google.registry.export.UploadDatastoreBackupAction.UPLOAD_BACKUP_FOLDER_PARAM;
|
||||
import static google.registry.export.UploadDatastoreBackupAction.UPLOAD_BACKUP_ID_PARAM;
|
||||
import static google.registry.export.UploadDatastoreBackupAction.UPLOAD_BACKUP_KINDS_PARAM;
|
||||
import static google.registry.export.UploadDatastoreBackupAction.enqueueUploadBackupTask;
|
||||
import static google.registry.export.UploadDatastoreBackupAction.getBackupInfoFileForKind;
|
||||
import static google.registry.testing.JUnitBackports.assertThrows;
|
||||
import static google.registry.testing.TaskQueueHelper.assertTasksEnqueued;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.times;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import com.google.api.services.bigquery.Bigquery;
|
||||
import com.google.api.services.bigquery.model.Dataset;
|
||||
import com.google.api.services.bigquery.model.Job;
|
||||
import com.google.api.services.bigquery.model.JobConfigurationLoad;
|
||||
import com.google.api.services.bigquery.model.JobReference;
|
||||
import com.google.appengine.api.taskqueue.QueueFactory;
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
import com.google.common.collect.Iterables;
|
||||
import google.registry.bigquery.CheckedBigquery;
|
||||
import google.registry.export.BigqueryPollJobAction.BigqueryPollJobEnqueuer;
|
||||
import google.registry.request.HttpException.InternalServerErrorException;
|
||||
import google.registry.testing.AppEngineRule;
|
||||
import google.registry.testing.TaskQueueHelper.TaskMatcher;
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
import org.junit.Before;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.junit.runners.JUnit4;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
|
||||
/** Unit tests for {@link UploadDatastoreBackupAction}. */
|
||||
@RunWith(JUnit4.class)
|
||||
public class UploadDatastoreBackupActionTest {
|
||||
|
||||
@Rule
|
||||
public final AppEngineRule appEngine = AppEngineRule.builder()
|
||||
.withTaskQueue()
|
||||
.build();
|
||||
private final CheckedBigquery checkedBigquery = mock(CheckedBigquery.class);
|
||||
private final Bigquery bigquery = mock(Bigquery.class);
|
||||
private final Bigquery.Jobs bigqueryJobs = mock(Bigquery.Jobs.class);
|
||||
private final Bigquery.Jobs.Insert bigqueryJobsInsert = mock(Bigquery.Jobs.Insert.class);
|
||||
private final Bigquery.Datasets bigqueryDatasets = mock(Bigquery.Datasets.class);
|
||||
private final Bigquery.Datasets.Insert bigqueryDatasetsInsert =
|
||||
mock(Bigquery.Datasets.Insert.class);
|
||||
private final BigqueryPollJobEnqueuer bigqueryPollEnqueuer = mock(BigqueryPollJobEnqueuer.class);
|
||||
private UploadDatastoreBackupAction action;
|
||||
|
||||
@Before
|
||||
public void before() throws Exception {
|
||||
when(checkedBigquery.ensureDataSetExists("Project-Id", BACKUP_DATASET)).thenReturn(bigquery);
|
||||
when(bigquery.jobs()).thenReturn(bigqueryJobs);
|
||||
when(bigqueryJobs.insert(eq("Project-Id"), any(Job.class))).thenReturn(bigqueryJobsInsert);
|
||||
when(bigquery.datasets()).thenReturn(bigqueryDatasets);
|
||||
when(bigqueryDatasets.insert(eq("Project-Id"), any(Dataset.class)))
|
||||
.thenReturn(bigqueryDatasetsInsert);
|
||||
action = new UploadDatastoreBackupAction();
|
||||
action.checkedBigquery = checkedBigquery;
|
||||
action.bigqueryPollEnqueuer = bigqueryPollEnqueuer;
|
||||
action.projectId = "Project-Id";
|
||||
action.backupFolderUrl = "gs://bucket/path";
|
||||
action.backupId = "2018-12-05T17:46:39_92612";
|
||||
action.backupKinds = "one,two,three";
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSuccess_enqueueLoadTask() {
|
||||
enqueueUploadBackupTask("id12345", "gs://bucket/path", ImmutableSet.of("one", "two", "three"));
|
||||
assertTasksEnqueued(
|
||||
QUEUE,
|
||||
new TaskMatcher()
|
||||
.url(PATH)
|
||||
.method("POST")
|
||||
.param(UPLOAD_BACKUP_ID_PARAM, "id12345")
|
||||
.param(UPLOAD_BACKUP_FOLDER_PARAM, "gs://bucket/path")
|
||||
.param(UPLOAD_BACKUP_KINDS_PARAM, "one,two,three"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSuccess_doPost() throws Exception {
|
||||
action.run();
|
||||
|
||||
// Verify that checkedBigquery was called in a way that would create the dataset if it didn't
|
||||
// already exist.
|
||||
verify(checkedBigquery).ensureDataSetExists("Project-Id", BACKUP_DATASET);
|
||||
|
||||
// Capture the load jobs we inserted to do additional checking on them.
|
||||
ArgumentCaptor<Job> jobArgument = ArgumentCaptor.forClass(Job.class);
|
||||
verify(bigqueryJobs, times(3)).insert(eq("Project-Id"), jobArgument.capture());
|
||||
List<Job> jobs = jobArgument.getAllValues();
|
||||
assertThat(jobs).hasSize(3);
|
||||
|
||||
// Check properties that should be common to all load jobs.
|
||||
for (Job job : jobs) {
|
||||
assertThat(job.getJobReference().getProjectId()).isEqualTo("Project-Id");
|
||||
JobConfigurationLoad config = job.getConfiguration().getLoad();
|
||||
assertThat(config.getSourceFormat()).isEqualTo("DATASTORE_BACKUP");
|
||||
assertThat(config.getDestinationTable().getProjectId()).isEqualTo("Project-Id");
|
||||
assertThat(config.getDestinationTable().getDatasetId()).isEqualTo(BACKUP_DATASET);
|
||||
}
|
||||
|
||||
// Check the job IDs for each load job.
|
||||
assertThat(transform(jobs, job -> job.getJobReference().getJobId()))
|
||||
.containsExactly(
|
||||
"load-backup-2018_12_05T17_46_39_92612-one",
|
||||
"load-backup-2018_12_05T17_46_39_92612-two",
|
||||
"load-backup-2018_12_05T17_46_39_92612-three");
|
||||
|
||||
// Check the source URI for each load job.
|
||||
assertThat(
|
||||
transform(
|
||||
jobs,
|
||||
job -> Iterables.getOnlyElement(job.getConfiguration().getLoad().getSourceUris())))
|
||||
.containsExactly(
|
||||
"gs://bucket/path/all_namespaces/kind_one/all_namespaces_kind_one.export_metadata",
|
||||
"gs://bucket/path/all_namespaces/kind_two/all_namespaces_kind_two.export_metadata",
|
||||
"gs://bucket/path/all_namespaces/kind_three/all_namespaces_kind_three.export_metadata");
|
||||
|
||||
// Check the destination table ID for each load job.
|
||||
assertThat(
|
||||
transform(
|
||||
jobs, job -> job.getConfiguration().getLoad().getDestinationTable().getTableId()))
|
||||
.containsExactly(
|
||||
"2018_12_05T17_46_39_92612_one",
|
||||
"2018_12_05T17_46_39_92612_two",
|
||||
"2018_12_05T17_46_39_92612_three");
|
||||
|
||||
// Check that we executed the inserted jobs.
|
||||
verify(bigqueryJobsInsert, times(3)).execute();
|
||||
|
||||
// Check that the poll tasks for each load job were enqueued.
|
||||
verify(bigqueryPollEnqueuer)
|
||||
.enqueuePollTask(
|
||||
new JobReference()
|
||||
.setProjectId("Project-Id")
|
||||
.setJobId("load-backup-2018_12_05T17_46_39_92612-one"),
|
||||
UpdateSnapshotViewAction.createViewUpdateTask(
|
||||
BACKUP_DATASET, "2018_12_05T17_46_39_92612_one", "one", LATEST_BACKUP_VIEW_NAME),
|
||||
QueueFactory.getQueue(UpdateSnapshotViewAction.QUEUE));
|
||||
verify(bigqueryPollEnqueuer)
|
||||
.enqueuePollTask(
|
||||
new JobReference()
|
||||
.setProjectId("Project-Id")
|
||||
.setJobId("load-backup-2018_12_05T17_46_39_92612-two"),
|
||||
UpdateSnapshotViewAction.createViewUpdateTask(
|
||||
BACKUP_DATASET, "2018_12_05T17_46_39_92612_two", "two", LATEST_BACKUP_VIEW_NAME),
|
||||
QueueFactory.getQueue(UpdateSnapshotViewAction.QUEUE));
|
||||
verify(bigqueryPollEnqueuer)
|
||||
.enqueuePollTask(
|
||||
new JobReference()
|
||||
.setProjectId("Project-Id")
|
||||
.setJobId("load-backup-2018_12_05T17_46_39_92612-three"),
|
||||
UpdateSnapshotViewAction.createViewUpdateTask(
|
||||
BACKUP_DATASET,
|
||||
"2018_12_05T17_46_39_92612_three",
|
||||
"three",
|
||||
LATEST_BACKUP_VIEW_NAME),
|
||||
QueueFactory.getQueue(UpdateSnapshotViewAction.QUEUE));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testFailure_doPost_bigqueryThrowsException() throws Exception {
|
||||
when(bigqueryJobsInsert.execute()).thenThrow(new IOException("The Internet has gone missing"));
|
||||
InternalServerErrorException thrown =
|
||||
assertThrows(InternalServerErrorException.class, action::run);
|
||||
assertThat(thrown)
|
||||
.hasMessageThat()
|
||||
.contains("Error loading backup: The Internet has gone missing");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testgetBackupInfoFileForKind() {
|
||||
assertThat(
|
||||
getBackupInfoFileForKind(
|
||||
"gs://BucketName/2018-11-11T00:00:00_12345", "AllocationToken"))
|
||||
.isEqualTo(
|
||||
"gs://BucketName/2018-11-11T00:00:00_12345/"
|
||||
+ "all_namespaces/kind_AllocationToken/"
|
||||
+ "all_namespaces_kind_AllocationToken.export_metadata");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
AllocationToken
|
||||
Cancellation
|
||||
ContactResource
|
||||
Cursor
|
||||
DomainBase
|
||||
EntityGroupRoot
|
||||
EppResourceIndex
|
||||
ForeignKeyContactIndex
|
||||
ForeignKeyDomainIndex
|
||||
ForeignKeyHostIndex
|
||||
HistoryEntry
|
||||
HostResource
|
||||
KmsSecret
|
||||
KmsSecretRevision
|
||||
Modification
|
||||
OneTime
|
||||
PollMessage
|
||||
PremiumList
|
||||
PremiumListEntry
|
||||
PremiumListRevision
|
||||
RdeRevision
|
||||
Recurring
|
||||
Registrar
|
||||
RegistrarContact
|
||||
Registry
|
||||
ReservedList
|
||||
@@ -0,0 +1,34 @@
|
||||
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 = "datastore",
|
||||
srcs = glob(["*.java"]),
|
||||
resources = glob(["**/testdata/*.json"]),
|
||||
deps = [
|
||||
"//java/google/registry/export/datastore",
|
||||
"//java/google/registry/util",
|
||||
"//javatests/google/registry/testing",
|
||||
"@com_google_api_client",
|
||||
"@com_google_guava",
|
||||
"@com_google_http_client",
|
||||
"@com_google_http_client_jackson2",
|
||||
"@com_google_truth",
|
||||
"@com_google_truth_extensions_truth_java8_extension",
|
||||
"@joda_time",
|
||||
"@junit",
|
||||
"@org_mockito_core",
|
||||
],
|
||||
)
|
||||
|
||||
GenTestRules(
|
||||
name = "GeneratedTestRules",
|
||||
test_files = glob(["*Test.java"]),
|
||||
deps = [":datastore"],
|
||||
)
|
||||
@@ -0,0 +1,179 @@
|
||||
// Copyright 2018 The Nomulus Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package google.registry.export.datastore;
|
||||
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
import static com.google.common.truth.Truth8.assertThat;
|
||||
|
||||
import com.google.api.client.googleapis.auth.oauth2.GoogleCredential;
|
||||
import com.google.api.client.http.GenericUrl;
|
||||
import com.google.api.client.http.HttpRequest;
|
||||
import com.google.api.client.http.HttpTransport;
|
||||
import com.google.api.client.http.javanet.NetHttpTransport;
|
||||
import com.google.api.client.json.jackson2.JacksonFactory;
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import google.registry.testing.TestDataHelper;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Optional;
|
||||
import org.junit.Before;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.junit.runners.JUnit4;
|
||||
import org.mockito.junit.MockitoJUnit;
|
||||
import org.mockito.junit.MockitoRule;
|
||||
|
||||
/** Unit tests for {@link DatastoreAdmin}. */
|
||||
@RunWith(JUnit4.class)
|
||||
public class DatastoreAdminTest {
|
||||
|
||||
private static final String AUTH_HEADER_PREFIX = "Bearer ";
|
||||
private static final String ACCESS_TOKEN = "MyAccessToken";
|
||||
private static final ImmutableList<String> KINDS =
|
||||
ImmutableList.of("Registry", "Registrar", "DomainBase");
|
||||
|
||||
@Rule public final MockitoRule mocks = MockitoJUnit.rule();
|
||||
|
||||
private HttpTransport httpTransport;
|
||||
private GoogleCredential googleCredential;
|
||||
private DatastoreAdmin datastoreAdmin;
|
||||
|
||||
@Before
|
||||
public void setup() {
|
||||
httpTransport = new NetHttpTransport();
|
||||
googleCredential =
|
||||
new GoogleCredential.Builder()
|
||||
.setTransport(httpTransport)
|
||||
.setJsonFactory(JacksonFactory.getDefaultInstance())
|
||||
.setClock(() -> 0)
|
||||
.build();
|
||||
googleCredential.setAccessToken(ACCESS_TOKEN);
|
||||
googleCredential.setExpiresInSeconds(1_000L);
|
||||
|
||||
datastoreAdmin =
|
||||
new DatastoreAdmin.Builder(
|
||||
googleCredential.getTransport(),
|
||||
googleCredential.getJsonFactory(),
|
||||
googleCredential)
|
||||
.setApplicationName("MyApplication")
|
||||
.setProjectId("MyCloudProject")
|
||||
.build();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testExport() throws IOException {
|
||||
DatastoreAdmin.Export export = datastoreAdmin.export("gs://mybucket/path", KINDS);
|
||||
HttpRequest httpRequest = export.buildHttpRequest();
|
||||
assertThat(httpRequest.getUrl().toString())
|
||||
.isEqualTo("https://datastore.googleapis.com/v1/projects/MyCloudProject:export");
|
||||
assertThat(httpRequest.getRequestMethod()).isEqualTo("POST");
|
||||
|
||||
assertThat(getRequestContent(httpRequest))
|
||||
.hasValue(
|
||||
TestDataHelper.loadFile(getClass(), "export_request_content.json")
|
||||
.replaceAll("[\\s\\n]+", ""));
|
||||
|
||||
simulateSendRequest(httpRequest);
|
||||
assertThat(getAccessToken(httpRequest)).hasValue(ACCESS_TOKEN);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetOperation() throws IOException {
|
||||
DatastoreAdmin.Get get =
|
||||
datastoreAdmin.get("projects/MyCloudProject/operations/ASAzNjMwOTEyNjUJ");
|
||||
HttpRequest httpRequest = get.buildHttpRequest();
|
||||
assertThat(httpRequest.getUrl().toString())
|
||||
.isEqualTo(
|
||||
"https://datastore.googleapis.com/v1/projects/MyCloudProject/operations/ASAzNjMwOTEyNjUJ");
|
||||
assertThat(httpRequest.getRequestMethod()).isEqualTo("GET");
|
||||
assertThat(httpRequest.getContent()).isNull();
|
||||
|
||||
simulateSendRequest(httpRequest);
|
||||
assertThat(getAccessToken(httpRequest)).hasValue(ACCESS_TOKEN);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testListOperations_all() throws IOException {
|
||||
DatastoreAdmin.ListOperations listOperations = datastoreAdmin.listAll();
|
||||
HttpRequest httpRequest = listOperations.buildHttpRequest();
|
||||
assertThat(httpRequest.getUrl().toString())
|
||||
.isEqualTo("https://datastore.googleapis.com/v1/projects/MyCloudProject/operations");
|
||||
assertThat(httpRequest.getRequestMethod()).isEqualTo("GET");
|
||||
assertThat(httpRequest.getContent()).isNull();
|
||||
|
||||
simulateSendRequest(httpRequest);
|
||||
assertThat(getAccessToken(httpRequest)).hasValue(ACCESS_TOKEN);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testListOperations_filterByStartTime() throws IOException {
|
||||
DatastoreAdmin.ListOperations listOperations =
|
||||
datastoreAdmin.list("metadata.common.startTime>\"2018-10-31T00:00:00.0Z\"");
|
||||
HttpRequest httpRequest = listOperations.buildHttpRequest();
|
||||
assertThat(httpRequest.getUrl().toString())
|
||||
.isEqualTo(
|
||||
"https://datastore.googleapis.com/v1/projects/MyCloudProject/operations"
|
||||
+ "?filter=metadata.common.startTime%3E%222018-10-31T00:00:00.0Z%22");
|
||||
assertThat(httpRequest.getRequestMethod()).isEqualTo("GET");
|
||||
assertThat(httpRequest.getContent()).isNull();
|
||||
|
||||
simulateSendRequest(httpRequest);
|
||||
assertThat(getAccessToken(httpRequest)).hasValue(ACCESS_TOKEN);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testListOperations_filterByState() throws IOException {
|
||||
// TODO(weiminyu): consider adding a method to DatastoreAdmin to support query by state.
|
||||
DatastoreAdmin.ListOperations listOperations =
|
||||
datastoreAdmin.list("metadata.common.state=PROCESSING");
|
||||
HttpRequest httpRequest = listOperations.buildHttpRequest();
|
||||
assertThat(httpRequest.getUrl().toString())
|
||||
.isEqualTo(
|
||||
"https://datastore.googleapis.com/v1/projects/MyCloudProject/operations"
|
||||
+ "?filter=metadata.common.state%3DPROCESSING");
|
||||
assertThat(httpRequest.getRequestMethod()).isEqualTo("GET");
|
||||
assertThat(httpRequest.getContent()).isNull();
|
||||
|
||||
simulateSendRequest(httpRequest);
|
||||
assertThat(getAccessToken(httpRequest)).hasValue(ACCESS_TOKEN);
|
||||
}
|
||||
|
||||
private static HttpRequest simulateSendRequest(HttpRequest httpRequest) {
|
||||
try {
|
||||
httpRequest.setUrl(new GenericUrl("https://localhost:65537")).execute();
|
||||
} catch (Exception expected) {
|
||||
}
|
||||
return httpRequest;
|
||||
}
|
||||
|
||||
private static Optional<String> getAccessToken(HttpRequest httpRequest) {
|
||||
return httpRequest.getHeaders().getAuthorizationAsList().stream()
|
||||
.filter(header -> header.startsWith(AUTH_HEADER_PREFIX))
|
||||
.map(header -> header.substring(AUTH_HEADER_PREFIX.length()))
|
||||
.findAny();
|
||||
}
|
||||
|
||||
private static Optional<String> getRequestContent(HttpRequest httpRequest) throws IOException {
|
||||
if (httpRequest.getContent() == null) {
|
||||
return Optional.empty();
|
||||
}
|
||||
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
|
||||
httpRequest.getContent().writeTo(outputStream);
|
||||
outputStream.close();
|
||||
return Optional.of(outputStream.toString(StandardCharsets.UTF_8.name()));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
// Copyright 2018 The Nomulus Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package google.registry.export.datastore;
|
||||
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
import static google.registry.testing.JUnitBackports.assertThrows;
|
||||
|
||||
import com.google.api.client.json.JsonFactory;
|
||||
import com.google.api.client.json.jackson2.JacksonFactory;
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import google.registry.testing.TestDataHelper;
|
||||
import java.io.IOException;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.junit.runners.JUnit4;
|
||||
|
||||
/** Unit tests for the instantiation, marshalling and unmarshalling of {@link EntityFilter}. */
|
||||
@RunWith(JUnit4.class)
|
||||
public class EntityFilterTest {
|
||||
|
||||
private static final JsonFactory JSON_FACTORY = JacksonFactory.getDefaultInstance();
|
||||
|
||||
@Test
|
||||
public void testEntityFilter_create_nullKinds() {
|
||||
assertThrows(NullPointerException.class, () -> new EntityFilter(null));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testEntityFilter_marshall() throws IOException {
|
||||
EntityFilter entityFilter =
|
||||
new EntityFilter(ImmutableList.of("Registry", "Registrar", "DomainBase"));
|
||||
assertThat(JSON_FACTORY.toString(entityFilter))
|
||||
.isEqualTo(loadJsonString("entity_filter.json").replaceAll("[\\s\\n]+", ""));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testEntityFilter_unmarshall() throws IOException {
|
||||
EntityFilter entityFilter = loadJson("entity_filter.json", EntityFilter.class);
|
||||
assertThat(entityFilter.getKinds())
|
||||
.containsExactly("Registry", "Registrar", "DomainBase")
|
||||
.inOrder();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testEntityFilter_unmarshall_noKinds() throws IOException {
|
||||
EntityFilter entityFilter = JSON_FACTORY.fromString("{}", EntityFilter.class);
|
||||
assertThat(entityFilter.getKinds()).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testEntityFilter_unmarshall_emptyKinds() throws IOException {
|
||||
EntityFilter entityFilter = JSON_FACTORY.fromString("{ \"kinds\" : [] }", EntityFilter.class);
|
||||
assertThat(entityFilter.getKinds()).isEmpty();
|
||||
}
|
||||
|
||||
private static <T> T loadJson(String fileName, Class<T> type) throws IOException {
|
||||
return JSON_FACTORY.fromString(loadJsonString(fileName), type);
|
||||
}
|
||||
|
||||
private static String loadJsonString(String fileName) {
|
||||
return TestDataHelper.loadFile(EntityFilterTest.class, fileName);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
// Copyright 2018 The Nomulus Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package google.registry.export.datastore;
|
||||
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
import static com.google.common.truth.Truth8.assertThat;
|
||||
|
||||
import com.google.api.client.json.JsonFactory;
|
||||
import com.google.api.client.json.jackson2.JacksonFactory;
|
||||
import google.registry.export.datastore.Operation.CommonMetadata;
|
||||
import google.registry.export.datastore.Operation.Metadata;
|
||||
import google.registry.export.datastore.Operation.Progress;
|
||||
import google.registry.testing.FakeClock;
|
||||
import google.registry.testing.TestDataHelper;
|
||||
import java.io.IOException;
|
||||
import org.joda.time.DateTime;
|
||||
import org.joda.time.Duration;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.junit.runners.JUnit4;
|
||||
|
||||
/** Unit tests for unmarshalling {@link Operation} and its member types. */
|
||||
@RunWith(JUnit4.class)
|
||||
public class OperationTest {
|
||||
private static final JsonFactory JSON_FACTORY = JacksonFactory.getDefaultInstance();
|
||||
|
||||
@Test
|
||||
public void testCommonMetadata_unmarshall() throws IOException {
|
||||
CommonMetadata commonMetadata = loadJson("common_metadata.json", CommonMetadata.class);
|
||||
assertThat(commonMetadata.getState()).isEqualTo("SUCCESSFUL");
|
||||
assertThat(commonMetadata.getOperationType()).isEqualTo("EXPORT_ENTITIES");
|
||||
assertThat(commonMetadata.getStartTime())
|
||||
.isEqualTo(DateTime.parse("2018-10-29T16:01:04.645299Z"));
|
||||
assertThat(commonMetadata.getEndTime()).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testProgress_unmarshall() throws IOException {
|
||||
Progress progress = loadJson("progress.json", Progress.class);
|
||||
assertThat(progress.getWorkCompleted()).isEqualTo(51797);
|
||||
assertThat(progress.getWorkEstimated()).isEqualTo(54513);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testMetadata_unmarshall() throws IOException {
|
||||
Metadata metadata = loadJson("metadata.json", Metadata.class);
|
||||
assertThat(metadata.getCommonMetadata().getOperationType()).isEqualTo("EXPORT_ENTITIES");
|
||||
assertThat(metadata.getCommonMetadata().getState()).isEqualTo("SUCCESSFUL");
|
||||
assertThat(metadata.getCommonMetadata().getStartTime())
|
||||
.isEqualTo(DateTime.parse("2018-10-29T16:01:04.645299Z"));
|
||||
assertThat(metadata.getCommonMetadata().getEndTime())
|
||||
.hasValue(DateTime.parse("2018-10-29T16:02:19.009859Z"));
|
||||
assertThat(metadata.getOutputUrlPrefix())
|
||||
.isEqualTo("gs://domain-registry-alpha-datastore-export-test/2018-10-29T16:01:04_99364");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testOperation_unmarshall() throws IOException {
|
||||
Operation operation = loadJson("operation.json", Operation.class);
|
||||
assertThat(operation.getName())
|
||||
.startsWith("projects/domain-registry-alpha/operations/ASAzNjMwOTEyNjUJ");
|
||||
assertThat(operation.isProcessing()).isTrue();
|
||||
assertThat(operation.isSuccessful()).isFalse();
|
||||
assertThat(operation.isDone()).isFalse();
|
||||
assertThat(operation.getStartTime()).isEqualTo(DateTime.parse("2018-10-29T16:01:04.645299Z"));
|
||||
assertThat(operation.getExportFolderUrl())
|
||||
.isEqualTo("gs://domain-registry-alpha-datastore-export-test/2018-10-29T16:01:04_99364");
|
||||
assertThat(operation.getExportId()).isEqualTo("2018-10-29T16:01:04_99364");
|
||||
assertThat(operation.getKinds()).containsExactly("Registry", "Registrar", "DomainBase");
|
||||
assertThat(operation.toPrettyString())
|
||||
.isEqualTo(
|
||||
TestDataHelper.loadFile(OperationTest.class, "prettyprinted_operation.json").trim());
|
||||
assertThat(operation.getProgress()).isEqualTo("Progress: N/A");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testOperationList_unmarshall() throws IOException {
|
||||
Operation.OperationList operationList =
|
||||
loadJson("operation_list.json", Operation.OperationList.class);
|
||||
assertThat(operationList.toList()).hasSize(2);
|
||||
FakeClock clock = new FakeClock(DateTime.parse("2018-10-29T16:01:04.645299Z"));
|
||||
clock.advanceOneMilli();
|
||||
assertThat(operationList.toList().get(0).getRunningTime(clock)).isEqualTo(Duration.millis(1));
|
||||
assertThat(operationList.toList().get(0).getProgress())
|
||||
.isEqualTo("Progress: [51797/54513 entities]");
|
||||
assertThat(operationList.toList().get(1).getRunningTime(clock))
|
||||
.isEqualTo(Duration.standardMinutes(1));
|
||||
// Work completed may exceed work estimated
|
||||
assertThat(operationList.toList().get(1).getProgress())
|
||||
.isEqualTo("Progress: [96908367/73773755 bytes] [51797/54513 entities]");
|
||||
}
|
||||
|
||||
private static <T> T loadJson(String fileName, Class<T> type) throws IOException {
|
||||
return JSON_FACTORY.fromString(TestDataHelper.loadFile(OperationTest.class, fileName), type);
|
||||
}
|
||||
}
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"startTime": "2018-10-29T16:01:04.645299Z",
|
||||
"operationType": "EXPORT_ENTITIES",
|
||||
"state": "SUCCESSFUL"
|
||||
}
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"kinds": [
|
||||
"Registry",
|
||||
"Registrar",
|
||||
"DomainBase"
|
||||
]
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"entityFilter": {
|
||||
"kinds": ["Registry", "Registrar", "DomainBase"]
|
||||
},
|
||||
"outputUrlPrefix": "gs://mybucket/path"
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"@type": "type.googleapis.com/google.datastore.admin.v1.ExportEntitiesMetadata",
|
||||
"common": {
|
||||
"startTime": "2018-10-29T16:01:04.645299Z",
|
||||
"endTime": "2018-10-29T16:02:19.009859Z",
|
||||
"operationType": "EXPORT_ENTITIES",
|
||||
"state": "SUCCESSFUL"
|
||||
},
|
||||
"progressEntities": {
|
||||
"workCompleted": "51797",
|
||||
"workEstimated": "54513"
|
||||
},
|
||||
"progressBytes": {
|
||||
"workCompleted": "96908367",
|
||||
"workEstimated": "73773755"
|
||||
},
|
||||
"entityFilter": {
|
||||
"kinds": [
|
||||
"Registry",
|
||||
"Registrar",
|
||||
"DomainBase"
|
||||
]
|
||||
},
|
||||
"outputUrlPrefix": "gs://domain-registry-alpha-datastore-export-test/2018-10-29T16:01:04_99364"
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"name": "projects/domain-registry-alpha/operations/ASAzNjMwOTEyNjUJ",
|
||||
"metadata": {
|
||||
"@type": "type.googleapis.com/google.datastore.admin.v1.ExportEntitiesMetadata",
|
||||
"common": {
|
||||
"startTime": "2018-10-29T16:01:04.645299Z",
|
||||
"operationType": "EXPORT_ENTITIES",
|
||||
"state": "PROCESSING"
|
||||
},
|
||||
"entityFilter": {
|
||||
"kinds": [
|
||||
"Registry",
|
||||
"Registrar",
|
||||
"DomainBase"
|
||||
]
|
||||
},
|
||||
"outputUrlPrefix": "gs://domain-registry-alpha-datastore-export-test/2018-10-29T16:01:04_99364"
|
||||
}
|
||||
}
|
||||
+55
@@ -0,0 +1,55 @@
|
||||
{
|
||||
"operations": [
|
||||
{
|
||||
"name": "projects/domain-registry-alpha/operations/ASAzNjMwOTEyNjUJ",
|
||||
"metadata": {
|
||||
"@type": "type.googleapis.com/google.datastore.admin.v1.ExportEntitiesMetadata",
|
||||
"common": {
|
||||
"startTime": "2018-10-29T16:01:04.645299Z",
|
||||
"operationType": "EXPORT_ENTITIES",
|
||||
"state": "PROCESSING"
|
||||
},
|
||||
"progressEntities": {
|
||||
"workCompleted": "51797",
|
||||
"workEstimated": "54513"
|
||||
},
|
||||
"entityFilter": {
|
||||
"kinds": [
|
||||
"Registry",
|
||||
"Registrar",
|
||||
"DomainBase"
|
||||
]
|
||||
},
|
||||
"outputUrlPrefix": "gs://domain-registry-alpha-datastore-export-test/2018-10-29T16:01:04_99364"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "projects/domain-registry-alpha/operations/ASAzNjMwOTEyNjUJ",
|
||||
"metadata": {
|
||||
"@type": "type.googleapis.com/google.datastore.admin.v1.ExportEntitiesMetadata",
|
||||
"common": {
|
||||
"startTime": "2018-10-29T16:01:04.645299Z",
|
||||
"endTime": "2018-10-29T16:02:04.645299Z",
|
||||
"operationType": "EXPORT_ENTITIES",
|
||||
"state": "PROCESSING"
|
||||
},
|
||||
"progressEntities": {
|
||||
"workCompleted": "51797",
|
||||
"workEstimated": "54513"
|
||||
},
|
||||
"progressBytes": {
|
||||
"workCompleted": "96908367",
|
||||
"workEstimated": "73773755"
|
||||
},
|
||||
"entityFilter": {
|
||||
"kinds": [
|
||||
"Registry",
|
||||
"Registrar",
|
||||
"DomainBase"
|
||||
]
|
||||
},
|
||||
"outputUrlPrefix": "gs://domain-registry-alpha-datastore-export-test/2018-10-29T16:01:04_99364"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"done" : false,
|
||||
"metadata" : {
|
||||
"common" : {
|
||||
"operationType" : "EXPORT_ENTITIES",
|
||||
"startTime" : "2018-10-29T16:01:04.645299Z",
|
||||
"state" : "PROCESSING"
|
||||
},
|
||||
"entityFilter" : {
|
||||
"kinds" : [ "Registry", "Registrar", "DomainBase" ]
|
||||
},
|
||||
"outputUrlPrefix" : "gs://domain-registry-alpha-datastore-export-test/2018-10-29T16:01:04_99364",
|
||||
"@type" : "type.googleapis.com/google.datastore.admin.v1.ExportEntitiesMetadata"
|
||||
},
|
||||
"name" : "projects/domain-registry-alpha/operations/ASAzNjMwOTEyNjUJ"
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"workCompleted": "51797",
|
||||
"workEstimated": "54513"
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
AllocationToken
|
||||
Cancellation
|
||||
ContactResource
|
||||
DomainBase
|
||||
EppResourceIndex
|
||||
ForeignKeyContactIndex
|
||||
ForeignKeyDomainIndex
|
||||
ForeignKeyHostIndex
|
||||
HistoryEntry
|
||||
HostResource
|
||||
KmsSecret
|
||||
KmsSecretRevision
|
||||
Modification
|
||||
OneTime
|
||||
PollMessage
|
||||
PremiumList
|
||||
PremiumListEntry
|
||||
PremiumListRevision
|
||||
Recurring
|
||||
Registrar
|
||||
RegistrarContact
|
||||
Registry
|
||||
@@ -0,0 +1,35 @@
|
||||
package(
|
||||
default_testonly = 1,
|
||||
default_visibility = ["//java/google/registry:registry_project"],
|
||||
)
|
||||
|
||||
licenses(["notice"]) # Apache 2.0
|
||||
|
||||
java_library(
|
||||
name = "sheet",
|
||||
srcs = glob(["*.java"]),
|
||||
deps = [
|
||||
"//java/google/registry/config",
|
||||
"//java/google/registry/export/sheet",
|
||||
"//java/google/registry/model",
|
||||
"//javatests/google/registry/testing",
|
||||
"//third_party/objectify:objectify-v4_1",
|
||||
"@com_google_apis_google_api_services_sheets",
|
||||
"@com_google_code_findbugs_jsr305",
|
||||
"@com_google_guava",
|
||||
"@com_google_truth",
|
||||
"@com_google_truth_extensions_truth_java8_extension",
|
||||
"@joda_time",
|
||||
"@junit",
|
||||
"@org_joda_money",
|
||||
"@org_mockito_core",
|
||||
],
|
||||
)
|
||||
|
||||
load("//java/com/google/testing/builddefs:GenTestRules.bzl", "GenTestRules")
|
||||
|
||||
GenTestRules(
|
||||
name = "GeneratedTestRules",
|
||||
test_files = glob(["*Test.java"]),
|
||||
deps = [":sheet"],
|
||||
)
|
||||
@@ -0,0 +1,193 @@
|
||||
// Copyright 2017 The Nomulus Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package google.registry.export.sheet;
|
||||
|
||||
import static com.google.common.collect.Lists.newArrayList;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.verifyZeroInteractions;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import com.google.api.services.sheets.v4.Sheets;
|
||||
import com.google.api.services.sheets.v4.model.AppendValuesResponse;
|
||||
import com.google.api.services.sheets.v4.model.BatchUpdateValuesRequest;
|
||||
import com.google.api.services.sheets.v4.model.BatchUpdateValuesResponse;
|
||||
import com.google.api.services.sheets.v4.model.ClearValuesRequest;
|
||||
import com.google.api.services.sheets.v4.model.ClearValuesResponse;
|
||||
import com.google.api.services.sheets.v4.model.ValueRange;
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.google.common.collect.ImmutableMap;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.junit.runners.JUnit4;
|
||||
|
||||
/** Unit tests for {@link SheetSynchronizer}. */
|
||||
@RunWith(JUnit4.class)
|
||||
public class SheetSynchronizerTest {
|
||||
private final SheetSynchronizer sheetSynchronizer = new SheetSynchronizer();
|
||||
private final Sheets sheetsService = mock(Sheets.class);
|
||||
private final Sheets.Spreadsheets spreadsheets = mock(Sheets.Spreadsheets.class);
|
||||
private final Sheets.Spreadsheets.Values values = mock(Sheets.Spreadsheets.Values.class);
|
||||
private final Sheets.Spreadsheets.Values.Get getReq = mock(Sheets.Spreadsheets.Values.Get.class);
|
||||
private final Sheets.Spreadsheets.Values.Append appendReq =
|
||||
mock(Sheets.Spreadsheets.Values.Append.class);
|
||||
private final Sheets.Spreadsheets.Values.BatchUpdate updateReq =
|
||||
mock(Sheets.Spreadsheets.Values.BatchUpdate.class);
|
||||
private final Sheets.Spreadsheets.Values.Clear clearReq =
|
||||
mock(Sheets.Spreadsheets.Values.Clear.class);
|
||||
|
||||
private List<List<Object>> existingSheet;
|
||||
private ImmutableList<ImmutableMap<String, String>> data;
|
||||
|
||||
@Before
|
||||
public void before() throws Exception {
|
||||
sheetSynchronizer.sheetsService = sheetsService;
|
||||
when(sheetsService.spreadsheets()).thenReturn(spreadsheets);
|
||||
when(spreadsheets.values()).thenReturn(values);
|
||||
|
||||
when(values.get(any(String.class), any(String.class))).thenReturn(getReq);
|
||||
when(values.append(any(String.class), any(String.class), any(ValueRange.class)))
|
||||
.thenReturn(appendReq);
|
||||
when(values.clear(any(String.class), any(String.class), any(ClearValuesRequest.class)))
|
||||
.thenReturn(clearReq);
|
||||
when(values.batchUpdate(any(String.class), any(BatchUpdateValuesRequest.class)))
|
||||
.thenReturn(updateReq);
|
||||
|
||||
when(appendReq.execute()).thenReturn(new AppendValuesResponse());
|
||||
when(appendReq.setValueInputOption(any(String.class))).thenReturn(appendReq);
|
||||
when(appendReq.setInsertDataOption(any(String.class))).thenReturn(appendReq);
|
||||
when(clearReq.execute()).thenReturn(new ClearValuesResponse());
|
||||
when(updateReq.execute()).thenReturn(new BatchUpdateValuesResponse());
|
||||
|
||||
existingSheet = newArrayList();
|
||||
data = ImmutableList.of();
|
||||
ValueRange valueRange = new ValueRange().setValues(existingSheet);
|
||||
when(getReq.execute()).thenReturn(valueRange);
|
||||
}
|
||||
|
||||
// Explicitly constructs a List<Object> to avoid newArrayList typing to ArrayList<String>
|
||||
private List<Object> createRow(Object... elements) {
|
||||
return new ArrayList<>(Arrays.asList(elements));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSynchronize_dataAndSheetEmpty_doNothing() throws Exception {
|
||||
existingSheet.add(createRow("a", "b"));
|
||||
sheetSynchronizer.synchronize("aSheetId", data);
|
||||
verifyZeroInteractions(appendReq);
|
||||
verifyZeroInteractions(clearReq);
|
||||
verifyZeroInteractions(updateReq);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSynchronize_differentValues_updatesValues() throws Exception {
|
||||
existingSheet.add(createRow("a", "b"));
|
||||
existingSheet.add(createRow("diffVal1l", "diffVal2"));
|
||||
data = ImmutableList.of(ImmutableMap.of("a", "val1", "b", "val2"));
|
||||
sheetSynchronizer.synchronize("aSheetId", data);
|
||||
|
||||
verifyZeroInteractions(appendReq);
|
||||
verifyZeroInteractions(clearReq);
|
||||
|
||||
BatchUpdateValuesRequest expectedRequest = new BatchUpdateValuesRequest();
|
||||
List<List<Object>> expectedVals = newArrayList();
|
||||
expectedVals.add(createRow("val1", "val2"));
|
||||
expectedRequest.setData(
|
||||
newArrayList(new ValueRange().setRange("Registrars!A2").setValues(expectedVals)));
|
||||
expectedRequest.setValueInputOption("RAW");
|
||||
verify(values).batchUpdate("aSheetId", expectedRequest);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSynchronize_unknownFields_doesntUpdate() throws Exception {
|
||||
existingSheet.add(createRow("a", "c", "b"));
|
||||
existingSheet.add(createRow("diffVal1", "sameVal", "diffVal2"));
|
||||
data = ImmutableList.of(ImmutableMap.of("a", "val1", "b", "val2", "d", "val3"));
|
||||
sheetSynchronizer.synchronize("aSheetId", data);
|
||||
|
||||
verifyZeroInteractions(appendReq);
|
||||
verifyZeroInteractions(clearReq);
|
||||
|
||||
BatchUpdateValuesRequest expectedRequest = new BatchUpdateValuesRequest();
|
||||
List<List<Object>> expectedVals = newArrayList();
|
||||
expectedVals.add(createRow("val1", "sameVal", "val2"));
|
||||
expectedRequest.setData(
|
||||
newArrayList(new ValueRange().setRange("Registrars!A2").setValues(expectedVals)));
|
||||
expectedRequest.setValueInputOption("RAW");
|
||||
verify(values).batchUpdate("aSheetId", expectedRequest);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSynchronize_notFullRow_getsPadded() throws Exception {
|
||||
existingSheet.add(createRow("a", "c", "b"));
|
||||
existingSheet.add(createRow("diffVal1", "diffVal2"));
|
||||
data = ImmutableList.of(ImmutableMap.of("a", "val1", "b", "paddedVal", "d", "val3"));
|
||||
sheetSynchronizer.synchronize("aSheetId", data);
|
||||
|
||||
verifyZeroInteractions(appendReq);
|
||||
verifyZeroInteractions(clearReq);
|
||||
|
||||
BatchUpdateValuesRequest expectedRequest = new BatchUpdateValuesRequest();
|
||||
List<List<Object>> expectedVals = newArrayList();
|
||||
expectedVals.add(createRow("val1", "diffVal2", "paddedVal"));
|
||||
expectedRequest.setData(
|
||||
newArrayList(new ValueRange().setRange("Registrars!A2").setValues(expectedVals)));
|
||||
expectedRequest.setValueInputOption("RAW");
|
||||
verify(values).batchUpdate("aSheetId", expectedRequest);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSynchronize_moreData_appendsValues() throws Exception {
|
||||
existingSheet.add(createRow("a", "b"));
|
||||
existingSheet.add(createRow("diffVal1", "diffVal2"));
|
||||
data = ImmutableList.of(
|
||||
ImmutableMap.of("a", "val1", "b", "val2"),
|
||||
ImmutableMap.of("a", "val3", "b", "val4"));
|
||||
sheetSynchronizer.synchronize("aSheetId", data);
|
||||
|
||||
verifyZeroInteractions(clearReq);
|
||||
|
||||
BatchUpdateValuesRequest expectedRequest = new BatchUpdateValuesRequest();
|
||||
List<List<Object>> updatedVals = newArrayList();
|
||||
updatedVals.add(createRow("val1", "val2"));
|
||||
expectedRequest.setData(
|
||||
newArrayList(
|
||||
new ValueRange().setRange("Registrars!A2").setValues(updatedVals)));
|
||||
expectedRequest.setValueInputOption("RAW");
|
||||
verify(values).batchUpdate("aSheetId", expectedRequest);
|
||||
|
||||
List<List<Object>> appendedVals = newArrayList();
|
||||
appendedVals.add(createRow("val3", "val4"));
|
||||
ValueRange appendRequest = new ValueRange().setValues(appendedVals);
|
||||
verify(values).append("aSheetId", "Registrars!A3", appendRequest);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSynchronize_lessData_clearsValues() throws Exception {
|
||||
existingSheet.add(createRow("a", "b"));
|
||||
existingSheet.add(createRow("val1", "val2"));
|
||||
existingSheet.add(createRow("diffVal3", "diffVal4"));
|
||||
data = ImmutableList.of(ImmutableMap.of("a", "val1", "b", "val2"));
|
||||
sheetSynchronizer.synchronize("aSheetId", data);
|
||||
|
||||
verify(values).clear("aSheetId", "Registrars!3:4", new ClearValuesRequest());
|
||||
verifyZeroInteractions(updateReq);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
// Copyright 2017 The Nomulus Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package google.registry.export.sheet;
|
||||
|
||||
import static com.google.common.net.MediaType.PLAIN_TEXT_UTF_8;
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.verifyNoMoreInteractions;
|
||||
import static org.mockito.Mockito.verifyZeroInteractions;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import google.registry.testing.AppEngineRule;
|
||||
import google.registry.testing.FakeLockHandler;
|
||||
import google.registry.testing.FakeResponse;
|
||||
import java.util.Optional;
|
||||
import javax.annotation.Nullable;
|
||||
import org.joda.time.Duration;
|
||||
import org.junit.Before;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.junit.runners.JUnit4;
|
||||
|
||||
/** Unit tests for {@link SyncRegistrarsSheetAction}. */
|
||||
@RunWith(JUnit4.class)
|
||||
public class SyncRegistrarsSheetActionTest {
|
||||
|
||||
@Rule
|
||||
public final AppEngineRule appEngine = AppEngineRule.builder()
|
||||
.withDatastore()
|
||||
.withTaskQueue()
|
||||
.build();
|
||||
|
||||
private final FakeResponse response = new FakeResponse();
|
||||
private final SyncRegistrarsSheet syncRegistrarsSheet = mock(SyncRegistrarsSheet.class);
|
||||
|
||||
private SyncRegistrarsSheetAction action;
|
||||
|
||||
private void runAction(@Nullable String idConfig, @Nullable String idParam) {
|
||||
action.idConfig = Optional.ofNullable(idConfig);
|
||||
action.idParam = Optional.ofNullable(idParam);
|
||||
action.run();
|
||||
}
|
||||
|
||||
@Before
|
||||
public void setUp() {
|
||||
action = new SyncRegistrarsSheetAction();
|
||||
action.response = response;
|
||||
action.syncRegistrarsSheet = syncRegistrarsSheet;
|
||||
action.timeout = Duration.standardHours(1);
|
||||
action.lockHandler = new FakeLockHandler(true);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testPost_withoutParamsOrSystemProperty_dropsTask() {
|
||||
runAction(null, null);
|
||||
assertThat(response.getPayload()).startsWith("MISSINGNO");
|
||||
verifyZeroInteractions(syncRegistrarsSheet);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testPost_withoutParams_runsSyncWithDefaultIdAndChecksIfModified() throws Exception {
|
||||
when(syncRegistrarsSheet.wereRegistrarsModified()).thenReturn(true);
|
||||
runAction("jazz", null);
|
||||
assertThat(response.getStatus()).isEqualTo(200);
|
||||
assertThat(response.getContentType()).isEqualTo(PLAIN_TEXT_UTF_8);
|
||||
assertThat(response.getPayload()).startsWith("OK");
|
||||
verify(syncRegistrarsSheet).wereRegistrarsModified();
|
||||
verify(syncRegistrarsSheet).run(eq("jazz"));
|
||||
verifyNoMoreInteractions(syncRegistrarsSheet);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testPost_noModificationsToRegistrarEntities_doesNothing() {
|
||||
when(syncRegistrarsSheet.wereRegistrarsModified()).thenReturn(false);
|
||||
runAction("NewRegistrar", null);
|
||||
assertThat(response.getPayload()).startsWith("NOTMODIFIED");
|
||||
verify(syncRegistrarsSheet).wereRegistrarsModified();
|
||||
verifyNoMoreInteractions(syncRegistrarsSheet);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testPost_overrideId_runsSyncWithCustomIdAndDoesNotCheckModified() throws Exception {
|
||||
runAction(null, "foobar");
|
||||
assertThat(response.getPayload()).startsWith("OK");
|
||||
verify(syncRegistrarsSheet).run(eq("foobar"));
|
||||
verifyNoMoreInteractions(syncRegistrarsSheet);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testPost_failToAquireLock_servletDoesNothingAndReturns() {
|
||||
action.lockHandler = new FakeLockHandler(false);
|
||||
runAction(null, "foobar");
|
||||
assertThat(response.getPayload()).startsWith("LOCKED");
|
||||
verifyZeroInteractions(syncRegistrarsSheet);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,367 @@
|
||||
// Copyright 2017 The Nomulus Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package google.registry.export.sheet;
|
||||
|
||||
import static com.google.common.collect.Iterables.getOnlyElement;
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
import static google.registry.config.RegistryConfig.getDefaultRegistrarWhoisServer;
|
||||
import static google.registry.model.common.Cursor.CursorType.SYNC_REGISTRAR_SHEET;
|
||||
import static google.registry.model.ofy.ObjectifyService.ofy;
|
||||
import static google.registry.testing.DatastoreHelper.createTld;
|
||||
import static google.registry.testing.DatastoreHelper.persistNewRegistrar;
|
||||
import static google.registry.testing.DatastoreHelper.persistResource;
|
||||
import static google.registry.testing.DatastoreHelper.persistSimpleResources;
|
||||
import static org.joda.money.CurrencyUnit.JPY;
|
||||
import static org.joda.money.CurrencyUnit.USD;
|
||||
import static org.joda.time.DateTimeZone.UTC;
|
||||
import static org.joda.time.Duration.standardMinutes;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.verify;
|
||||
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.google.common.collect.ImmutableMap;
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
import google.registry.model.common.Cursor;
|
||||
import google.registry.model.ofy.Ofy;
|
||||
import google.registry.model.registrar.Registrar;
|
||||
import google.registry.model.registrar.RegistrarAddress;
|
||||
import google.registry.model.registrar.RegistrarContact;
|
||||
import google.registry.testing.AppEngineRule;
|
||||
import google.registry.testing.DatastoreHelper;
|
||||
import google.registry.testing.FakeClock;
|
||||
import google.registry.testing.InjectRule;
|
||||
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 org.mockito.ArgumentCaptor;
|
||||
import org.mockito.Captor;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.MockitoJUnit;
|
||||
import org.mockito.junit.MockitoRule;
|
||||
|
||||
/** Unit tests for {@link SyncRegistrarsSheet}. */
|
||||
@RunWith(JUnit4.class)
|
||||
public class SyncRegistrarsSheetTest {
|
||||
|
||||
@Rule public final AppEngineRule appEngine = AppEngineRule.builder().withDatastore().build();
|
||||
@Rule public final MockitoRule mocks = MockitoJUnit.rule();
|
||||
@Rule public final InjectRule inject = new InjectRule();
|
||||
|
||||
@Captor private ArgumentCaptor<ImmutableList<ImmutableMap<String, String>>> rowsCaptor;
|
||||
@Mock private SheetSynchronizer sheetSynchronizer;
|
||||
|
||||
private final FakeClock clock = new FakeClock(DateTime.now(UTC));
|
||||
|
||||
private SyncRegistrarsSheet newSyncRegistrarsSheet() {
|
||||
SyncRegistrarsSheet result = new SyncRegistrarsSheet();
|
||||
result.clock = clock;
|
||||
result.sheetSynchronizer = sheetSynchronizer;
|
||||
return result;
|
||||
}
|
||||
|
||||
@Before
|
||||
public void before() {
|
||||
inject.setStaticField(Ofy.class, "clock", clock);
|
||||
createTld("example");
|
||||
// Remove Registrar entities created by AppEngineRule.
|
||||
Registrar.loadAll().forEach(DatastoreHelper::deleteResource);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test_wereRegistrarsModified_noRegistrars_returnsFalse() {
|
||||
assertThat(newSyncRegistrarsSheet().wereRegistrarsModified()).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test_wereRegistrarsModified_atDifferentCursorTimes() {
|
||||
persistNewRegistrar("SomeRegistrar", "Some Registrar Inc.", Registrar.Type.REAL, 8L);
|
||||
persistResource(Cursor.createGlobal(SYNC_REGISTRAR_SHEET, clock.nowUtc().minusHours(1)));
|
||||
assertThat(newSyncRegistrarsSheet().wereRegistrarsModified()).isTrue();
|
||||
persistResource(Cursor.createGlobal(SYNC_REGISTRAR_SHEET, clock.nowUtc().plusHours(1)));
|
||||
assertThat(newSyncRegistrarsSheet().wereRegistrarsModified()).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRun() throws Exception {
|
||||
DateTime beforeExecution = clock.nowUtc();
|
||||
persistResource(new Registrar.Builder()
|
||||
.setClientId("anotherregistrar")
|
||||
.setRegistrarName("Another Registrar LLC")
|
||||
.setType(Registrar.Type.REAL)
|
||||
.setIanaIdentifier(1L)
|
||||
.setState(Registrar.State.ACTIVE)
|
||||
.setInternationalizedAddress(new RegistrarAddress.Builder()
|
||||
.setStreet(ImmutableList.of("I will get ignored :'("))
|
||||
.setCity("Williamsburg")
|
||||
.setState("NY")
|
||||
.setZip("11211")
|
||||
.setCountryCode("US")
|
||||
.build())
|
||||
.setLocalizedAddress(new RegistrarAddress.Builder()
|
||||
.setStreet(ImmutableList.of(
|
||||
"123 Main St",
|
||||
"Suite 100"))
|
||||
.setCity("Smalltown")
|
||||
.setState("NY")
|
||||
.setZip("11211")
|
||||
.setCountryCode("US")
|
||||
.build())
|
||||
.setPhoneNumber("+1.2125551212")
|
||||
.setFaxNumber("+1.2125551213")
|
||||
.setEmailAddress("contact-us@example.com")
|
||||
.setWhoisServer("whois.example.com")
|
||||
.setUrl("http://www.example.org/another_registrar")
|
||||
.setIcannReferralEmail("jim@example.net")
|
||||
.build());
|
||||
|
||||
Registrar registrar =
|
||||
new Registrar.Builder()
|
||||
.setClientId("aaaregistrar")
|
||||
.setRegistrarName("AAA Registrar Inc.")
|
||||
.setType(Registrar.Type.REAL)
|
||||
.setIanaIdentifier(8L)
|
||||
.setState(Registrar.State.SUSPENDED)
|
||||
.setPassword("pa$$word")
|
||||
.setEmailAddress("nowhere@example.org")
|
||||
.setInternationalizedAddress(
|
||||
new RegistrarAddress.Builder()
|
||||
.setStreet(
|
||||
ImmutableList.of("I get fallen back upon since there's no l10n addr"))
|
||||
.setCity("Williamsburg")
|
||||
.setState("NY")
|
||||
.setZip("11211")
|
||||
.setCountryCode("US")
|
||||
.build())
|
||||
.setAllowedTlds(ImmutableSet.of("example"))
|
||||
.setPhoneNumber("+1.2223334444")
|
||||
.setUrl("http://www.example.org/aaa_registrar")
|
||||
.setBillingAccountMap(ImmutableMap.of(USD, "USD1234", JPY, "JPY7890"))
|
||||
.build();
|
||||
ImmutableList<RegistrarContact> contacts = ImmutableList.of(
|
||||
new RegistrarContact.Builder()
|
||||
.setParent(registrar)
|
||||
.setName("Jane Doe")
|
||||
.setEmailAddress("contact@example.com")
|
||||
.setPhoneNumber("+1.1234567890")
|
||||
.setTypes(ImmutableSet.of(RegistrarContact.Type.ADMIN, RegistrarContact.Type.BILLING))
|
||||
.build(),
|
||||
new RegistrarContact.Builder()
|
||||
.setParent(registrar)
|
||||
.setName("John Doe")
|
||||
.setEmailAddress("john.doe@example.tld")
|
||||
.setPhoneNumber("+1.1234567890")
|
||||
.setFaxNumber("+1.1234567891")
|
||||
.setTypes(ImmutableSet.of(RegistrarContact.Type.ADMIN))
|
||||
// Purposely flip the internal/external admin/tech
|
||||
// distinction to make sure we're not relying on it. Sigh.
|
||||
.setVisibleInWhoisAsAdmin(false)
|
||||
.setVisibleInWhoisAsTech(true)
|
||||
.setGaeUserId("light")
|
||||
.build(),
|
||||
new RegistrarContact.Builder()
|
||||
.setParent(registrar)
|
||||
.setName("Jane Smith")
|
||||
.setEmailAddress("pride@example.net")
|
||||
.setTypes(ImmutableSet.of(RegistrarContact.Type.TECH))
|
||||
.build());
|
||||
// Use registrar key for contacts' parent.
|
||||
persistSimpleResources(contacts);
|
||||
persistResource(registrar);
|
||||
|
||||
clock.advanceBy(standardMinutes(1));
|
||||
newSyncRegistrarsSheet().run("foobar");
|
||||
|
||||
verify(sheetSynchronizer).synchronize(eq("foobar"), rowsCaptor.capture());
|
||||
ImmutableList<ImmutableMap<String, String>> rows = getOnlyElement(rowsCaptor.getAllValues());
|
||||
assertThat(rows).hasSize(2);
|
||||
|
||||
ImmutableMap<String, String> row = rows.get(0);
|
||||
assertThat(row).containsEntry("clientIdentifier", "aaaregistrar");
|
||||
assertThat(row).containsEntry("registrarName", "AAA Registrar Inc.");
|
||||
assertThat(row).containsEntry("state", "SUSPENDED");
|
||||
assertThat(row).containsEntry("ianaIdentifier", "8");
|
||||
assertThat(row).containsEntry("billingIdentifier", "");
|
||||
assertThat(row)
|
||||
.containsEntry(
|
||||
"primaryContacts",
|
||||
""
|
||||
+ "Jane Doe\n"
|
||||
+ "contact@example.com\n"
|
||||
+ "Tel: +1.1234567890\n"
|
||||
+ "Types: [ADMIN, BILLING]\n"
|
||||
+ "Visible in registrar WHOIS query as Admin contact: No\n"
|
||||
+ "Visible in registrar WHOIS query as Technical contact: No\n"
|
||||
+ "Phone number and email visible in domain WHOIS query as "
|
||||
+ "Registrar Abuse contact info: No\n"
|
||||
+ "Registrar-Console access: No\n"
|
||||
+ "\n"
|
||||
+ "John Doe\n"
|
||||
+ "john.doe@example.tld\n"
|
||||
+ "Tel: +1.1234567890\n"
|
||||
+ "Fax: +1.1234567891\n"
|
||||
+ "Types: [ADMIN]\n"
|
||||
+ "Visible in registrar WHOIS query as Admin contact: No\n"
|
||||
+ "Visible in registrar WHOIS query as Technical contact: Yes\n"
|
||||
+ "Phone number and email visible in domain WHOIS query as "
|
||||
+ "Registrar Abuse contact info: No\n"
|
||||
+ "Registrar-Console access: Yes\n"
|
||||
+ "GAE-UserID: light\n");
|
||||
assertThat(row)
|
||||
.containsEntry(
|
||||
"techContacts",
|
||||
""
|
||||
+ "Jane Smith\n"
|
||||
+ "pride@example.net\n"
|
||||
+ "Types: [TECH]\n"
|
||||
+ "Visible in registrar WHOIS query as Admin contact: No\n"
|
||||
+ "Visible in registrar WHOIS query as Technical contact: No\n"
|
||||
+ "Phone number and email visible in domain WHOIS query as "
|
||||
+ "Registrar Abuse contact info: No\n"
|
||||
+ "Registrar-Console access: No\n");
|
||||
assertThat(row).containsEntry("marketingContacts", "");
|
||||
assertThat(row).containsEntry("abuseContacts", "");
|
||||
assertThat(row).containsEntry("whoisInquiryContacts", "");
|
||||
assertThat(row).containsEntry("legalContacts", "");
|
||||
assertThat(row)
|
||||
.containsEntry(
|
||||
"billingContacts",
|
||||
""
|
||||
+ "Jane Doe\n"
|
||||
+ "contact@example.com\n"
|
||||
+ "Tel: +1.1234567890\n"
|
||||
+ "Types: [ADMIN, BILLING]\n"
|
||||
+ "Visible in registrar WHOIS query as Admin contact: No\n"
|
||||
+ "Visible in registrar WHOIS query as Technical contact: No\n"
|
||||
+ "Phone number and email visible in domain WHOIS query as "
|
||||
+ "Registrar Abuse contact info: No\n"
|
||||
+ "Registrar-Console access: No\n");
|
||||
assertThat(row).containsEntry("contactsMarkedAsWhoisAdmin", "");
|
||||
assertThat(row)
|
||||
.containsEntry(
|
||||
"contactsMarkedAsWhoisTech",
|
||||
""
|
||||
+ "John Doe\n"
|
||||
+ "john.doe@example.tld\n"
|
||||
+ "Tel: +1.1234567890\n"
|
||||
+ "Fax: +1.1234567891\n"
|
||||
+ "Types: [ADMIN]\n"
|
||||
+ "Visible in registrar WHOIS query as Admin contact: No\n"
|
||||
+ "Visible in registrar WHOIS query as Technical contact: Yes\n"
|
||||
+ "Phone number and email visible in domain WHOIS query as "
|
||||
+ "Registrar Abuse contact info: No\n"
|
||||
+ "Registrar-Console access: Yes\n"
|
||||
+ "GAE-UserID: light\n");
|
||||
assertThat(row).containsEntry("emailAddress", "nowhere@example.org");
|
||||
assertThat(row).containsEntry(
|
||||
"address.street", "I get fallen back upon since there's no l10n addr");
|
||||
assertThat(row).containsEntry("address.city", "Williamsburg");
|
||||
assertThat(row).containsEntry("address.state", "NY");
|
||||
assertThat(row).containsEntry("address.zip", "11211");
|
||||
assertThat(row).containsEntry("address.countryCode", "US");
|
||||
assertThat(row).containsEntry("phoneNumber", "+1.2223334444");
|
||||
assertThat(row).containsEntry("faxNumber", "");
|
||||
assertThat(row.get("creationTime")).isEqualTo(beforeExecution.toString());
|
||||
assertThat(row.get("lastUpdateTime")).isEqualTo(beforeExecution.toString());
|
||||
assertThat(row).containsEntry("allowedTlds", "example");
|
||||
assertThat(row).containsEntry("blockPremiumNames", "false");
|
||||
assertThat(row).containsEntry("ipAddressWhitelist", "");
|
||||
assertThat(row).containsEntry("url", "http://www.example.org/aaa_registrar");
|
||||
assertThat(row).containsEntry("icannReferralEmail", "");
|
||||
assertThat(row).containsEntry("whoisServer", getDefaultRegistrarWhoisServer());
|
||||
assertThat(row).containsEntry("referralUrl", "http://www.example.org/aaa_registrar");
|
||||
assertThat(row).containsEntry("billingAccountMap", "{JPY=JPY7890, USD=USD1234}");
|
||||
|
||||
row = rows.get(1);
|
||||
assertThat(row).containsEntry("clientIdentifier", "anotherregistrar");
|
||||
assertThat(row).containsEntry("registrarName", "Another Registrar LLC");
|
||||
assertThat(row).containsEntry("state", "ACTIVE");
|
||||
assertThat(row).containsEntry("ianaIdentifier", "1");
|
||||
assertThat(row).containsEntry("billingIdentifier", "");
|
||||
assertThat(row).containsEntry("primaryContacts", "");
|
||||
assertThat(row).containsEntry("techContacts", "");
|
||||
assertThat(row).containsEntry("marketingContacts", "");
|
||||
assertThat(row).containsEntry("abuseContacts", "");
|
||||
assertThat(row).containsEntry("whoisInquiryContacts", "");
|
||||
assertThat(row).containsEntry("legalContacts", "");
|
||||
assertThat(row).containsEntry("billingContacts", "");
|
||||
assertThat(row).containsEntry("contactsMarkedAsWhoisAdmin", "");
|
||||
assertThat(row).containsEntry("contactsMarkedAsWhoisTech", "");
|
||||
assertThat(row).containsEntry("emailAddress", "contact-us@example.com");
|
||||
assertThat(row).containsEntry("address.street", "123 Main St\nSuite 100");
|
||||
assertThat(row).containsEntry("address.city", "Smalltown");
|
||||
assertThat(row).containsEntry("address.state", "NY");
|
||||
assertThat(row).containsEntry("address.zip", "11211");
|
||||
assertThat(row).containsEntry("address.countryCode", "US");
|
||||
assertThat(row).containsEntry("phoneNumber", "+1.2125551212");
|
||||
assertThat(row).containsEntry("faxNumber", "+1.2125551213");
|
||||
assertThat(row.get("creationTime")).isEqualTo(beforeExecution.toString());
|
||||
assertThat(row.get("lastUpdateTime")).isEqualTo(beforeExecution.toString());
|
||||
assertThat(row).containsEntry("allowedTlds", "");
|
||||
assertThat(row).containsEntry("whoisServer", "whois.example.com");
|
||||
assertThat(row).containsEntry("blockPremiumNames", "false");
|
||||
assertThat(row).containsEntry("ipAddressWhitelist", "");
|
||||
assertThat(row).containsEntry("url", "http://www.example.org/another_registrar");
|
||||
assertThat(row).containsEntry("referralUrl", "http://www.example.org/another_registrar");
|
||||
assertThat(row).containsEntry("icannReferralEmail", "jim@example.net");
|
||||
assertThat(row).containsEntry("billingAccountMap", "{}");
|
||||
|
||||
Cursor cursor = ofy().load().key(Cursor.createGlobalKey(SYNC_REGISTRAR_SHEET)).now();
|
||||
assertThat(cursor).isNotNull();
|
||||
assertThat(cursor.getCursorTime()).isGreaterThan(beforeExecution);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRun_missingValues_stillWorks() throws Exception {
|
||||
persistNewRegistrar("SomeRegistrar", "Some Registrar", Registrar.Type.REAL, 8L);
|
||||
|
||||
newSyncRegistrarsSheet().run("foobar");
|
||||
|
||||
verify(sheetSynchronizer).synchronize(eq("foobar"), rowsCaptor.capture());
|
||||
ImmutableMap<String, String> row = getOnlyElement(getOnlyElement(rowsCaptor.getAllValues()));
|
||||
assertThat(row).containsEntry("clientIdentifier", "SomeRegistrar");
|
||||
assertThat(row).containsEntry("registrarName", "Some Registrar");
|
||||
assertThat(row).containsEntry("state", "");
|
||||
assertThat(row).containsEntry("ianaIdentifier", "8");
|
||||
assertThat(row).containsEntry("billingIdentifier", "");
|
||||
assertThat(row).containsEntry("primaryContacts", "");
|
||||
assertThat(row).containsEntry("techContacts", "");
|
||||
assertThat(row).containsEntry("marketingContacts", "");
|
||||
assertThat(row).containsEntry("abuseContacts", "");
|
||||
assertThat(row).containsEntry("whoisInquiryContacts", "");
|
||||
assertThat(row).containsEntry("legalContacts", "");
|
||||
assertThat(row).containsEntry("billingContacts", "");
|
||||
assertThat(row).containsEntry("contactsMarkedAsWhoisAdmin", "");
|
||||
assertThat(row).containsEntry("contactsMarkedAsWhoisTech", "");
|
||||
assertThat(row).containsEntry("emailAddress", "");
|
||||
assertThat(row).containsEntry("address.street", "123 Fake St");
|
||||
assertThat(row).containsEntry("address.city", "Fakington");
|
||||
assertThat(row).containsEntry("address.state", "");
|
||||
assertThat(row).containsEntry("address.zip", "");
|
||||
assertThat(row).containsEntry("address.countryCode", "US");
|
||||
assertThat(row).containsEntry("phoneNumber", "");
|
||||
assertThat(row).containsEntry("faxNumber", "");
|
||||
assertThat(row).containsEntry("allowedTlds", "");
|
||||
assertThat(row).containsEntry("whoisServer", getDefaultRegistrarWhoisServer());
|
||||
assertThat(row).containsEntry("blockPremiumNames", "false");
|
||||
assertThat(row).containsEntry("ipAddressWhitelist", "");
|
||||
assertThat(row).containsEntry("url", "");
|
||||
assertThat(row).containsEntry("referralUrl", "");
|
||||
assertThat(row).containsEntry("icannReferralEmail", "");
|
||||
assertThat(row).containsEntry("billingAccountMap", "{}");
|
||||
}
|
||||
}
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"name": "projects/registry-project-id/operations/ASAzNjMwOTEyNjUJ",
|
||||
"metadata": {
|
||||
"@type": "type.googleapis.com/google.datastore.admin.v1.ExportEntitiesMetadata",
|
||||
"common": {
|
||||
"startTime": "2014-08-01T01:02:03Z",
|
||||
"operationType": "EXPORT_ENTITIES",
|
||||
"state": "PROCESSING"
|
||||
},
|
||||
"entityFilter": {
|
||||
"kinds": [
|
||||
"one",
|
||||
"two"
|
||||
]
|
||||
},
|
||||
"outputUrlPrefix": "gs://registry-project-id-datastore-export-test/2014-08-01T01:02:03_99364"
|
||||
}
|
||||
}
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"name": "projects/registry-project-id/operations/ASAzNjMwOTEyNjUJ",
|
||||
"metadata": {
|
||||
"@type": "type.googleapis.com/google.datastore.admin.v1.ExportEntitiesMetadata",
|
||||
"common": {
|
||||
"startTime": "2014-08-01T01:02:03Z",
|
||||
"endTime": "2014-08-01T01:32:03Z",
|
||||
"operationType": "EXPORT_ENTITIES",
|
||||
"state": "SUCCESSFUL"
|
||||
},
|
||||
"entityFilter": {
|
||||
"kinds": [
|
||||
"one",
|
||||
"two"
|
||||
]
|
||||
},
|
||||
"outputUrlPrefix": "gs://registry-project-id-datastore-export-test/2014-08-01T01:02:03_99364"
|
||||
},
|
||||
"done": true
|
||||
}
|
||||
Vendored
+17
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"done" : true,
|
||||
"metadata" : {
|
||||
"common" : {
|
||||
"endTime" : "2014-08-01T01:32:03Z",
|
||||
"operationType" : "EXPORT_ENTITIES",
|
||||
"startTime" : "2014-08-01T01:02:03Z",
|
||||
"state" : "SUCCESSFUL"
|
||||
},
|
||||
"entityFilter" : {
|
||||
"kinds" : [ "one", "two" ]
|
||||
},
|
||||
"outputUrlPrefix" : "gs://registry-project-id-datastore-export-test/2014-08-01T01:02:03_99364",
|
||||
"@type" : "type.googleapis.com/google.datastore.admin.v1.ExportEntitiesMetadata"
|
||||
},
|
||||
"name" : "projects/registry-project-id/operations/ASAzNjMwOTEyNjUJ"
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
package(
|
||||
default_testonly = 1,
|
||||
default_visibility = ["//visibility:public"],
|
||||
)
|
||||
|
||||
licenses(["notice"]) # Apache 2.0
|
||||
|
||||
load("//java/com/google/testing/builddefs:GenTestRules.bzl", "GenTestRules")
|
||||
|
||||
# Needed for the documentation tests
|
||||
filegroup(
|
||||
name = "flows_files",
|
||||
srcs = glob([
|
||||
"*.java",
|
||||
"**/*.java",
|
||||
]),
|
||||
)
|
||||
|
||||
java_library(
|
||||
name = "flows",
|
||||
srcs = glob([
|
||||
"*.java",
|
||||
"**/*.java",
|
||||
]),
|
||||
resources = glob(["**/testdata/*.xml"]),
|
||||
deps = [
|
||||
"//java/google/registry/batch",
|
||||
"//java/google/registry/config",
|
||||
"//java/google/registry/dns",
|
||||
"//java/google/registry/flows",
|
||||
"//java/google/registry/model",
|
||||
"//java/google/registry/monitoring/whitebox",
|
||||
"//java/google/registry/pricing",
|
||||
"//java/google/registry/request",
|
||||
"//java/google/registry/request/auth",
|
||||
"//java/google/registry/request/lock",
|
||||
"//java/google/registry/tmch",
|
||||
"//java/google/registry/util",
|
||||
"//java/google/registry/xml",
|
||||
"//javatests/google/registry/model",
|
||||
"//javatests/google/registry/testing",
|
||||
"//javatests/google/registry/xml",
|
||||
"//third_party/objectify:objectify-v4_1",
|
||||
"@com_google_appengine_api_1_0_sdk",
|
||||
"@com_google_appengine_testing",
|
||||
"@com_google_code_findbugs_jsr305",
|
||||
"@com_google_dagger",
|
||||
"@com_google_flogger",
|
||||
"@com_google_flogger_system_backend",
|
||||
"@com_google_guava",
|
||||
"@com_google_guava_testlib",
|
||||
"@com_google_monitoring_client_contrib",
|
||||
"@com_google_monitoring_client_metrics",
|
||||
"@com_google_re2j",
|
||||
"@com_google_truth",
|
||||
"@com_google_truth_extensions_truth_java8_extension",
|
||||
"@com_googlecode_json_simple",
|
||||
"@javax_inject",
|
||||
"@javax_servlet_api",
|
||||
"@joda_time",
|
||||
"@junit",
|
||||
"@org_joda_money",
|
||||
"@org_mockito_core",
|
||||
],
|
||||
)
|
||||
|
||||
# If the flows tests should grow again to the point that they last longer than
|
||||
# sixty seconds, then shard_count should be tuned. You can binary search for a
|
||||
# good value that balances time reduction with environmental impact. However,
|
||||
# any unit test that contains fewer @Test methods than the shard count will
|
||||
# need to be updated to add dummy methods, otherwise blaze will lose its mind.
|
||||
# If you grep for testNothing you can find the existing dummy methods.
|
||||
GenTestRules(
|
||||
name = "GeneratedTestRules",
|
||||
default_test_size = "medium",
|
||||
shard_count = 4,
|
||||
test_files = glob([
|
||||
"*Test.java",
|
||||
"**/*Test.java",
|
||||
]),
|
||||
deps = [":flows"],
|
||||
)
|
||||
@@ -0,0 +1,294 @@
|
||||
// Copyright 2018 The Nomulus Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package google.registry.flows;
|
||||
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
import static com.google.common.truth.Truth8.assertThat;
|
||||
import static google.registry.model.registry.Registry.TldState.PREDELEGATION;
|
||||
import static google.registry.monitoring.whitebox.CheckApiMetric.Availability.AVAILABLE;
|
||||
import static google.registry.monitoring.whitebox.CheckApiMetric.Availability.REGISTERED;
|
||||
import static google.registry.monitoring.whitebox.CheckApiMetric.Availability.RESERVED;
|
||||
import static google.registry.monitoring.whitebox.CheckApiMetric.Tier.PREMIUM;
|
||||
import static google.registry.monitoring.whitebox.CheckApiMetric.Tier.STANDARD;
|
||||
import static google.registry.testing.DatastoreHelper.createTld;
|
||||
import static google.registry.testing.DatastoreHelper.persistActiveDomain;
|
||||
import static google.registry.testing.DatastoreHelper.persistReservedList;
|
||||
import static google.registry.testing.DatastoreHelper.persistResource;
|
||||
import static org.mockito.Mockito.verify;
|
||||
|
||||
import google.registry.model.registry.Registry;
|
||||
import google.registry.monitoring.whitebox.CheckApiMetric;
|
||||
import google.registry.monitoring.whitebox.CheckApiMetric.Availability;
|
||||
import google.registry.monitoring.whitebox.CheckApiMetric.Status;
|
||||
import google.registry.monitoring.whitebox.CheckApiMetric.Tier;
|
||||
import google.registry.testing.AppEngineRule;
|
||||
import google.registry.testing.FakeClock;
|
||||
import google.registry.testing.FakeResponse;
|
||||
import java.util.Map;
|
||||
import org.joda.time.DateTime;
|
||||
import org.json.simple.JSONValue;
|
||||
import org.junit.Before;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.junit.runners.JUnit4;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.mockito.Captor;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.MockitoJUnit;
|
||||
import org.mockito.junit.MockitoRule;
|
||||
|
||||
/** Tests for {@link CheckApiAction}. */
|
||||
@RunWith(JUnit4.class)
|
||||
public class CheckApiActionTest {
|
||||
|
||||
private static final DateTime START_TIME = DateTime.parse("2000-01-01T00:00:00.0Z");
|
||||
|
||||
@Rule public final AppEngineRule appEngine = AppEngineRule.builder().withDatastore().build();
|
||||
@Rule public final MockitoRule mocks = MockitoJUnit.rule();
|
||||
|
||||
@Mock private CheckApiMetrics checkApiMetrics;
|
||||
@Captor private ArgumentCaptor<CheckApiMetric> metricCaptor;
|
||||
|
||||
private DateTime endTime;
|
||||
|
||||
@Before
|
||||
public void init() {
|
||||
createTld("example");
|
||||
persistResource(
|
||||
Registry.get("example")
|
||||
.asBuilder()
|
||||
.setReservedLists(
|
||||
persistReservedList(
|
||||
"example-reserved", "foo,FULLY_BLOCKED", "gold,RESERVED_FOR_SPECIFIC_USE"))
|
||||
.build());
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private Map<String, Object> getCheckResponse(String domain) {
|
||||
CheckApiAction action = new CheckApiAction();
|
||||
action.domain = domain;
|
||||
action.response = new FakeResponse();
|
||||
FakeClock fakeClock = new FakeClock(START_TIME);
|
||||
action.clock = fakeClock;
|
||||
action.metricBuilder = CheckApiMetric.builder(fakeClock);
|
||||
action.checkApiMetrics = checkApiMetrics;
|
||||
fakeClock.advanceOneMilli();
|
||||
endTime = fakeClock.nowUtc();
|
||||
|
||||
action.run();
|
||||
return (Map<String, Object>) JSONValue.parse(((FakeResponse) action.response).getPayload());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testFailure_nullDomain() {
|
||||
assertThat(getCheckResponse(null))
|
||||
.containsExactly(
|
||||
"status", "error",
|
||||
"reason", "Must supply a valid domain name on an authoritative TLD");
|
||||
|
||||
verifyFailureMetric(Status.INVALID_NAME);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testFailure_emptyDomain() {
|
||||
assertThat(getCheckResponse(""))
|
||||
.containsExactly(
|
||||
"status", "error",
|
||||
"reason", "Must supply a valid domain name on an authoritative TLD");
|
||||
|
||||
verifyFailureMetric(Status.INVALID_NAME);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testFailure_invalidDomain() {
|
||||
assertThat(getCheckResponse("@#$%^"))
|
||||
.containsExactly(
|
||||
"status", "error",
|
||||
"reason", "Must supply a valid domain name on an authoritative TLD");
|
||||
|
||||
verifyFailureMetric(Status.INVALID_NAME);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testFailure_singlePartDomain() {
|
||||
assertThat(getCheckResponse("foo"))
|
||||
.containsExactly(
|
||||
"status", "error",
|
||||
"reason", "Must supply a valid domain name on an authoritative TLD");
|
||||
|
||||
verifyFailureMetric(Status.INVALID_NAME);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testFailure_nonExistentTld() {
|
||||
assertThat(getCheckResponse("foo.bar"))
|
||||
.containsExactly(
|
||||
"status", "error",
|
||||
"reason", "Must supply a valid domain name on an authoritative TLD");
|
||||
|
||||
verifyFailureMetric(Status.INVALID_NAME);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testFailure_invalidIdnTable() {
|
||||
assertThat(getCheckResponse("ΑΒΓ.example"))
|
||||
.containsExactly(
|
||||
"status", "error",
|
||||
"reason", "Domain label is not allowed by IDN table");
|
||||
|
||||
verifyFailureMetric(Status.INVALID_NAME);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testFailure_tldInPredelegation() {
|
||||
createTld("predelegated", PREDELEGATION);
|
||||
assertThat(getCheckResponse("foo.predelegated"))
|
||||
.containsExactly(
|
||||
"status", "error",
|
||||
"reason", "Check in this TLD is not allowed in the current registry phase");
|
||||
|
||||
verifyFailureMetric(Status.INVALID_REGISTRY_PHASE);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSuccess_availableStandard() {
|
||||
assertThat(getCheckResponse("somedomain.example"))
|
||||
.containsExactly(
|
||||
"status", "success",
|
||||
"available", true,
|
||||
"tier", "standard");
|
||||
|
||||
verifySuccessMetric(STANDARD, AVAILABLE);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSuccess_availableCapital() {
|
||||
assertThat(getCheckResponse("SOMEDOMAIN.EXAMPLE"))
|
||||
.containsExactly(
|
||||
"status", "success",
|
||||
"available", true,
|
||||
"tier", "standard");
|
||||
|
||||
verifySuccessMetric(STANDARD, AVAILABLE);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSuccess_availableUnicode() {
|
||||
assertThat(getCheckResponse("ééé.example"))
|
||||
.containsExactly(
|
||||
"status", "success",
|
||||
"available", true,
|
||||
"tier", "standard");
|
||||
|
||||
verifySuccessMetric(STANDARD, AVAILABLE);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSuccess_availablePunycode() {
|
||||
assertThat(getCheckResponse("xn--9caaa.example"))
|
||||
.containsExactly(
|
||||
"status", "success",
|
||||
"available", true,
|
||||
"tier", "standard");
|
||||
|
||||
verifySuccessMetric(STANDARD, AVAILABLE);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSuccess_availablePremium() {
|
||||
assertThat(getCheckResponse("rich.example"))
|
||||
.containsExactly(
|
||||
"status", "success",
|
||||
"available", true,
|
||||
"tier", "premium");
|
||||
|
||||
verifySuccessMetric(PREMIUM, AVAILABLE);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSuccess_registered_standard() {
|
||||
persistActiveDomain("somedomain.example");
|
||||
assertThat(getCheckResponse("somedomain.example"))
|
||||
.containsExactly(
|
||||
"tier", "standard",
|
||||
"status", "success",
|
||||
"available", false,
|
||||
"reason", "In use");
|
||||
|
||||
verifySuccessMetric(STANDARD, REGISTERED);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSuccess_reserved_standard() {
|
||||
assertThat(getCheckResponse("foo.example"))
|
||||
.containsExactly(
|
||||
"tier", "standard",
|
||||
"status", "success",
|
||||
"available", false,
|
||||
"reason", "Reserved");
|
||||
|
||||
verifySuccessMetric(STANDARD, RESERVED);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSuccess_registered_premium() {
|
||||
persistActiveDomain("rich.example");
|
||||
assertThat(getCheckResponse("rich.example"))
|
||||
.containsExactly(
|
||||
"tier", "premium",
|
||||
"status", "success",
|
||||
"available", false,
|
||||
"reason", "In use");
|
||||
|
||||
verifySuccessMetric(PREMIUM, REGISTERED);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSuccess_reserved_premium() {
|
||||
assertThat(getCheckResponse("gold.example"))
|
||||
.containsExactly(
|
||||
"tier", "premium",
|
||||
"status", "success",
|
||||
"available", false,
|
||||
"reason", "Reserved");
|
||||
|
||||
verifySuccessMetric(PREMIUM, RESERVED);
|
||||
}
|
||||
|
||||
private void verifySuccessMetric(Tier tier, Availability availability) {
|
||||
verify(checkApiMetrics).incrementCheckApiRequest(metricCaptor.capture());
|
||||
CheckApiMetric metric = metricCaptor.getValue();
|
||||
|
||||
verify(checkApiMetrics).recordProcessingTime(metric);
|
||||
assertThat(metric.availability()).hasValue(availability);
|
||||
assertThat(metric.tier()).hasValue(tier);
|
||||
assertThat(metric.status()).isEqualTo(Status.SUCCESS);
|
||||
assertThat(metric.startTimestamp()).isEqualTo(START_TIME);
|
||||
assertThat(metric.endTimestamp()).isEqualTo(endTime);
|
||||
}
|
||||
|
||||
private void verifyFailureMetric(Status status) {
|
||||
verify(checkApiMetrics).incrementCheckApiRequest(metricCaptor.capture());
|
||||
CheckApiMetric metric = metricCaptor.getValue();
|
||||
|
||||
verify(checkApiMetrics).recordProcessingTime(metric);
|
||||
assertThat(metric.availability()).isEmpty();
|
||||
assertThat(metric.tier()).isEmpty();
|
||||
assertThat(metric.status()).isEqualTo(status);
|
||||
assertThat(metric.startTimestamp()).isEqualTo(START_TIME);
|
||||
assertThat(metric.endTimestamp()).isEqualTo(endTime);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,169 @@
|
||||
// Copyright 2017 The Nomulus Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package google.registry.flows;
|
||||
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
import static google.registry.model.EppResourceUtils.loadAtPointInTime;
|
||||
import static google.registry.model.ofy.ObjectifyService.ofy;
|
||||
import static google.registry.testing.DatastoreHelper.createTld;
|
||||
import static google.registry.testing.DatastoreHelper.persistActiveContact;
|
||||
import static google.registry.testing.DatastoreHelper.persistActiveHost;
|
||||
import static java.nio.charset.StandardCharsets.UTF_8;
|
||||
import static org.joda.time.DateTimeZone.UTC;
|
||||
import static org.joda.time.Duration.standardDays;
|
||||
|
||||
import com.google.common.collect.ImmutableMap;
|
||||
import com.googlecode.objectify.Key;
|
||||
import google.registry.flows.EppTestComponent.FakesAndMocksModule;
|
||||
import google.registry.model.domain.DomainBase;
|
||||
import google.registry.model.ofy.Ofy;
|
||||
import google.registry.monitoring.whitebox.EppMetric;
|
||||
import google.registry.testing.AppEngineRule;
|
||||
import google.registry.testing.EppLoader;
|
||||
import google.registry.testing.FakeClock;
|
||||
import google.registry.testing.FakeHttpSession;
|
||||
import google.registry.testing.InjectRule;
|
||||
import google.registry.testing.ShardableTestCase;
|
||||
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;
|
||||
|
||||
/** Test that domain flows create the commit logs needed to reload at points in the past. */
|
||||
@RunWith(JUnit4.class)
|
||||
public class EppCommitLogsTest extends ShardableTestCase {
|
||||
|
||||
@Rule
|
||||
public final AppEngineRule appEngine = AppEngineRule.builder()
|
||||
.withDatastore()
|
||||
.withTaskQueue()
|
||||
.build();
|
||||
|
||||
@Rule
|
||||
public final InjectRule inject = new InjectRule();
|
||||
|
||||
private final FakeClock clock = new FakeClock(DateTime.now(UTC));
|
||||
private EppLoader eppLoader;
|
||||
|
||||
@Before
|
||||
public void init() {
|
||||
createTld("tld");
|
||||
inject.setStaticField(Ofy.class, "clock", clock);
|
||||
}
|
||||
|
||||
private void runFlow() throws Exception {
|
||||
SessionMetadata sessionMetadata = new HttpSessionMetadata(new FakeHttpSession());
|
||||
sessionMetadata.setClientId("TheRegistrar");
|
||||
DaggerEppTestComponent.builder()
|
||||
.fakesAndMocksModule(
|
||||
FakesAndMocksModule.create(clock, EppMetric.builderForRequest(clock)))
|
||||
.build()
|
||||
.startRequest()
|
||||
.flowComponentBuilder()
|
||||
.flowModule(
|
||||
new FlowModule.Builder()
|
||||
.setSessionMetadata(sessionMetadata)
|
||||
.setCredentials(new PasswordOnlyTransportCredentials())
|
||||
.setEppRequestSource(EppRequestSource.UNIT_TEST)
|
||||
.setIsDryRun(false)
|
||||
.setIsSuperuser(false)
|
||||
.setInputXmlBytes(eppLoader.getEppXml().getBytes(UTF_8))
|
||||
.setEppInput(eppLoader.getEpp())
|
||||
.build())
|
||||
.build()
|
||||
.flowRunner()
|
||||
.run(EppMetric.builder());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testLoadAtPointInTime() throws Exception {
|
||||
clock.setTo(DateTime.parse("1984-12-18T12:30Z")); // not midnight
|
||||
|
||||
persistActiveHost("ns1.example.net");
|
||||
persistActiveHost("ns2.example.net");
|
||||
persistActiveContact("jd1234");
|
||||
persistActiveContact("sh8013");
|
||||
|
||||
clock.advanceBy(standardDays(1));
|
||||
DateTime timeAtCreate = clock.nowUtc();
|
||||
clock.setTo(timeAtCreate);
|
||||
eppLoader = new EppLoader(this, "domain_create.xml", ImmutableMap.of("DOMAIN", "example.tld"));
|
||||
runFlow();
|
||||
ofy().clearSessionCache();
|
||||
Key<DomainBase> key = Key.create(ofy().load().type(DomainBase.class).first().now());
|
||||
DomainBase domainAfterCreate = ofy().load().key(key).now();
|
||||
assertThat(domainAfterCreate.getFullyQualifiedDomainName()).isEqualTo("example.tld");
|
||||
|
||||
clock.advanceBy(standardDays(2));
|
||||
DateTime timeAtFirstUpdate = clock.nowUtc();
|
||||
eppLoader = new EppLoader(this, "domain_update_dsdata_add.xml");
|
||||
runFlow();
|
||||
ofy().clearSessionCache();
|
||||
|
||||
DomainBase domainAfterFirstUpdate = ofy().load().key(key).now();
|
||||
assertThat(domainAfterCreate).isNotEqualTo(domainAfterFirstUpdate);
|
||||
|
||||
clock.advanceOneMilli(); // same day as first update
|
||||
DateTime timeAtSecondUpdate = clock.nowUtc();
|
||||
eppLoader = new EppLoader(this, "domain_update_dsdata_rem.xml");
|
||||
runFlow();
|
||||
ofy().clearSessionCache();
|
||||
DomainBase domainAfterSecondUpdate = ofy().load().key(key).now();
|
||||
|
||||
clock.advanceBy(standardDays(2));
|
||||
DateTime timeAtDelete = clock.nowUtc(); // before 'add' grace period ends
|
||||
eppLoader = new EppLoader(this, "domain_delete.xml", ImmutableMap.of("DOMAIN", "example.tld"));
|
||||
runFlow();
|
||||
ofy().clearSessionCache();
|
||||
|
||||
assertThat(domainAfterFirstUpdate).isNotEqualTo(domainAfterSecondUpdate);
|
||||
|
||||
// Point-in-time can only rewind an object from the current version, not roll forward.
|
||||
DomainBase latest = ofy().load().key(key).now();
|
||||
|
||||
// Creation time has millisecond granularity due to isActive() check.
|
||||
ofy().clearSessionCache();
|
||||
assertThat(loadAtPointInTime(latest, timeAtCreate.minusMillis(1)).now()).isNull();
|
||||
assertThat(loadAtPointInTime(latest, timeAtCreate).now()).isNotNull();
|
||||
assertThat(loadAtPointInTime(latest, timeAtCreate.plusMillis(1)).now()).isNotNull();
|
||||
|
||||
ofy().clearSessionCache();
|
||||
assertThat(loadAtPointInTime(latest, timeAtCreate.plusDays(1)).now())
|
||||
.isEqualTo(domainAfterCreate);
|
||||
|
||||
// Both updates happened on the same day. Since the revisions field has day granularity, the
|
||||
// key to the first update should have been overwritten by the second, and its timestamp rolled
|
||||
// forward. So we have to fall back to the last revision before midnight.
|
||||
ofy().clearSessionCache();
|
||||
assertThat(loadAtPointInTime(latest, timeAtFirstUpdate).now())
|
||||
.isEqualTo(domainAfterCreate);
|
||||
|
||||
ofy().clearSessionCache();
|
||||
assertThat(loadAtPointInTime(latest, timeAtSecondUpdate).now())
|
||||
.isEqualTo(domainAfterSecondUpdate);
|
||||
|
||||
ofy().clearSessionCache();
|
||||
assertThat(loadAtPointInTime(latest, timeAtSecondUpdate.plusDays(1)).now())
|
||||
.isEqualTo(domainAfterSecondUpdate);
|
||||
|
||||
// Deletion time has millisecond granularity due to isActive() check.
|
||||
ofy().clearSessionCache();
|
||||
assertThat(loadAtPointInTime(latest, timeAtDelete.minusMillis(1)).now()).isNotNull();
|
||||
assertThat(loadAtPointInTime(latest, timeAtDelete).now()).isNull();
|
||||
assertThat(loadAtPointInTime(latest, timeAtDelete.plusMillis(1)).now()).isNull();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,249 @@
|
||||
// Copyright 2017 The Nomulus Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package google.registry.flows;
|
||||
|
||||
import static com.google.common.io.BaseEncoding.base64;
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
import static google.registry.model.eppcommon.EppXmlTransformer.marshal;
|
||||
import static google.registry.testing.DatastoreHelper.createTld;
|
||||
import static google.registry.testing.LogsSubject.assertAboutLogs;
|
||||
import static google.registry.testing.TestDataHelper.loadFile;
|
||||
import static google.registry.testing.TestLogHandlerUtils.findFirstLogRecordWithMessagePrefix;
|
||||
import static java.nio.charset.StandardCharsets.UTF_8;
|
||||
import static java.util.logging.Level.INFO;
|
||||
import static java.util.logging.Level.SEVERE;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.verifyZeroInteractions;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import com.google.common.base.Splitter;
|
||||
import com.google.common.testing.TestLogHandler;
|
||||
import google.registry.flows.EppException.UnimplementedExtensionException;
|
||||
import google.registry.flows.EppTestComponent.FakeServerTridProvider;
|
||||
import google.registry.flows.FlowModule.EppExceptionInProviderException;
|
||||
import google.registry.model.eppcommon.Trid;
|
||||
import google.registry.model.eppoutput.EppOutput;
|
||||
import google.registry.model.eppoutput.EppResponse;
|
||||
import google.registry.model.eppoutput.Result;
|
||||
import google.registry.model.eppoutput.Result.Code;
|
||||
import google.registry.monitoring.whitebox.EppMetric;
|
||||
import google.registry.testing.AppEngineRule;
|
||||
import google.registry.testing.FakeClock;
|
||||
import google.registry.testing.ShardableTestCase;
|
||||
import google.registry.util.Clock;
|
||||
import google.registry.xml.ValidationMode;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.logging.LogRecord;
|
||||
import java.util.logging.Logger;
|
||||
import org.joda.time.DateTime;
|
||||
import org.json.simple.JSONValue;
|
||||
import org.junit.After;
|
||||
import org.junit.Before;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.junit.runners.JUnit4;
|
||||
import org.mockito.ArgumentMatchers;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.MockitoJUnit;
|
||||
import org.mockito.junit.MockitoRule;
|
||||
|
||||
/** Unit tests for {@link EppController}. */
|
||||
@RunWith(JUnit4.class)
|
||||
public class EppControllerTest extends ShardableTestCase {
|
||||
|
||||
@Rule public AppEngineRule appEngineRule = new AppEngineRule.Builder().withDatastore().build();
|
||||
@Rule public final MockitoRule mocks = MockitoJUnit.rule();
|
||||
|
||||
@Mock SessionMetadata sessionMetadata;
|
||||
@Mock TransportCredentials transportCredentials;
|
||||
@Mock EppMetrics eppMetrics;
|
||||
@Mock FlowComponent.Builder flowComponentBuilder;
|
||||
@Mock FlowComponent flowComponent;
|
||||
@Mock FlowRunner flowRunner;
|
||||
@Mock EppOutput eppOutput;
|
||||
@Mock EppResponse eppResponse;
|
||||
@Mock Result result;
|
||||
|
||||
private static final DateTime START_TIME = DateTime.parse("2016-09-01T00:00:00Z");
|
||||
|
||||
private final Clock clock = new FakeClock(START_TIME);
|
||||
private final TestLogHandler logHandler = new TestLogHandler();
|
||||
|
||||
/**
|
||||
* Hold a strong reference to the logger whose output is being intercepted to prevent it being
|
||||
* GCed.
|
||||
*/
|
||||
private final Logger loggerToIntercept = Logger.getLogger(EppController.class.getCanonicalName());
|
||||
|
||||
private final String domainCreateXml = loadFile(getClass(), "domain_create_prettyprinted.xml");
|
||||
|
||||
private EppController eppController;
|
||||
|
||||
@Before
|
||||
public void setUp() throws Exception {
|
||||
loggerToIntercept.addHandler(logHandler);
|
||||
|
||||
when(sessionMetadata.getClientId()).thenReturn("some-client");
|
||||
when(flowComponentBuilder.flowModule(ArgumentMatchers.any())).thenReturn(flowComponentBuilder);
|
||||
when(flowComponentBuilder.build()).thenReturn(flowComponent);
|
||||
when(flowComponent.flowRunner()).thenReturn(flowRunner);
|
||||
when(eppOutput.isResponse()).thenReturn(true);
|
||||
when(eppOutput.getResponse()).thenReturn(eppResponse);
|
||||
when(eppResponse.getResult()).thenReturn(result);
|
||||
when(result.getCode()).thenReturn(Code.SUCCESS_WITH_NO_MESSAGES);
|
||||
|
||||
eppController = new EppController();
|
||||
eppController.eppMetricBuilder = EppMetric.builderForRequest(clock);
|
||||
when(flowRunner.run(eppController.eppMetricBuilder)).thenReturn(eppOutput);
|
||||
eppController.flowComponentBuilder = flowComponentBuilder;
|
||||
eppController.eppMetrics = eppMetrics;
|
||||
eppController.serverTridProvider = new FakeServerTridProvider();
|
||||
}
|
||||
|
||||
@After
|
||||
public void tearDown() {
|
||||
loggerToIntercept.removeHandler(logHandler);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testMarshallingUnknownError() throws Exception {
|
||||
marshal(
|
||||
EppController.getErrorResponse(
|
||||
Result.create(Code.COMMAND_FAILED), Trid.create(null, "server-trid")),
|
||||
ValidationMode.STRICT);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testHandleEppCommand_regularEppCommand_exportsEppMetrics() {
|
||||
createTld("tld");
|
||||
// Note that some of the EPP metric fields, like # of attempts and command name, are set in
|
||||
// FlowRunner, not EppController, and since FlowRunner is mocked out for these tests they won't
|
||||
// actually get values.
|
||||
EppMetric.Builder metricBuilder =
|
||||
EppMetric.builderForRequest(clock)
|
||||
.setClientId("some-client")
|
||||
.setStatus(Code.SUCCESS_WITH_NO_MESSAGES)
|
||||
.setTld("tld");
|
||||
eppController.handleEppCommand(
|
||||
sessionMetadata,
|
||||
transportCredentials,
|
||||
EppRequestSource.UNIT_TEST,
|
||||
false,
|
||||
true,
|
||||
domainCreateXml.getBytes(UTF_8));
|
||||
|
||||
EppMetric expectedMetric = metricBuilder.build();
|
||||
verify(eppMetrics).incrementEppRequests(eq(expectedMetric));
|
||||
verify(eppMetrics).recordProcessingTime(eq(expectedMetric));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testHandleEppCommand_dryRunEppCommand_doesNotExportMetric() {
|
||||
eppController.handleEppCommand(
|
||||
sessionMetadata,
|
||||
transportCredentials,
|
||||
EppRequestSource.UNIT_TEST,
|
||||
true,
|
||||
true,
|
||||
domainCreateXml.getBytes(UTF_8));
|
||||
verifyZeroInteractions(eppMetrics);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testHandleEppCommand_unmarshallableData_loggedAtInfo_withJsonData() throws Exception {
|
||||
eppController.handleEppCommand(
|
||||
sessionMetadata,
|
||||
transportCredentials,
|
||||
EppRequestSource.UNIT_TEST,
|
||||
false,
|
||||
false,
|
||||
"GET / HTTP/1.1\n\n".getBytes(UTF_8));
|
||||
|
||||
assertAboutLogs().that(logHandler)
|
||||
.hasLogAtLevelWithMessage(INFO, "EPP request XML unmarshalling failed");
|
||||
LogRecord logRecord =
|
||||
findFirstLogRecordWithMessagePrefix(logHandler, "EPP request XML unmarshalling failed");
|
||||
List<String> messageParts = Splitter.on('\n').splitToList(logRecord.getMessage());
|
||||
assertThat(messageParts.size()).isAtLeast(2);
|
||||
Map<String, Object> json = parseJsonMap(messageParts.get(1));
|
||||
assertThat(json).containsEntry("clientId", "some-client");
|
||||
assertThat(json).containsEntry("resultCode", 2001L); // Must be Long to compare equal.
|
||||
assertThat(json).containsEntry("resultMessage", "Command syntax error");
|
||||
assertThat(json)
|
||||
.containsEntry("xmlBytes", base64().encode("GET / HTTP/1.1\n\n".getBytes(UTF_8)));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testHandleEppCommand_throwsEppException_loggedAtInfo() throws Exception {
|
||||
when(flowRunner.run(eppController.eppMetricBuilder))
|
||||
.thenThrow(new UnimplementedExtensionException());
|
||||
eppController.handleEppCommand(
|
||||
sessionMetadata,
|
||||
transportCredentials,
|
||||
EppRequestSource.UNIT_TEST,
|
||||
false,
|
||||
true,
|
||||
domainCreateXml.getBytes(UTF_8));
|
||||
assertAboutLogs().that(logHandler)
|
||||
.hasLogAtLevelWithMessage(INFO, "Flow returned failure response");
|
||||
LogRecord logRecord =
|
||||
findFirstLogRecordWithMessagePrefix(logHandler, "Flow returned failure response");
|
||||
assertThat(logRecord.getThrown()).isInstanceOf(UnimplementedExtensionException.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testHandleEppCommand_throwsEppExceptionInProviderException_loggedAtInfo()
|
||||
throws Exception {
|
||||
when(flowRunner.run(eppController.eppMetricBuilder))
|
||||
.thenThrow(new EppExceptionInProviderException(new UnimplementedExtensionException()));
|
||||
eppController.handleEppCommand(
|
||||
sessionMetadata,
|
||||
transportCredentials,
|
||||
EppRequestSource.UNIT_TEST,
|
||||
false,
|
||||
true,
|
||||
domainCreateXml.getBytes(UTF_8));
|
||||
assertAboutLogs().that(logHandler)
|
||||
.hasLogAtLevelWithMessage(INFO, "Flow returned failure response");
|
||||
LogRecord logRecord =
|
||||
findFirstLogRecordWithMessagePrefix(logHandler, "Flow returned failure response");
|
||||
assertThat(logRecord.getThrown()).isInstanceOf(EppExceptionInProviderException.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testHandleEppCommand_throwsRuntimeException_loggedAtSevere() throws Exception {
|
||||
when(flowRunner.run(eppController.eppMetricBuilder)).thenThrow(new IllegalStateException());
|
||||
eppController.handleEppCommand(
|
||||
sessionMetadata,
|
||||
transportCredentials,
|
||||
EppRequestSource.UNIT_TEST,
|
||||
false,
|
||||
true,
|
||||
domainCreateXml.getBytes(UTF_8));
|
||||
assertAboutLogs().that(logHandler)
|
||||
.hasLogAtLevelWithMessage(SEVERE, "Unexpected failure in flow execution");
|
||||
LogRecord logRecord =
|
||||
findFirstLogRecordWithMessagePrefix(logHandler, "Unexpected failure in flow execution");
|
||||
assertThat(logRecord.getThrown()).isInstanceOf(IllegalStateException.class);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private static Map<String, Object> parseJsonMap(String json) throws Exception {
|
||||
return (Map<String, Object>) JSONValue.parseWithException(json);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
// Copyright 2017 The Nomulus Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package google.registry.flows;
|
||||
|
||||
import static google.registry.model.eppoutput.Result.Code.SUCCESS;
|
||||
import static google.registry.model.eppoutput.Result.Code.SUCCESS_WITH_ACK_MESSAGE;
|
||||
import static google.registry.model.eppoutput.Result.Code.SUCCESS_WITH_ACTION_PENDING;
|
||||
import static google.registry.model.eppoutput.Result.Code.SUCCESS_WITH_NO_MESSAGES;
|
||||
import static google.registry.testing.EppMetricSubject.assertThat;
|
||||
|
||||
import com.google.common.collect.ImmutableMap;
|
||||
import google.registry.testing.AppEngineRule;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.junit.runners.JUnit4;
|
||||
|
||||
/** Tests for contact lifecycle. */
|
||||
@RunWith(JUnit4.class)
|
||||
public class EppLifecycleContactTest extends EppTestCase {
|
||||
|
||||
@Rule
|
||||
public final AppEngineRule appEngine = AppEngineRule.builder()
|
||||
.withDatastore()
|
||||
.withTaskQueue()
|
||||
.build();
|
||||
|
||||
@Test
|
||||
public void testContactLifecycle() throws Exception {
|
||||
assertThatLoginSucceeds("NewRegistrar", "foo-BAR2");
|
||||
assertThatCommand("contact_create_sh8013.xml")
|
||||
.atTime("2000-06-01T00:00:00Z")
|
||||
.hasResponse(
|
||||
"contact_create_response_sh8013.xml",
|
||||
ImmutableMap.of("CRDATE", "2000-06-01T00:00:00Z"));
|
||||
assertThat(getRecordedEppMetric())
|
||||
.hasClientId("NewRegistrar")
|
||||
.and()
|
||||
.hasNoTld()
|
||||
.and()
|
||||
.hasCommandName("ContactCreate")
|
||||
.and()
|
||||
.hasStatus(SUCCESS);
|
||||
assertThatCommand("contact_info.xml")
|
||||
.atTime("2000-06-01T00:01:00Z")
|
||||
.hasResponse("contact_info_from_create_response.xml");
|
||||
assertThat(getRecordedEppMetric())
|
||||
.hasClientId("NewRegistrar")
|
||||
.and()
|
||||
.hasCommandName("ContactInfo")
|
||||
.and()
|
||||
.hasStatus(SUCCESS);
|
||||
assertThatCommand("contact_delete_sh8013.xml")
|
||||
.hasResponse("contact_delete_response_sh8013.xml");
|
||||
assertThat(getRecordedEppMetric())
|
||||
.hasClientId("NewRegistrar")
|
||||
.and()
|
||||
.hasCommandName("ContactDelete")
|
||||
.and()
|
||||
.hasStatus(SUCCESS_WITH_ACTION_PENDING);
|
||||
assertThatLogoutSucceeds();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testContactTransferPollMessage() throws Exception {
|
||||
assertThatLoginSucceeds("NewRegistrar", "foo-BAR2");
|
||||
assertThatCommand("contact_create_sh8013.xml")
|
||||
.atTime("2000-06-01T00:00:00Z")
|
||||
.hasResponse(
|
||||
"contact_create_response_sh8013.xml",
|
||||
ImmutableMap.of("CRDATE", "2000-06-01T00:00:00Z"));
|
||||
assertThatLogoutSucceeds();
|
||||
|
||||
// Initiate a transfer of the newly created contact.
|
||||
assertThatLoginSucceeds("TheRegistrar", "password2");
|
||||
assertThatCommand("contact_transfer_request.xml")
|
||||
.atTime("2000-06-08T22:00:00Z")
|
||||
.hasResponse("contact_transfer_request_response_alternate.xml");
|
||||
assertThatLogoutSucceeds();
|
||||
|
||||
// Log back in with the losing registrar, read the poll message, and then ack it.
|
||||
assertThatLoginSucceeds("NewRegistrar", "foo-BAR2");
|
||||
assertThatCommand("poll.xml")
|
||||
.atTime("2000-06-08T22:01:00Z")
|
||||
.hasResponse("poll_response_contact_transfer.xml");
|
||||
assertThat(getRecordedEppMetric())
|
||||
.hasClientId("NewRegistrar")
|
||||
.and()
|
||||
.hasCommandName("PollRequest")
|
||||
.and()
|
||||
.hasStatus(SUCCESS_WITH_ACK_MESSAGE);
|
||||
assertThatCommand("poll_ack.xml", ImmutableMap.of("ID", "2-1-ROID-3-6-2000"))
|
||||
.atTime("2000-06-08T22:02:00Z")
|
||||
.hasResponse("poll_ack_response_empty.xml");
|
||||
assertThat(getRecordedEppMetric())
|
||||
.hasClientId("NewRegistrar")
|
||||
.and()
|
||||
.hasCommandName("PollAck")
|
||||
.and()
|
||||
.hasStatus(SUCCESS_WITH_NO_MESSAGES);
|
||||
assertThatLogoutSucceeds();
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,248 @@
|
||||
// Copyright 2017 The Nomulus Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package google.registry.flows;
|
||||
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
import static google.registry.model.EppResourceUtils.loadByForeignKey;
|
||||
import static google.registry.model.eppoutput.Result.Code.SUCCESS;
|
||||
import static google.registry.model.eppoutput.Result.Code.SUCCESS_WITH_ACTION_PENDING;
|
||||
import static google.registry.testing.DatastoreHelper.createTld;
|
||||
import static google.registry.testing.DatastoreHelper.createTlds;
|
||||
import static google.registry.testing.EppMetricSubject.assertThat;
|
||||
import static google.registry.testing.HostResourceSubject.assertAboutHosts;
|
||||
|
||||
import com.google.common.collect.ImmutableMap;
|
||||
import com.googlecode.objectify.Key;
|
||||
import google.registry.model.domain.DomainBase;
|
||||
import google.registry.model.host.HostResource;
|
||||
import google.registry.testing.AppEngineRule;
|
||||
import org.joda.time.DateTime;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.junit.runners.JUnit4;
|
||||
|
||||
/** Tests for host lifecycle. */
|
||||
@RunWith(JUnit4.class)
|
||||
public class EppLifecycleHostTest extends EppTestCase {
|
||||
|
||||
@Rule
|
||||
public final AppEngineRule appEngine = AppEngineRule.builder()
|
||||
.withDatastore()
|
||||
.withTaskQueue()
|
||||
.build();
|
||||
|
||||
@Test
|
||||
public void testLifecycle() throws Exception {
|
||||
assertThatLoginSucceeds("NewRegistrar", "foo-BAR2");
|
||||
assertThatCommand("hello.xml")
|
||||
.atTime("2000-06-02T00:00:00Z")
|
||||
.hasResponse("greeting.xml", ImmutableMap.of("DATE", "2000-06-02T00:00:00Z"));
|
||||
// Note that Hello commands don't set a status code on the response.
|
||||
assertThat(getRecordedEppMetric())
|
||||
.hasClientId("NewRegistrar")
|
||||
.and()
|
||||
.hasCommandName("Hello")
|
||||
.and()
|
||||
.hasNoStatus();
|
||||
assertThatCommand("host_create.xml", ImmutableMap.of("HOSTNAME", "ns1.example.tld"))
|
||||
.atTime("2000-06-02T00:01:00Z")
|
||||
.hasResponse(
|
||||
"host_create_response.xml",
|
||||
ImmutableMap.of("HOSTNAME", "ns1.example.tld", "CRDATE", "2000-06-02T00:01:00Z"));
|
||||
assertThat(getRecordedEppMetric())
|
||||
.hasClientId("NewRegistrar")
|
||||
.and()
|
||||
.hasCommandName("HostCreate")
|
||||
.and()
|
||||
.hasStatus(SUCCESS);
|
||||
assertThatCommand("host_info.xml", ImmutableMap.of("HOSTNAME", "ns1.example.tld"))
|
||||
.atTime("2000-06-02T00:02:00Z")
|
||||
.hasResponse(
|
||||
"host_info_response.xml",
|
||||
ImmutableMap.of(
|
||||
"HOSTNAME", "ns1.example.tld", "ROID", "1-ROID", "CRDATE", "2000-06-02T00:01:00Z"));
|
||||
assertThat(getRecordedEppMetric())
|
||||
.hasClientId("NewRegistrar")
|
||||
.and()
|
||||
.hasCommandName("HostInfo")
|
||||
.and()
|
||||
.hasStatus(SUCCESS);
|
||||
assertThatCommand("host_delete.xml", ImmutableMap.of("HOSTNAME", "ns1.example.tld"))
|
||||
.atTime("2000-06-02T00:03:00Z")
|
||||
.hasResponse("generic_success_action_pending_response.xml");
|
||||
assertThat(getRecordedEppMetric())
|
||||
.hasClientId("NewRegistrar")
|
||||
.and()
|
||||
.hasCommandName("HostDelete")
|
||||
.and()
|
||||
.hasStatus(SUCCESS_WITH_ACTION_PENDING);
|
||||
assertThatLogoutSucceeds();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRenamingHostToExistingHost_fails() throws Exception {
|
||||
createTld("example");
|
||||
assertThatLoginSucceeds("NewRegistrar", "foo-BAR2");
|
||||
// Create the fakesite domain.
|
||||
assertThatCommand("contact_create_sh8013.xml")
|
||||
.atTime("2000-06-01T00:00:00Z")
|
||||
.hasResponse(
|
||||
"contact_create_response_sh8013.xml",
|
||||
ImmutableMap.of("CRDATE", "2000-06-01T00:00:00Z"));
|
||||
assertThatCommand("contact_create_jd1234.xml")
|
||||
.atTime("2000-06-01T00:01:00Z")
|
||||
.hasResponse("contact_create_response_jd1234.xml");
|
||||
assertThatCommand("domain_create_fakesite_no_nameservers.xml")
|
||||
.atTime("2000-06-01T00:04:00Z")
|
||||
.hasResponse(
|
||||
"domain_create_response.xml",
|
||||
ImmutableMap.of(
|
||||
"DOMAIN", "fakesite.example",
|
||||
"CRDATE", "2000-06-01T00:04:00.0Z",
|
||||
"EXDATE", "2002-06-01T00:04:00.0Z"));
|
||||
assertThatCommand("domain_info_fakesite.xml")
|
||||
.atTime("2000-06-05T00:02:00Z")
|
||||
.hasResponse("domain_info_response_fakesite_inactive.xml");
|
||||
// Add the fakesite subordinate host (requires that domain is already created).
|
||||
assertThatCommand("host_create_fakesite.xml")
|
||||
.atTime("2000-06-06T00:01:00Z")
|
||||
.hasResponse("host_create_response_fakesite.xml");
|
||||
// Add the 2nd fakesite subordinate host.
|
||||
assertThatCommand("host_create_fakesite2.xml")
|
||||
.atTime("2000-06-09T00:01:00Z")
|
||||
.hasResponse("host_create_response_fakesite2.xml");
|
||||
// Attempt overwriting of 2nd fakesite subordinate host with the 1st.
|
||||
assertThatCommand("host_update_fakesite1_to_fakesite2.xml")
|
||||
.atTime("2000-06-10T00:01:00Z")
|
||||
.hasResponse(
|
||||
"response_error.xml",
|
||||
ImmutableMap.of(
|
||||
"CODE", "2302",
|
||||
"MSG", "Object with given ID (ns4.fakesite.example) already exists"));
|
||||
// Verify that fakesite hosts still exist in their unmodified states.
|
||||
assertThatCommand("host_info_fakesite.xml")
|
||||
.atTime("2000-06-11T00:07:00Z")
|
||||
.hasResponse("host_info_response_fakesite_ok.xml");
|
||||
assertThatCommand("host_info_fakesite2.xml")
|
||||
.atTime("2000-06-11T00:08:00Z")
|
||||
.hasResponse("host_info_response_fakesite2.xml");
|
||||
assertThatLogoutSucceeds();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSuccess_multipartTldsWithSharedSuffixes() throws Exception {
|
||||
createTlds("bar.foo.tld", "foo.tld", "tld");
|
||||
|
||||
assertThatLoginSucceeds("NewRegistrar", "foo-BAR2");
|
||||
|
||||
assertThatCommand("contact_create_sh8013.xml")
|
||||
.atTime("2000-06-01T00:00:00Z")
|
||||
.hasResponse(
|
||||
"contact_create_response_sh8013.xml",
|
||||
ImmutableMap.of("CRDATE", "2000-06-01T00:00:00Z"));
|
||||
assertThatCommand("contact_create_jd1234.xml")
|
||||
.atTime("2000-06-01T00:01:00Z")
|
||||
.hasResponse("contact_create_response_jd1234.xml");
|
||||
|
||||
// Create domain example.bar.foo.tld
|
||||
assertThatCommand(
|
||||
"domain_create_no_hosts_or_dsdata.xml",
|
||||
ImmutableMap.of("DOMAIN", "example.bar.foo.tld"))
|
||||
.atTime("2000-06-01T00:02:00.000Z")
|
||||
.hasResponse(
|
||||
"domain_create_response.xml",
|
||||
ImmutableMap.of(
|
||||
"DOMAIN", "example.bar.foo.tld",
|
||||
"CRDATE", "2000-06-01T00:02:00Z",
|
||||
"EXDATE", "2002-06-01T00:02:00Z"));
|
||||
|
||||
// Create domain example.foo.tld
|
||||
assertThatCommand(
|
||||
"domain_create_no_hosts_or_dsdata.xml", ImmutableMap.of("DOMAIN", "example.foo.tld"))
|
||||
.atTime("2000-06-01T00:02:00.001Z")
|
||||
.hasResponse(
|
||||
"domain_create_response.xml",
|
||||
ImmutableMap.of(
|
||||
"DOMAIN", "example.foo.tld",
|
||||
"CRDATE", "2000-06-01T00:02:00Z",
|
||||
"EXDATE", "2002-06-01T00:02:00Z"));
|
||||
|
||||
// Create domain example.tld
|
||||
assertThatCommand(
|
||||
"domain_create_no_hosts_or_dsdata.xml", ImmutableMap.of("DOMAIN", "example.tld"))
|
||||
.atTime("2000-06-01T00:02:00.002Z")
|
||||
.hasResponse(
|
||||
"domain_create_response.xml",
|
||||
ImmutableMap.of(
|
||||
"DOMAIN", "example.tld",
|
||||
"CRDATE", "2000-06-01T00:02:00Z",
|
||||
"EXDATE", "2002-06-01T00:02:00Z"));
|
||||
|
||||
// Create host ns1.example.bar.foo.tld
|
||||
assertThatCommand(
|
||||
"host_create_with_ips.xml", ImmutableMap.of("HOSTNAME", "ns1.example.bar.foo.tld"))
|
||||
.atTime("2000-06-01T00:03:00Z")
|
||||
.hasResponse(
|
||||
"host_create_response.xml",
|
||||
ImmutableMap.of(
|
||||
"HOSTNAME", "ns1.example.bar.foo.tld", "CRDATE", "2000-06-01T00:03:00Z"));
|
||||
|
||||
// Create host ns1.example.foo.tld
|
||||
assertThatCommand(
|
||||
"host_create_with_ips.xml", ImmutableMap.of("HOSTNAME", "ns1.example.foo.tld"))
|
||||
.atTime("2000-06-01T00:04:00Z")
|
||||
.hasResponse(
|
||||
"host_create_response.xml",
|
||||
ImmutableMap.of("HOSTNAME", "ns1.example.foo.tld", "CRDATE", "2000-06-01T00:04:00Z"));
|
||||
|
||||
// Create host ns1.example.tld
|
||||
assertThatCommand("host_create_with_ips.xml", ImmutableMap.of("HOSTNAME", "ns1.example.tld"))
|
||||
.atTime("2000-06-01T00:05:00Z")
|
||||
.hasResponse(
|
||||
"host_create_response.xml",
|
||||
ImmutableMap.of("HOSTNAME", "ns1.example.tld", "CRDATE", "2000-06-01T00:05:00Z"));
|
||||
|
||||
DateTime timeAfterCreates = DateTime.parse("2000-06-01T00:06:00Z");
|
||||
|
||||
HostResource exampleBarFooTldHost =
|
||||
loadByForeignKey(HostResource.class, "ns1.example.bar.foo.tld", timeAfterCreates).get();
|
||||
DomainBase exampleBarFooTldDomain =
|
||||
loadByForeignKey(DomainBase.class, "example.bar.foo.tld", timeAfterCreates).get();
|
||||
assertAboutHosts()
|
||||
.that(exampleBarFooTldHost)
|
||||
.hasSuperordinateDomain(Key.create(exampleBarFooTldDomain));
|
||||
assertThat(exampleBarFooTldDomain.getSubordinateHosts())
|
||||
.containsExactly("ns1.example.bar.foo.tld");
|
||||
|
||||
HostResource exampleFooTldHost =
|
||||
loadByForeignKey(HostResource.class, "ns1.example.foo.tld", timeAfterCreates).get();
|
||||
DomainBase exampleFooTldDomain =
|
||||
loadByForeignKey(DomainBase.class, "example.foo.tld", timeAfterCreates).get();
|
||||
assertAboutHosts()
|
||||
.that(exampleFooTldHost)
|
||||
.hasSuperordinateDomain(Key.create(exampleFooTldDomain));
|
||||
assertThat(exampleFooTldDomain.getSubordinateHosts()).containsExactly("ns1.example.foo.tld");
|
||||
|
||||
HostResource exampleTldHost =
|
||||
loadByForeignKey(HostResource.class, "ns1.example.tld", timeAfterCreates).get();
|
||||
DomainBase exampleTldDomain =
|
||||
loadByForeignKey(DomainBase.class, "example.tld", timeAfterCreates).get();
|
||||
assertAboutHosts().that(exampleTldHost).hasSuperordinateDomain(Key.create(exampleTldDomain));
|
||||
assertThat(exampleTldDomain.getSubordinateHosts()).containsExactly("ns1.example.tld");
|
||||
|
||||
assertThatLogoutSucceeds();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
// Copyright 2017 The Nomulus Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package google.registry.flows;
|
||||
|
||||
import static google.registry.model.eppoutput.Result.Code.SUCCESS;
|
||||
import static google.registry.model.eppoutput.Result.Code.SUCCESS_AND_CLOSE;
|
||||
import static google.registry.testing.EppMetricSubject.assertThat;
|
||||
|
||||
import google.registry.testing.AppEngineRule;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.junit.runners.JUnit4;
|
||||
|
||||
/** Tests for login lifecycle. */
|
||||
@RunWith(JUnit4.class)
|
||||
public class EppLifecycleLoginTest extends EppTestCase {
|
||||
|
||||
@Rule
|
||||
public final AppEngineRule appEngine =
|
||||
AppEngineRule.builder().withDatastore().withTaskQueue().build();
|
||||
|
||||
@Test
|
||||
public void testLoginAndLogout_recordsEppMetric() throws Exception {
|
||||
assertThatLoginSucceeds("NewRegistrar", "foo-BAR2");
|
||||
assertThat(getRecordedEppMetric())
|
||||
.hasClientId("NewRegistrar")
|
||||
.and()
|
||||
.hasCommandName("Login")
|
||||
.and()
|
||||
.hasStatus(SUCCESS);
|
||||
assertThatLogoutSucceeds();
|
||||
assertThat(getRecordedEppMetric())
|
||||
.hasClientId("NewRegistrar")
|
||||
.and()
|
||||
.hasCommandName("Logout")
|
||||
.and()
|
||||
.hasStatus(SUCCESS_AND_CLOSE);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
// Copyright 2017 The Nomulus Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package google.registry.flows;
|
||||
|
||||
import static org.joda.time.DateTimeZone.UTC;
|
||||
import static org.joda.time.format.ISODateTimeFormat.dateTimeNoMillis;
|
||||
|
||||
import com.google.common.collect.ImmutableMap;
|
||||
import google.registry.testing.AppEngineRule;
|
||||
import org.joda.time.DateTime;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.junit.runners.JUnit4;
|
||||
|
||||
/** Test flows without login. */
|
||||
@RunWith(JUnit4.class)
|
||||
public class EppLoggedOutTest extends EppTestCase {
|
||||
|
||||
@Rule
|
||||
public final AppEngineRule appEngine = AppEngineRule.builder()
|
||||
.withDatastore()
|
||||
.build();
|
||||
|
||||
@Test
|
||||
public void testHello() throws Exception {
|
||||
DateTime now = DateTime.now(UTC);
|
||||
assertThatCommand("hello.xml", null)
|
||||
.atTime(now)
|
||||
.hasResponse("greeting.xml", ImmutableMap.of("DATE", now.toString(dateTimeNoMillis())));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSyntaxError() throws Exception {
|
||||
assertThatCommand("syntax_error.xml")
|
||||
.hasResponse(
|
||||
"response_error_no_cltrid.xml",
|
||||
ImmutableMap.of(
|
||||
"CODE", "2001",
|
||||
"MSG", "Syntax error at line 4, column 65: cvc-complex-type.3.2.2: "
|
||||
+ "Attribute 'xsi:schemaLocation' is not allowed to appear in element 'epp'."));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,173 @@
|
||||
// Copyright 2017 The Nomulus Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package google.registry.flows;
|
||||
|
||||
import static google.registry.testing.DatastoreHelper.loadRegistrar;
|
||||
import static google.registry.testing.DatastoreHelper.persistResource;
|
||||
import static org.joda.time.DateTimeZone.UTC;
|
||||
|
||||
import com.google.common.collect.ImmutableMap;
|
||||
import google.registry.testing.AppEngineRule;
|
||||
import google.registry.testing.CertificateSamples;
|
||||
import java.util.Optional;
|
||||
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;
|
||||
|
||||
/** Test logging in with TLS credentials. */
|
||||
@RunWith(JUnit4.class)
|
||||
public class EppLoginTlsTest extends EppTestCase {
|
||||
|
||||
@Rule public final AppEngineRule appEngine = AppEngineRule.builder().withDatastore().build();
|
||||
|
||||
void setClientCertificateHash(String clientCertificateHash) {
|
||||
setTransportCredentials(
|
||||
new TlsCredentials(true, clientCertificateHash, Optional.of("192.168.1.100:54321")));
|
||||
}
|
||||
|
||||
@Before
|
||||
public void initTest() {
|
||||
persistResource(
|
||||
loadRegistrar("NewRegistrar")
|
||||
.asBuilder()
|
||||
.setClientCertificateHash(CertificateSamples.SAMPLE_CERT_HASH)
|
||||
.build());
|
||||
// Set a cert for the second registrar, or else any cert will be allowed for login.
|
||||
persistResource(
|
||||
loadRegistrar("TheRegistrar")
|
||||
.asBuilder()
|
||||
.setClientCertificateHash(CertificateSamples.SAMPLE_CERT2_HASH)
|
||||
.build());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testLoginLogout() throws Exception {
|
||||
setClientCertificateHash(CertificateSamples.SAMPLE_CERT_HASH);
|
||||
assertThatLoginSucceeds("NewRegistrar", "foo-BAR2");
|
||||
assertThatLogoutSucceeds();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testLogin_wrongPasswordFails() throws Exception {
|
||||
setClientCertificateHash(CertificateSamples.SAMPLE_CERT_HASH);
|
||||
// For TLS login, we also check the epp xml password.
|
||||
assertThatLogin("NewRegistrar", "incorrect")
|
||||
.hasResponse(
|
||||
"response_error.xml",
|
||||
ImmutableMap.of("CODE", "2200", "MSG", "Registrar password is incorrect"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testMultiLogin() throws Exception {
|
||||
setClientCertificateHash(CertificateSamples.SAMPLE_CERT_HASH);
|
||||
assertThatLoginSucceeds("NewRegistrar", "foo-BAR2");
|
||||
assertThatLogoutSucceeds();
|
||||
assertThatLoginSucceeds("NewRegistrar", "foo-BAR2");
|
||||
assertThatLogoutSucceeds();
|
||||
assertThatLogin("TheRegistrar", "password2")
|
||||
.hasResponse(
|
||||
"response_error.xml",
|
||||
ImmutableMap.of(
|
||||
"CODE", "2200", "MSG", "Registrar certificate does not match stored certificate"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testNonAuthedLogin_fails() throws Exception {
|
||||
setClientCertificateHash(CertificateSamples.SAMPLE_CERT_HASH);
|
||||
assertThatLogin("TheRegistrar", "password2")
|
||||
.hasResponse(
|
||||
"response_error.xml",
|
||||
ImmutableMap.of(
|
||||
"CODE", "2200", "MSG", "Registrar certificate does not match stored certificate"));
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
public void testBadCertificate_failsBadCertificate2200() throws Exception {
|
||||
setClientCertificateHash("laffo");
|
||||
assertThatLogin("NewRegistrar", "foo-BAR2")
|
||||
.hasResponse(
|
||||
"response_error.xml",
|
||||
ImmutableMap.of(
|
||||
"CODE", "2200", "MSG", "Registrar certificate does not match stored certificate"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGfeDidntProvideClientCertificate_failsMissingCertificate2200() throws Exception {
|
||||
setClientCertificateHash("");
|
||||
assertThatLogin("NewRegistrar", "foo-BAR2")
|
||||
.hasResponse(
|
||||
"response_error.xml",
|
||||
ImmutableMap.of("CODE", "2200", "MSG", "Registrar certificate not present"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGoodPrimaryCertificate() throws Exception {
|
||||
setClientCertificateHash(CertificateSamples.SAMPLE_CERT_HASH);
|
||||
DateTime now = DateTime.now(UTC);
|
||||
persistResource(
|
||||
loadRegistrar("NewRegistrar")
|
||||
.asBuilder()
|
||||
.setClientCertificate(CertificateSamples.SAMPLE_CERT, now)
|
||||
.setFailoverClientCertificate(CertificateSamples.SAMPLE_CERT2, now)
|
||||
.build());
|
||||
assertThatLoginSucceeds("NewRegistrar", "foo-BAR2");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGoodFailoverCertificate() throws Exception {
|
||||
setClientCertificateHash(CertificateSamples.SAMPLE_CERT2_HASH);
|
||||
DateTime now = DateTime.now(UTC);
|
||||
persistResource(
|
||||
loadRegistrar("NewRegistrar")
|
||||
.asBuilder()
|
||||
.setClientCertificate(CertificateSamples.SAMPLE_CERT, now)
|
||||
.setFailoverClientCertificate(CertificateSamples.SAMPLE_CERT2, now)
|
||||
.build());
|
||||
assertThatLoginSucceeds("NewRegistrar", "foo-BAR2");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testMissingPrimaryCertificateButHasFailover_usesFailover() throws Exception {
|
||||
setClientCertificateHash(CertificateSamples.SAMPLE_CERT2_HASH);
|
||||
DateTime now = DateTime.now(UTC);
|
||||
persistResource(
|
||||
loadRegistrar("NewRegistrar")
|
||||
.asBuilder()
|
||||
.setClientCertificate(null, now)
|
||||
.setFailoverClientCertificate(CertificateSamples.SAMPLE_CERT2, now)
|
||||
.build());
|
||||
assertThatLoginSucceeds("NewRegistrar", "foo-BAR2");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRegistrarHasNoCertificatesOnFile_fails() throws Exception {
|
||||
setClientCertificateHash("laffo");
|
||||
DateTime now = DateTime.now(UTC);
|
||||
persistResource(
|
||||
loadRegistrar("NewRegistrar")
|
||||
.asBuilder()
|
||||
.setClientCertificate(null, now)
|
||||
.setFailoverClientCertificate(null, now)
|
||||
.build());
|
||||
assertThatLogin("NewRegistrar", "foo-BAR2")
|
||||
.hasResponse(
|
||||
"response_error.xml",
|
||||
ImmutableMap.of("CODE", "2200", "MSG", "Registrar certificate is not configured"));
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user