Refactor to be more in line with a standard Gradle project structure

This commit is contained in:
Gus Brodman 2019-05-21 14:12:47 -04:00
parent 8fa45e8c76
commit a7a983bfed
3141 changed files with 99 additions and 100 deletions

View file

@ -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"],
)

View file

@ -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);
}
}

View file

@ -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()));
}
}

View file

@ -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);
}
}

View file

@ -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))));
}
}

View file

@ -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();
}
}

View file

@ -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());
}
}
}

View file

@ -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");
}
}

View file

@ -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();
}
}

View file

@ -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"],
)

View file

@ -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);
}
}

View file

@ -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();
}
}
}

View file

@ -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.
}
}

View file

@ -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);
}
}

View file

@ -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);
}
}

View file

@ -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))));
}
}

View file

@ -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"],
)

View file

@ -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}");
}
}

View file

@ -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"],
)

View file

@ -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");
}
}

View file

@ -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"));
}
}

View file

@ -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);
}
}

View file

@ -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

View file

@ -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"],
)

View file

@ -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"));
}
}

View file

@ -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"],
)

View file

@ -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.
}
}

View file

@ -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));
}
}

View file

@ -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();
}
}

View file

@ -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",
},
)

View file

@ -0,0 +1 @@
hello

View file

@ -0,0 +1 @@
OMG IM AN OVERRIDE

View file

@ -0,0 +1 @@
world

View file

@ -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),
},
)

View file

@ -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"],
)

View file

@ -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.");
}
}

View file

@ -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();
}
}

View file

@ -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"],
)

View file

@ -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);
}
}

View file

@ -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);
}
}

View file

@ -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"],
)

View file

@ -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");
}
}

View file

@ -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");
}
}

View file

@ -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();
}

View file

@ -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);
}
}

View file

@ -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"));
}
}

View file

@ -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"));
}
}

View file

@ -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);
}
}

View file

@ -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"],
)

View file

@ -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();
}
}

View file

@ -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"],
)

View file

@ -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();
}
}

View file

@ -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;
}
}

View file

@ -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();
}
}

View file

@ -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);
}
}
}

View file

@ -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 {&#64;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();
}
}

View file

@ -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. &lt; &gt; &amp; &squot; &quot;"))
.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() {}
}

View file

@ -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"],
)

View file

@ -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");
}
}

View file

@ -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");
}
}

View file

@ -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");
}
}

View file

@ -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());
}
}

View file

@ -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);
}
}

View file

@ -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);
}
}

View file

@ -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");
}
}

View file

@ -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");
}
}

View file

@ -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();
}
}

View file

@ -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");
}
}

View file

@ -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");
}
}

View file

@ -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

View file

@ -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"],
)

View file

@ -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()));
}
}

View file

@ -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);
}
}

View file

@ -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);
}
}

View file

@ -0,0 +1,5 @@
{
"startTime": "2018-10-29T16:01:04.645299Z",
"operationType": "EXPORT_ENTITIES",
"state": "SUCCESSFUL"
}

View file

@ -0,0 +1,7 @@
{
"kinds": [
"Registry",
"Registrar",
"DomainBase"
]
}

View file

@ -0,0 +1,6 @@
{
"entityFilter": {
"kinds": ["Registry", "Registrar", "DomainBase"]
},
"outputUrlPrefix": "gs://mybucket/path"
}

View file

@ -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"
}

View file

@ -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"
}
}

View file

@ -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"
}
}
]
}

View file

@ -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"
}

View file

@ -0,0 +1,4 @@
{
"workCompleted": "51797",
"workEstimated": "54513"
}

View file

@ -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

View file

@ -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"],
)

View file

@ -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);
}
}

View file

@ -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);
}
}

View file

@ -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", "{}");
}
}

View file

@ -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"
}
}

View file

@ -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
}

View file

@ -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"
}

View file

@ -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"],
)

View file

@ -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);
}
}

View file

@ -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();
}
}

View file

@ -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);
}
}

View file

@ -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

View file

@ -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();
}
}

View file

@ -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);
}
}

View file

@ -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'."));
}
}

View file

@ -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