mirror of
https://github.com/google/nomulus
synced 2026-08-02 21:36:08 +00:00
Compare commits
21
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
73ba96a5d4 | ||
|
|
90db60643e | ||
|
|
98283a67ac | ||
|
|
e70f14001c | ||
|
|
22d3612be3 | ||
|
|
ad8bc05877 | ||
|
|
a3537447ef | ||
|
|
4e66fed497 | ||
|
|
886cdfa39b | ||
|
|
beefa9364b | ||
|
|
73210e4b09 | ||
|
|
08cec96a93 | ||
|
|
31ef402c50 | ||
|
|
e89cc4406a | ||
|
|
48de5d8375 | ||
|
|
59abc1d154 | ||
|
|
6794c6fbd7 | ||
|
|
0c384adc22 | ||
|
|
3b679058b0 | ||
|
|
9b5805f145 | ||
|
|
9e6f99face |
+11
-1
@@ -172,6 +172,8 @@ dependencies {
|
||||
|
||||
compile deps['com.beust:jcommander']
|
||||
compile deps['com.google.api:gax']
|
||||
compile deps['com.google.api.grpc:proto-google-cloud-datastore-v1']
|
||||
compile deps['com.google.api.grpc:proto-google-common-protos']
|
||||
compile deps['com.google.api.grpc:proto-google-cloud-secretmanager-v1']
|
||||
compile deps['com.google.api-client:google-api-client']
|
||||
compile deps['com.google.api-client:google-api-client-appengine']
|
||||
@@ -196,6 +198,8 @@ dependencies {
|
||||
compile deps['com.google.appengine:appengine-remote-api']
|
||||
compile deps['com.google.auth:google-auth-library-credentials']
|
||||
compile deps['com.google.auth:google-auth-library-oauth2-http']
|
||||
compile deps['com.google.cloud.bigdataoss:util']
|
||||
compile deps['com.google.cloud.datastore:datastore-v1-proto-client']
|
||||
compile deps['com.google.cloud.sql:jdbc-socket-factory-core']
|
||||
runtimeOnly deps['com.google.cloud.sql:postgres-socket-factory']
|
||||
compile deps['com.google.cloud:google-cloud-secretmanager']
|
||||
@@ -600,7 +604,6 @@ task compileProdJS(type: JavaExec) {
|
||||
closureArgs << "--js=${jsDir}/soyutils_usegoog.js"
|
||||
closureArgs << "--js=${cssSourceDir}/registrar_bin.css.js"
|
||||
closureArgs << "--js=${jsSourceDir}/**.js"
|
||||
// TODO(shicong) Verify the compiled JS file works in Alpha
|
||||
closureArgs << "--js=${externsDir}/json.js"
|
||||
closureArgs << "--js=${soySourceDir}/**.js"
|
||||
args closureArgs
|
||||
@@ -737,6 +740,13 @@ project.tasks.create('initSqlPipeline', JavaExec) {
|
||||
}
|
||||
}
|
||||
|
||||
// Caller must provide projectId, GCP region, runner, and the kinds to delete
|
||||
// (comma-separated kind names or '*' for all). E.g.:
|
||||
// nom_build :core:bulkDeleteDatastore --args="--project=domain-registry-crash \
|
||||
// --region=us-central1 --runner=DataflowRunner --kindsToDelete=*"
|
||||
createToolTask(
|
||||
'bulkDeleteDatastore', 'google.registry.beam.datastore.BulkDeletePipeline')
|
||||
|
||||
project.tasks.create('generateSqlSchema', JavaExec) {
|
||||
classpath = sourceSets.nonprod.runtimeClasspath
|
||||
main = 'google.registry.tools.DevTool'
|
||||
|
||||
@@ -85,7 +85,7 @@ public class ReplayCommitLogsToSqlAction implements Runnable {
|
||||
Optional<Lock> lock =
|
||||
Lock.acquire(
|
||||
this.getClass().getSimpleName(), null, LEASE_LENGTH, requestStatusChecker, false);
|
||||
if (lock.isEmpty()) {
|
||||
if (!lock.isPresent()) {
|
||||
String message = "Can't acquire SQL commit log replay lock, aborting.";
|
||||
logger.atSevere().log(message);
|
||||
// App Engine will retry on any non-2xx status code, which we don't want in this case.
|
||||
@@ -182,7 +182,7 @@ public class ReplayCommitLogsToSqlAction implements Runnable {
|
||||
}
|
||||
|
||||
private static int compareByWeight(VersionedEntity a, VersionedEntity b) {
|
||||
return getEntityPriority(a.key().getKind(), a.getEntity().isEmpty())
|
||||
- getEntityPriority(b.key().getKind(), b.getEntity().isEmpty());
|
||||
return getEntityPriority(a.key().getKind(), !a.getEntity().isPresent())
|
||||
- getEntityPriority(b.key().getKind(), !b.getEntity().isPresent());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,193 @@
|
||||
// Copyright 2021 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.collect.ImmutableList.toImmutableList;
|
||||
import static com.google.common.net.MediaType.PLAIN_TEXT_UTF_8;
|
||||
import static google.registry.flows.FlowUtils.marshalWithLenientRetry;
|
||||
import static google.registry.model.ofy.ObjectifyService.ofy;
|
||||
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
|
||||
import static google.registry.util.DateTimeUtils.END_OF_TIME;
|
||||
import static google.registry.util.ResourceUtils.readResourceUtf8;
|
||||
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_NO_CONTENT;
|
||||
import static javax.servlet.http.HttpServletResponse.SC_OK;
|
||||
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.google.common.flogger.FluentLogger;
|
||||
import google.registry.config.RegistryConfig.Config;
|
||||
import google.registry.flows.EppController;
|
||||
import google.registry.flows.EppRequestSource;
|
||||
import google.registry.flows.PasswordOnlyTransportCredentials;
|
||||
import google.registry.flows.StatelessRequestSessionMetadata;
|
||||
import google.registry.model.domain.DomainBase;
|
||||
import google.registry.model.eppcommon.ProtocolDefinition;
|
||||
import google.registry.model.eppoutput.EppOutput;
|
||||
import google.registry.request.Action;
|
||||
import google.registry.request.Action.Method;
|
||||
import google.registry.request.Response;
|
||||
import google.registry.request.auth.Auth;
|
||||
import google.registry.request.lock.LockHandler;
|
||||
import google.registry.util.Clock;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.Callable;
|
||||
import javax.inject.Inject;
|
||||
import org.joda.time.DateTime;
|
||||
import org.joda.time.Duration;
|
||||
|
||||
/**
|
||||
* An action that deletes all non-renewing domains whose expiration dates have now passed.
|
||||
*
|
||||
* <p>The registry runs on an autorenew domain model, so domains don't ever expire naturally; they
|
||||
* are only ever autorenewed. However, in some situations (such as URS) we don't want this to
|
||||
* happen. Thus, the domains are tagged as non-renewing and are deleted by the next daily invocation
|
||||
* of this action once they are past the date at which they were to expire.
|
||||
*
|
||||
* <p>Note that this action works by running a superuser EPP domain delete command, and as a side
|
||||
* effect of when domains are deleted (just past their expiration date), they are invariably in the
|
||||
* autorenew grace period when this happens.
|
||||
*/
|
||||
@Action(
|
||||
service = Action.Service.BACKEND,
|
||||
path = DeleteExpiredDomainsAction.PATH,
|
||||
auth = Auth.AUTH_INTERNAL_OR_ADMIN,
|
||||
method = Method.POST)
|
||||
public class DeleteExpiredDomainsAction implements Runnable {
|
||||
|
||||
public static final String PATH = "/_dr/task/deleteExpiredDomains";
|
||||
private static final String LOCK_NAME = "Delete expired domains";
|
||||
|
||||
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
|
||||
private final EppController eppController;
|
||||
private final String registryAdminClientId;
|
||||
private final Clock clock;
|
||||
private final LockHandler lockHandler;
|
||||
private final Response response;
|
||||
private final String deleteXmlTmpl;
|
||||
|
||||
@Inject
|
||||
DeleteExpiredDomainsAction(
|
||||
EppController eppController,
|
||||
@Config("registryAdminClientId") String registryAdminClientId,
|
||||
Clock clock,
|
||||
LockHandler lockHandler,
|
||||
Response response) {
|
||||
this.eppController = eppController;
|
||||
this.registryAdminClientId = registryAdminClientId;
|
||||
this.clock = clock;
|
||||
this.lockHandler = lockHandler;
|
||||
this.response = response;
|
||||
this.deleteXmlTmpl =
|
||||
readResourceUtf8(DeleteExpiredDomainsAction.class, "delete_expired_domain.xml");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
response.setContentType(PLAIN_TEXT_UTF_8);
|
||||
|
||||
Callable<Void> runner =
|
||||
() -> {
|
||||
try {
|
||||
runLocked();
|
||||
response.setStatus(SC_OK);
|
||||
} catch (Exception e) {
|
||||
response.setStatus(SC_INTERNAL_SERVER_ERROR);
|
||||
response.setPayload("Encountered error; see GCP logs for full details.");
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
if (!lockHandler.executeWithLocks(runner, null, Duration.standardHours(1), LOCK_NAME)) {
|
||||
// Send a 200-series status code to prevent this conflicting action from retrying.
|
||||
response.setStatus(SC_NO_CONTENT);
|
||||
response.setPayload("Could not acquire lock; already running?");
|
||||
}
|
||||
}
|
||||
|
||||
private void runLocked() {
|
||||
DateTime runTime = clock.nowUtc();
|
||||
logger.atInfo().log(
|
||||
"Deleting non-renewing domains with autorenew end times up through %s.", runTime);
|
||||
|
||||
// Note: This query is (and must be) non-transactional, and thus, is only eventually consistent.
|
||||
ImmutableList<DomainBase> domainsToDelete =
|
||||
ofy().load().type(DomainBase.class).filter("autorenewEndTime <=", runTime).list().stream()
|
||||
// Datastore can't do two inequalities in one query, so the second happens in-memory.
|
||||
.filter(d -> d.getDeletionTime().isEqual(END_OF_TIME))
|
||||
.collect(toImmutableList());
|
||||
if (domainsToDelete.isEmpty()) {
|
||||
logger.atInfo().log("Found 0 domains to delete.");
|
||||
response.setPayload("Found 0 domains to delete.");
|
||||
return;
|
||||
}
|
||||
|
||||
logger.atInfo().log(
|
||||
"Found %d domains to delete: %s.",
|
||||
domainsToDelete.size(),
|
||||
String.join(
|
||||
", ",
|
||||
domainsToDelete.stream().map(DomainBase::getDomainName).collect(toImmutableList())));
|
||||
domainsToDelete.forEach(this::runDomainDeleteFlow);
|
||||
logger.atInfo().log("Finished deleting domains.");
|
||||
response.setPayload("Finished deleting domains.");
|
||||
}
|
||||
|
||||
private void runDomainDeleteFlow(DomainBase domain) {
|
||||
logger.atInfo().log("Attempting to delete domain %s", domain.getDomainName());
|
||||
// Create a new transaction that the flow's execution will be enlisted in that loads the domain
|
||||
// transactionally. This way we can ensure that nothing else has modified the domain in question
|
||||
// in the intervening period since the query above found it.
|
||||
Optional<EppOutput> eppOutput =
|
||||
tm().transact(
|
||||
() -> {
|
||||
DomainBase transDomain = tm().loadByKey(domain.createVKey());
|
||||
if (!domain.getAutorenewEndTime().isPresent()
|
||||
|| domain.getAutorenewEndTime().get().isAfter(tm().getTransactionTime())) {
|
||||
logger.atSevere().log(
|
||||
"Failed to delete domain %s because of its autorenew end time: %s.",
|
||||
transDomain.getDomainName(), transDomain.getAutorenewEndTime());
|
||||
return Optional.empty();
|
||||
} else if (domain.getDeletionTime().isBefore(END_OF_TIME)) {
|
||||
logger.atSevere().log(
|
||||
"Failed to delete domain %s because it was already deleted on %s.",
|
||||
transDomain.getDomainName(), transDomain.getDeletionTime());
|
||||
return Optional.empty();
|
||||
}
|
||||
return Optional.of(
|
||||
eppController.handleEppCommand(
|
||||
new StatelessRequestSessionMetadata(
|
||||
registryAdminClientId,
|
||||
ProtocolDefinition.getVisibleServiceExtensionUris()),
|
||||
new PasswordOnlyTransportCredentials(),
|
||||
EppRequestSource.BACKEND,
|
||||
false,
|
||||
true,
|
||||
deleteXmlTmpl
|
||||
.replace("%DOMAIN%", transDomain.getDomainName())
|
||||
.getBytes(UTF_8)));
|
||||
});
|
||||
|
||||
if (eppOutput.isPresent()) {
|
||||
if (eppOutput.get().isSuccess()) {
|
||||
logger.atInfo().log("Successfully deleted domain %s", domain.getDomainName());
|
||||
} else {
|
||||
logger.atWarning().log(
|
||||
"Failed to delete domain %s; EPP response:\n\n%s",
|
||||
domain.getDomainName(), new String(marshalWithLenientRetry(eppOutput.get()), UTF_8));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
<epp xmlns="urn:ietf:params:xml:ns:epp-1.0">
|
||||
<command>
|
||||
<delete>
|
||||
<domain:delete
|
||||
xmlns:domain="urn:ietf:params:xml:ns:domain-1.0">
|
||||
<domain:name>%DOMAIN%</domain:name>
|
||||
</domain:delete>
|
||||
</delete>
|
||||
<extension>
|
||||
<metadata:metadata xmlns:metadata="urn:google:params:xml:ns:metadata-1.0">
|
||||
<metadata:reason>Non-renewing domain has reached expiration date.</metadata:reason>
|
||||
<metadata:requestedByRegistrar>false</metadata:requestedByRegistrar>
|
||||
</metadata:metadata>
|
||||
</extension>
|
||||
<clTRID>ABC-12345</clTRID>
|
||||
</command>
|
||||
</epp>
|
||||
@@ -0,0 +1,330 @@
|
||||
// Copyright 2020 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.datastore;
|
||||
|
||||
import static com.google.common.base.Preconditions.checkState;
|
||||
import static org.apache.beam.sdk.values.TypeDescriptors.kvs;
|
||||
import static org.apache.beam.sdk.values.TypeDescriptors.strings;
|
||||
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
import com.google.common.base.Splitter;
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
import com.google.common.collect.ImmutableSortedSet;
|
||||
import com.google.common.flogger.FluentLogger;
|
||||
import com.google.datastore.v1.Entity;
|
||||
import java.util.Iterator;
|
||||
import java.util.Map;
|
||||
import org.apache.beam.sdk.Pipeline;
|
||||
import org.apache.beam.sdk.extensions.gcp.options.GcpOptions;
|
||||
import org.apache.beam.sdk.io.gcp.datastore.DatastoreIO;
|
||||
import org.apache.beam.sdk.options.Default;
|
||||
import org.apache.beam.sdk.options.Description;
|
||||
import org.apache.beam.sdk.options.PipelineOptionsFactory;
|
||||
import org.apache.beam.sdk.options.Validation;
|
||||
import org.apache.beam.sdk.transforms.Create;
|
||||
import org.apache.beam.sdk.transforms.DoFn;
|
||||
import org.apache.beam.sdk.transforms.GroupByKey;
|
||||
import org.apache.beam.sdk.transforms.MapElements;
|
||||
import org.apache.beam.sdk.transforms.PTransform;
|
||||
import org.apache.beam.sdk.transforms.ParDo;
|
||||
import org.apache.beam.sdk.transforms.Reshuffle;
|
||||
import org.apache.beam.sdk.transforms.View;
|
||||
import org.apache.beam.sdk.values.KV;
|
||||
import org.apache.beam.sdk.values.PBegin;
|
||||
import org.apache.beam.sdk.values.PCollection;
|
||||
import org.apache.beam.sdk.values.PCollectionTuple;
|
||||
import org.apache.beam.sdk.values.PCollectionView;
|
||||
import org.apache.beam.sdk.values.TupleTag;
|
||||
import org.apache.beam.sdk.values.TupleTagList;
|
||||
|
||||
/**
|
||||
* A BEAM pipeline that deletes Datastore entities in bulk.
|
||||
*
|
||||
* <p>This pipeline provides an alternative to the <a
|
||||
* href="https://cloud.google.com/datastore/docs/bulk-delete">GCP builtin template</a> that performs
|
||||
* the same task. It solves the following performance and usability problems in the builtin
|
||||
* template:
|
||||
*
|
||||
* <ul>
|
||||
* <li>When deleting all data (by using the {@code select __key__} or {@code select *} queries),
|
||||
* the builtin template cannot parallelize the query, therefore has to query with a single
|
||||
* worker.
|
||||
* <li>When deleting all data, the builtin template also attempts to delete Datastore internal
|
||||
* tables which would cause permission-denied errors, which in turn MAY cause the pipeline to
|
||||
* abort before all data has been deleted.
|
||||
* <li>With the builtin template, it is possible to delete multiple entity types in one pipeline
|
||||
* ONLY if the user can come up with a single literal query that covers all of them. This is
|
||||
* not the case with most Nomulus entity types.
|
||||
* </ul>
|
||||
*
|
||||
* <p>A user of this pipeline must specify the types of entities to delete using the {@code
|
||||
* --kindsToDelete} command line argument. To delete specific entity types, give a comma-separated
|
||||
* string of their kind names; to delete all data, give {@code "*"}.
|
||||
*
|
||||
* <p>When deleting all data, it is recommended for the user to specify the number of user entity
|
||||
* types in the Datastore using the {@code --numOfKindsHint} argument. If the default value for this
|
||||
* parameter is too low, performance will suffer.
|
||||
*/
|
||||
public class BulkDeletePipeline {
|
||||
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
|
||||
|
||||
// This tool is not for use in our critical projects.
|
||||
private static final ImmutableSet<String> FORBIDDEN_PROJECTS =
|
||||
ImmutableSet.of("domain-registry", "domain-registry-sandbox");
|
||||
|
||||
private final BulkDeletePipelineOptions options;
|
||||
|
||||
private final Pipeline pipeline;
|
||||
|
||||
BulkDeletePipeline(BulkDeletePipelineOptions options) {
|
||||
this.options = options;
|
||||
pipeline = Pipeline.create(options);
|
||||
}
|
||||
|
||||
public void run() {
|
||||
setupPipeline();
|
||||
pipeline.run();
|
||||
}
|
||||
|
||||
@SuppressWarnings("deprecation") // org.apache.beam.sdk.transforms.Reshuffle
|
||||
private void setupPipeline() {
|
||||
checkState(
|
||||
!FORBIDDEN_PROJECTS.contains(options.getProject()),
|
||||
"Bulk delete is forbidden in %s",
|
||||
options.getProject());
|
||||
|
||||
// Pre-allocated tags to label entities by kind. In the case of delete-all, we must use a guess.
|
||||
TupleTagList deletionTags;
|
||||
PCollection<String> kindsToDelete;
|
||||
|
||||
if (options.getKindsToDelete().equals("*")) {
|
||||
deletionTags = getDeletionTags(options.getNumOfKindsHint());
|
||||
kindsToDelete =
|
||||
pipeline.apply("DiscoverEntityKinds", discoverEntityKinds(options.getProject()));
|
||||
} else {
|
||||
ImmutableList<String> kindsToDeleteParam = parseKindsToDelete(options);
|
||||
checkState(
|
||||
!kindsToDeleteParam.contains("*"),
|
||||
"The --kindsToDelete argument should not contain both '*' and other kinds.");
|
||||
deletionTags = getDeletionTags(kindsToDeleteParam.size());
|
||||
kindsToDelete = pipeline.apply("UseProvidedKinds", Create.of(kindsToDeleteParam));
|
||||
}
|
||||
|
||||
// Map each kind to a tag. The "SplitByKind" stage below will group entities by kind using
|
||||
// this mapping. In practice, this has been effective at avoiding entity group contentions.
|
||||
PCollectionView<Map<String, TupleTag<Entity>>> kindToTagMapping =
|
||||
mapKindsToDeletionTags(kindsToDelete, deletionTags).apply("GetKindsToTagMap", View.asMap());
|
||||
|
||||
PCollectionTuple entities =
|
||||
kindsToDelete
|
||||
.apply("GenerateQueries", ParDo.of(new GenerateQueries()))
|
||||
.apply("ReadEntities", DatastoreV1.read().withProjectId(options.getProject()))
|
||||
.apply(
|
||||
"SplitByKind",
|
||||
ParDo.of(new SplitEntities(kindToTagMapping))
|
||||
.withSideInputs(kindToTagMapping)
|
||||
.withOutputTags(getOneDeletionTag("placeholder"), deletionTags));
|
||||
|
||||
for (TupleTag<?> tag : deletionTags.getAll()) {
|
||||
entities
|
||||
.get((TupleTag<Entity>) tag)
|
||||
// Reshuffle calls GroupByKey which is one way to trigger load rebalance in the pipeline.
|
||||
// Using the deprecated "Reshuffle" for convenience given the short life of this tool.
|
||||
.apply("RebalanceLoad", Reshuffle.viaRandomKey())
|
||||
.apply(
|
||||
"DeleteEntities_" + tag.getId(),
|
||||
DatastoreIO.v1().deleteEntity().withProjectId(options.getProject()));
|
||||
}
|
||||
}
|
||||
|
||||
private static String toKeyOnlyQueryForKind(String kind) {
|
||||
return "select __key__ from `" + kind + "`";
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a {@link TupleTag} that retains the generic type parameter and may be used in a
|
||||
* multi-output {@link ParDo} (e.g. {@link SplitEntities}).
|
||||
*
|
||||
* <p>This method is NOT needed in tests when creating tags for assertions. Simply create them
|
||||
* with {@code new TupleTag<Entity>(String)}.
|
||||
*/
|
||||
@VisibleForTesting
|
||||
static TupleTag<Entity> getOneDeletionTag(String id) {
|
||||
// The trailing {} is needed to retain generic param type.
|
||||
return new TupleTag<Entity>(id) {};
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
static ImmutableList<String> parseKindsToDelete(BulkDeletePipelineOptions options) {
|
||||
return ImmutableList.copyOf(
|
||||
Splitter.on(",").omitEmptyStrings().trimResults().split(options.getKindsToDelete().trim()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a list of {@code n} {@link TupleTag TupleTags} numbered from {@code 0} to {@code n-1}.
|
||||
*/
|
||||
@VisibleForTesting
|
||||
static TupleTagList getDeletionTags(int n) {
|
||||
ImmutableList.Builder<TupleTag<?>> builder = new ImmutableList.Builder<>();
|
||||
for (int i = 0; i < n; i++) {
|
||||
builder.add(getOneDeletionTag(String.valueOf(i)));
|
||||
}
|
||||
return TupleTagList.of(builder.build());
|
||||
}
|
||||
|
||||
/** Returns a {@link PTransform} that finds all entity kinds in Datastore. */
|
||||
@VisibleForTesting
|
||||
static PTransform<PBegin, PCollection<String>> discoverEntityKinds(String project) {
|
||||
return new PTransform<PBegin, PCollection<String>>() {
|
||||
@Override
|
||||
public PCollection<String> expand(PBegin input) {
|
||||
// Use the __kind__ table to discover entity kinds. Data in the more informational
|
||||
// __Stat_Kind__ table may be up to 48-hour stale.
|
||||
return input
|
||||
.apply(
|
||||
"LoadEntityMetaData",
|
||||
DatastoreIO.v1()
|
||||
.read()
|
||||
.withProjectId(project)
|
||||
.withLiteralGqlQuery("select * from __kind__"))
|
||||
.apply(
|
||||
"GetKindNames",
|
||||
ParDo.of(
|
||||
new DoFn<Entity, String>() {
|
||||
@ProcessElement
|
||||
public void processElement(
|
||||
@Element Entity entity, OutputReceiver<String> out) {
|
||||
String kind = entity.getKey().getPath(0).getName();
|
||||
if (kind.startsWith("_")) {
|
||||
return;
|
||||
}
|
||||
out.output(kind);
|
||||
}
|
||||
}));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
static PCollection<KV<String, TupleTag<Entity>>> mapKindsToDeletionTags(
|
||||
PCollection<String> kinds, TupleTagList tags) {
|
||||
// The first two stages send all strings in the 'kinds' PCollection to one worker which
|
||||
// performs the mapping in the last stage.
|
||||
return kinds
|
||||
.apply(
|
||||
"AssignSingletonKeyToKinds",
|
||||
MapElements.into(kvs(strings(), strings())).via(kind -> KV.of("", kind)))
|
||||
.apply("GatherKindsIntoCollection", GroupByKey.create())
|
||||
.apply("MapKindsToTag", ParDo.of(new MapKindsToTags(tags)));
|
||||
}
|
||||
|
||||
/** Transforms each {@code kind} string into a Datastore query for that kind. */
|
||||
@VisibleForTesting
|
||||
static class GenerateQueries extends DoFn<String, String> {
|
||||
@ProcessElement
|
||||
public void processElement(@Element String kind, OutputReceiver<String> out) {
|
||||
out.output(toKeyOnlyQueryForKind(kind));
|
||||
}
|
||||
}
|
||||
|
||||
private static class MapKindsToTags
|
||||
extends DoFn<KV<String, Iterable<String>>, KV<String, TupleTag<Entity>>> {
|
||||
private final TupleTagList tupleTags;
|
||||
|
||||
MapKindsToTags(TupleTagList tupleTags) {
|
||||
this.tupleTags = tupleTags;
|
||||
}
|
||||
|
||||
@ProcessElement
|
||||
public void processElement(
|
||||
@Element KV<String, Iterable<String>> kv,
|
||||
OutputReceiver<KV<String, TupleTag<Entity>>> out) {
|
||||
// Sort kinds so that mapping is deterministic.
|
||||
ImmutableSortedSet<String> sortedKinds = ImmutableSortedSet.copyOf(kv.getValue());
|
||||
Iterator<String> kinds = sortedKinds.iterator();
|
||||
Iterator<TupleTag<?>> tags = tupleTags.getAll().iterator();
|
||||
|
||||
while (kinds.hasNext() && tags.hasNext()) {
|
||||
out.output(KV.of(kinds.next(), (TupleTag<Entity>) tags.next()));
|
||||
}
|
||||
|
||||
if (kinds.hasNext()) {
|
||||
logger.atWarning().log(
|
||||
"There are more kinds to delete (%s) than our estimate (%s). "
|
||||
+ "Performance may suffer.",
|
||||
sortedKinds.size(), tupleTags.size());
|
||||
}
|
||||
// Round robin assignment so that mapping is deterministic
|
||||
while (kinds.hasNext()) {
|
||||
tags = tupleTags.getAll().iterator();
|
||||
while (kinds.hasNext() && tags.hasNext()) {
|
||||
out.output(KV.of(kinds.next(), (TupleTag<Entity>) tags.next()));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@link DoFn} that splits one {@link PCollection} of mixed kinds into multiple single-kind
|
||||
* {@code PCollections}.
|
||||
*/
|
||||
@VisibleForTesting
|
||||
static class SplitEntities extends DoFn<Entity, Entity> {
|
||||
private final PCollectionView<Map<String, TupleTag<Entity>>> kindToTagMapping;
|
||||
|
||||
SplitEntities(PCollectionView<Map<String, TupleTag<Entity>>> kindToTagMapping) {
|
||||
super();
|
||||
this.kindToTagMapping = kindToTagMapping;
|
||||
}
|
||||
|
||||
@ProcessElement
|
||||
public void processElement(ProcessContext context) {
|
||||
Entity entity = context.element();
|
||||
com.google.datastore.v1.Key entityKey = entity.getKey();
|
||||
String kind = entityKey.getPath(entityKey.getPathCount() - 1).getKind();
|
||||
TupleTag<Entity> tag = context.sideInput(kindToTagMapping).get(kind);
|
||||
context.output(tag, entity);
|
||||
}
|
||||
}
|
||||
|
||||
public static void main(String[] args) {
|
||||
BulkDeletePipelineOptions options =
|
||||
PipelineOptionsFactory.fromArgs(args).withValidation().as(BulkDeletePipelineOptions.class);
|
||||
BulkDeletePipeline pipeline = new BulkDeletePipeline(options);
|
||||
pipeline.run();
|
||||
System.exit(0);
|
||||
}
|
||||
|
||||
public interface BulkDeletePipelineOptions extends GcpOptions {
|
||||
|
||||
@Description(
|
||||
"The Datastore KINDs to be deleted. The format may be:\n"
|
||||
+ "\t- The list of kinds to be deleted as a comma-separated string, or\n"
|
||||
+ "\t- '*', which causes all kinds to be deleted.")
|
||||
@Validation.Required
|
||||
String getKindsToDelete();
|
||||
|
||||
void setKindsToDelete(String kinds);
|
||||
|
||||
@Description(
|
||||
"An estimate of the number of KINDs to be deleted. "
|
||||
+ "This is recommended if --kindsToDelete is '*' and the default value is too low.")
|
||||
@Default.Integer(30)
|
||||
int getNumOfKindsHint();
|
||||
|
||||
void setNumOfKindsHint(int numOfKindsHint);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,765 @@
|
||||
// Copyright 2020 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 class is adapted from the Apache BEAM SDK. The original license may
|
||||
// be found at <a href="https://github.com/apache/beam/blob/master/LICENSE">
|
||||
// this link</a>.
|
||||
|
||||
package google.registry.beam.datastore;
|
||||
|
||||
import static com.google.common.base.Preconditions.checkArgument;
|
||||
import static com.google.common.base.Verify.verify;
|
||||
import static com.google.datastore.v1.PropertyFilter.Operator.EQUAL;
|
||||
import static com.google.datastore.v1.PropertyOrder.Direction.DESCENDING;
|
||||
import static com.google.datastore.v1.QueryResultBatch.MoreResultsType.NOT_FINISHED;
|
||||
import static com.google.datastore.v1.client.DatastoreHelper.makeAndFilter;
|
||||
import static com.google.datastore.v1.client.DatastoreHelper.makeFilter;
|
||||
import static com.google.datastore.v1.client.DatastoreHelper.makeOrder;
|
||||
import static com.google.datastore.v1.client.DatastoreHelper.makeValue;
|
||||
|
||||
import com.google.api.client.http.HttpRequestInitializer;
|
||||
import com.google.auth.Credentials;
|
||||
import com.google.auth.http.HttpCredentialsAdapter;
|
||||
import com.google.auto.value.AutoValue;
|
||||
import com.google.cloud.hadoop.util.ChainingHttpRequestInitializer;
|
||||
import com.google.common.base.Strings;
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
import com.google.common.flogger.FluentLogger;
|
||||
import com.google.datastore.v1.Entity;
|
||||
import com.google.datastore.v1.EntityResult;
|
||||
import com.google.datastore.v1.GqlQuery;
|
||||
import com.google.datastore.v1.PartitionId;
|
||||
import com.google.datastore.v1.Query;
|
||||
import com.google.datastore.v1.QueryResultBatch;
|
||||
import com.google.datastore.v1.RunQueryRequest;
|
||||
import com.google.datastore.v1.RunQueryResponse;
|
||||
import com.google.datastore.v1.client.Datastore;
|
||||
import com.google.datastore.v1.client.DatastoreException;
|
||||
import com.google.datastore.v1.client.DatastoreFactory;
|
||||
import com.google.datastore.v1.client.DatastoreHelper;
|
||||
import com.google.datastore.v1.client.DatastoreOptions;
|
||||
import com.google.datastore.v1.client.QuerySplitter;
|
||||
import com.google.protobuf.Int32Value;
|
||||
import com.google.rpc.Code;
|
||||
import java.io.Serializable;
|
||||
import java.util.List;
|
||||
import java.util.NoSuchElementException;
|
||||
import javax.annotation.Nullable;
|
||||
import org.apache.beam.sdk.extensions.gcp.options.GcpOptions;
|
||||
import org.apache.beam.sdk.extensions.gcp.util.RetryHttpRequestInitializer;
|
||||
import org.apache.beam.sdk.metrics.Counter;
|
||||
import org.apache.beam.sdk.metrics.Metrics;
|
||||
import org.apache.beam.sdk.options.PipelineOptions;
|
||||
import org.apache.beam.sdk.transforms.DoFn;
|
||||
import org.apache.beam.sdk.transforms.PTransform;
|
||||
import org.apache.beam.sdk.transforms.ParDo;
|
||||
import org.apache.beam.sdk.transforms.Reshuffle;
|
||||
import org.apache.beam.sdk.transforms.display.DisplayData;
|
||||
import org.apache.beam.sdk.transforms.display.HasDisplayData;
|
||||
import org.apache.beam.sdk.util.BackOff;
|
||||
import org.apache.beam.sdk.util.BackOffUtils;
|
||||
import org.apache.beam.sdk.util.FluentBackoff;
|
||||
import org.apache.beam.sdk.util.Sleeper;
|
||||
import org.apache.beam.sdk.values.KV;
|
||||
import org.apache.beam.sdk.values.PCollection;
|
||||
import org.joda.time.Duration;
|
||||
|
||||
/**
|
||||
* Contains an adaptation of {@link org.apache.beam.sdk.io.gcp.datastore.DatastoreV1.Read}. See
|
||||
* {@link MultiRead} for details.
|
||||
*/
|
||||
public class DatastoreV1 {
|
||||
|
||||
// A package-private constructor to prevent direct instantiation from outside of this package
|
||||
DatastoreV1() {}
|
||||
|
||||
/**
|
||||
* Non-retryable errors. See https://cloud.google.com/datastore/docs/concepts/errors#Error_Codes .
|
||||
*/
|
||||
private static final ImmutableSet<Code> NON_RETRYABLE_ERRORS =
|
||||
ImmutableSet.of(
|
||||
Code.FAILED_PRECONDITION,
|
||||
Code.INVALID_ARGUMENT,
|
||||
Code.PERMISSION_DENIED,
|
||||
Code.UNAUTHENTICATED);
|
||||
|
||||
/**
|
||||
* Returns an empty {@link MultiRead} builder. Configure the source {@code projectId}, {@code
|
||||
* query}, and optionally {@code namespace} and {@code numQuerySplits} using {@link
|
||||
* MultiRead#withProjectId}, {@link MultiRead#withNamespace}, {@link
|
||||
* MultiRead#withNumQuerySplits}.
|
||||
*/
|
||||
public static MultiRead read() {
|
||||
return new AutoValue_DatastoreV1_MultiRead.Builder().setNumQuerySplits(0).build();
|
||||
}
|
||||
|
||||
/**
|
||||
* A {@link PTransform} that executes every Cloud SQL queries in a {@link PCollection } and reads
|
||||
* their result rows as {@code Entity} objects.
|
||||
*
|
||||
* <p>This class is adapted from {@link org.apache.beam.sdk.io.gcp.datastore.DatastoreV1.Read}. It
|
||||
* uses literal GQL queries in the input {@link PCollection} instead of a constant query provided
|
||||
* to the builder. Only the {@link #expand} method is modified from the original. Everything else
|
||||
* including comments have been copied verbatim.
|
||||
*/
|
||||
@AutoValue
|
||||
public abstract static class MultiRead
|
||||
extends PTransform<PCollection<String>, PCollection<Entity>> {
|
||||
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
|
||||
|
||||
/** An upper bound on the number of splits for a query. */
|
||||
public static final int NUM_QUERY_SPLITS_MAX = 50000;
|
||||
|
||||
/** A lower bound on the number of splits for a query. */
|
||||
static final int NUM_QUERY_SPLITS_MIN = 12;
|
||||
|
||||
/** Default bundle size of 64MB. */
|
||||
static final long DEFAULT_BUNDLE_SIZE_BYTES = 64L * 1024L * 1024L;
|
||||
|
||||
/**
|
||||
* Maximum number of results to request per query.
|
||||
*
|
||||
* <p>Must be set, or it may result in an I/O error when querying Cloud Datastore.
|
||||
*/
|
||||
static final int QUERY_BATCH_LIMIT = 500;
|
||||
|
||||
public abstract @Nullable String getProjectId();
|
||||
|
||||
public abstract @Nullable String getNamespace();
|
||||
|
||||
public abstract int getNumQuerySplits();
|
||||
|
||||
public abstract @Nullable String getLocalhost();
|
||||
|
||||
@Override
|
||||
public abstract String toString();
|
||||
|
||||
abstract Builder toBuilder();
|
||||
|
||||
@AutoValue.Builder
|
||||
abstract static class Builder {
|
||||
abstract Builder setProjectId(String projectId);
|
||||
|
||||
abstract Builder setNamespace(String namespace);
|
||||
|
||||
abstract Builder setNumQuerySplits(int numQuerySplits);
|
||||
|
||||
abstract Builder setLocalhost(String localhost);
|
||||
|
||||
abstract MultiRead build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Computes the number of splits to be performed on the given query by querying the estimated
|
||||
* size from Cloud Datastore.
|
||||
*/
|
||||
static int getEstimatedNumSplits(Datastore datastore, Query query, @Nullable String namespace) {
|
||||
int numSplits;
|
||||
try {
|
||||
long estimatedSizeBytes = getEstimatedSizeBytes(datastore, query, namespace);
|
||||
logger.atInfo().log("Estimated size bytes for the query is: %s", estimatedSizeBytes);
|
||||
numSplits =
|
||||
(int)
|
||||
Math.min(
|
||||
NUM_QUERY_SPLITS_MAX,
|
||||
Math.round(((double) estimatedSizeBytes) / DEFAULT_BUNDLE_SIZE_BYTES));
|
||||
} catch (Exception e) {
|
||||
logger.atWarning().log("Failed the fetch estimatedSizeBytes for query: %s", query, e);
|
||||
// Fallback in case estimated size is unavailable.
|
||||
numSplits = NUM_QUERY_SPLITS_MIN;
|
||||
}
|
||||
return Math.max(numSplits, NUM_QUERY_SPLITS_MIN);
|
||||
}
|
||||
|
||||
/**
|
||||
* Cloud Datastore system tables with statistics are periodically updated. This method fetches
|
||||
* the latest timestamp (in microseconds) of statistics update using the {@code __Stat_Total__}
|
||||
* table.
|
||||
*/
|
||||
private static long queryLatestStatisticsTimestamp(
|
||||
Datastore datastore, @Nullable String namespace) throws DatastoreException {
|
||||
Query.Builder query = Query.newBuilder();
|
||||
// Note: namespace either being null or empty represents the default namespace, in which
|
||||
// case we treat it as not provided by the user.
|
||||
if (Strings.isNullOrEmpty(namespace)) {
|
||||
query.addKindBuilder().setName("__Stat_Total__");
|
||||
} else {
|
||||
query.addKindBuilder().setName("__Stat_Ns_Total__");
|
||||
}
|
||||
query.addOrder(makeOrder("timestamp", DESCENDING));
|
||||
query.setLimit(Int32Value.newBuilder().setValue(1));
|
||||
RunQueryRequest request = makeRequest(query.build(), namespace);
|
||||
|
||||
RunQueryResponse response = datastore.runQuery(request);
|
||||
QueryResultBatch batch = response.getBatch();
|
||||
if (batch.getEntityResultsCount() == 0) {
|
||||
throw new NoSuchElementException("Datastore total statistics unavailable");
|
||||
}
|
||||
Entity entity = batch.getEntityResults(0).getEntity();
|
||||
return entity.getProperties().get("timestamp").getTimestampValue().getSeconds() * 1000000;
|
||||
}
|
||||
|
||||
/** Retrieve latest table statistics for a given kind, namespace, and datastore. */
|
||||
private static Entity getLatestTableStats(
|
||||
String ourKind, @Nullable String namespace, Datastore datastore) throws DatastoreException {
|
||||
long latestTimestamp = queryLatestStatisticsTimestamp(datastore, namespace);
|
||||
logger.atInfo().log("Latest stats timestamp for kind %s is %s", ourKind, latestTimestamp);
|
||||
|
||||
Query.Builder queryBuilder = Query.newBuilder();
|
||||
if (Strings.isNullOrEmpty(namespace)) {
|
||||
queryBuilder.addKindBuilder().setName("__Stat_Kind__");
|
||||
} else {
|
||||
queryBuilder.addKindBuilder().setName("__Stat_Ns_Kind__");
|
||||
}
|
||||
|
||||
queryBuilder.setFilter(
|
||||
makeAndFilter(
|
||||
makeFilter("kind_name", EQUAL, makeValue(ourKind).build()).build(),
|
||||
makeFilter("timestamp", EQUAL, makeValue(latestTimestamp).build()).build()));
|
||||
|
||||
RunQueryRequest request = makeRequest(queryBuilder.build(), namespace);
|
||||
|
||||
long now = System.currentTimeMillis();
|
||||
RunQueryResponse response = datastore.runQuery(request);
|
||||
logger.atFine().log(
|
||||
"Query for per-kind statistics took %sms", System.currentTimeMillis() - now);
|
||||
|
||||
QueryResultBatch batch = response.getBatch();
|
||||
if (batch.getEntityResultsCount() == 0) {
|
||||
throw new NoSuchElementException(
|
||||
"Datastore statistics for kind " + ourKind + " unavailable");
|
||||
}
|
||||
return batch.getEntityResults(0).getEntity();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the estimated size of the data returned by the given query.
|
||||
*
|
||||
* <p>Cloud Datastore provides no way to get a good estimate of how large the result of a query
|
||||
* entity kind being queried, using the __Stat_Kind__ system table, assuming exactly 1 kind is
|
||||
* specified in the query.
|
||||
*
|
||||
* <p>See https://cloud.google.com/datastore/docs/concepts/stats.
|
||||
*/
|
||||
static long getEstimatedSizeBytes(Datastore datastore, Query query, @Nullable String namespace)
|
||||
throws DatastoreException {
|
||||
String ourKind = query.getKind(0).getName();
|
||||
Entity entity = getLatestTableStats(ourKind, namespace, datastore);
|
||||
return entity.getProperties().get("entity_bytes").getIntegerValue();
|
||||
}
|
||||
|
||||
private static PartitionId.Builder forNamespace(@Nullable String namespace) {
|
||||
PartitionId.Builder partitionBuilder = PartitionId.newBuilder();
|
||||
// Namespace either being null or empty represents the default namespace.
|
||||
// Datastore Client libraries expect users to not set the namespace proto field in
|
||||
// either of these cases.
|
||||
if (!Strings.isNullOrEmpty(namespace)) {
|
||||
partitionBuilder.setNamespaceId(namespace);
|
||||
}
|
||||
return partitionBuilder;
|
||||
}
|
||||
|
||||
/** Builds a {@link RunQueryRequest} from the {@code query} and {@code namespace}. */
|
||||
static RunQueryRequest makeRequest(Query query, @Nullable String namespace) {
|
||||
return RunQueryRequest.newBuilder()
|
||||
.setQuery(query)
|
||||
.setPartitionId(forNamespace(namespace))
|
||||
.build();
|
||||
}
|
||||
|
||||
/** Builds a {@link RunQueryRequest} from the {@code GqlQuery} and {@code namespace}. */
|
||||
private static RunQueryRequest makeRequest(GqlQuery gqlQuery, @Nullable String namespace) {
|
||||
return RunQueryRequest.newBuilder()
|
||||
.setGqlQuery(gqlQuery)
|
||||
.setPartitionId(forNamespace(namespace))
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* A helper function to get the split queries, taking into account the optional {@code
|
||||
* namespace}.
|
||||
*/
|
||||
private static List<Query> splitQuery(
|
||||
Query query,
|
||||
@Nullable String namespace,
|
||||
Datastore datastore,
|
||||
QuerySplitter querySplitter,
|
||||
int numSplits)
|
||||
throws DatastoreException {
|
||||
// If namespace is set, include it in the split request so splits are calculated accordingly.
|
||||
return querySplitter.getSplits(query, forNamespace(namespace).build(), numSplits, datastore);
|
||||
}
|
||||
|
||||
/**
|
||||
* Translates a Cloud Datastore gql query string to {@link Query}.
|
||||
*
|
||||
* <p>Currently, the only way to translate a gql query string to a Query is to run the query
|
||||
* against Cloud Datastore and extract the {@code Query} from the response. To prevent reading
|
||||
* any data, we set the {@code LIMIT} to 0 but if the gql query already has a limit set, we
|
||||
* catch the exception with {@code INVALID_ARGUMENT} error code and retry the translation
|
||||
* without the zero limit.
|
||||
*
|
||||
* <p>Note: This may result in reading actual data from Cloud Datastore but the service has a
|
||||
* cap on the number of entities returned for a single rpc request, so this should not be a
|
||||
* problem in practice.
|
||||
*/
|
||||
private static Query translateGqlQueryWithLimitCheck(
|
||||
String gql, Datastore datastore, String namespace) throws DatastoreException {
|
||||
String gqlQueryWithZeroLimit = gql + " LIMIT 0";
|
||||
try {
|
||||
Query translatedQuery = translateGqlQuery(gqlQueryWithZeroLimit, datastore, namespace);
|
||||
// Clear the limit that we set.
|
||||
return translatedQuery.toBuilder().clearLimit().build();
|
||||
} catch (DatastoreException e) {
|
||||
// Note: There is no specific error code or message to detect if the query already has a
|
||||
// limit, so we just check for INVALID_ARGUMENT and assume that that the query might have
|
||||
// a limit already set.
|
||||
if (e.getCode() == Code.INVALID_ARGUMENT) {
|
||||
logger.atWarning().log(
|
||||
"Failed to translate Gql query '%s': %s", gqlQueryWithZeroLimit, e.getMessage());
|
||||
logger.atWarning().log(
|
||||
"User query might have a limit already set, so trying without zero limit");
|
||||
// Retry without the zero limit.
|
||||
return translateGqlQuery(gql, datastore, namespace);
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Translates a gql query string to {@link Query}. */
|
||||
private static Query translateGqlQuery(String gql, Datastore datastore, String namespace)
|
||||
throws DatastoreException {
|
||||
logger.atInfo().log("Translating gql %s", gql);
|
||||
GqlQuery gqlQuery = GqlQuery.newBuilder().setQueryString(gql).setAllowLiterals(true).build();
|
||||
RunQueryRequest req = makeRequest(gqlQuery, namespace);
|
||||
return datastore.runQuery(req).getQuery();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a new {@link MultiRead} that reads from the Cloud Datastore for the specified
|
||||
* project.
|
||||
*/
|
||||
public MultiRead withProjectId(String projectId) {
|
||||
checkArgument(projectId != null, "projectId can not be null");
|
||||
return toBuilder().setProjectId(projectId).build();
|
||||
}
|
||||
|
||||
/** Returns a new {@link MultiRead} that reads from the given namespace. */
|
||||
public MultiRead withNamespace(String namespace) {
|
||||
return toBuilder().setNamespace(namespace).build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a new {@link MultiRead} that reads by splitting the given {@code query} into {@code
|
||||
* numQuerySplits}.
|
||||
*
|
||||
* <p>The semantics for the query splitting is defined below:
|
||||
*
|
||||
* <ul>
|
||||
* <li>Any value less than or equal to 0 will be ignored, and the number of splits will be
|
||||
* chosen dynamically at runtime based on the query data size.
|
||||
* <li>Any value greater than {@link MultiRead#NUM_QUERY_SPLITS_MAX} will be capped at {@code
|
||||
* NUM_QUERY_SPLITS_MAX}.
|
||||
* <li>If the {@code query} has a user limit set, then {@code numQuerySplits} will be ignored
|
||||
* and no split will be performed.
|
||||
* <li>Under certain cases Cloud Datastore is unable to split query to the requested number of
|
||||
* splits. In such cases we just use whatever the Cloud Datastore returns.
|
||||
* </ul>
|
||||
*/
|
||||
public MultiRead withNumQuerySplits(int numQuerySplits) {
|
||||
return toBuilder()
|
||||
.setNumQuerySplits(Math.min(Math.max(numQuerySplits, 0), NUM_QUERY_SPLITS_MAX))
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a new {@link MultiRead} that reads from a Datastore Emulator running at the given
|
||||
* localhost address.
|
||||
*/
|
||||
public MultiRead withLocalhost(String localhost) {
|
||||
return toBuilder().setLocalhost(localhost).build();
|
||||
}
|
||||
|
||||
/** Returns Number of entities available for reading. */
|
||||
public long getNumEntities(
|
||||
PipelineOptions options, String ourKind, @Nullable String namespace) {
|
||||
try {
|
||||
V1Options v1Options = V1Options.from(getProjectId(), getNamespace(), getLocalhost());
|
||||
V1DatastoreFactory datastoreFactory = new V1DatastoreFactory();
|
||||
Datastore datastore =
|
||||
datastoreFactory.getDatastore(
|
||||
options, v1Options.getProjectId(), v1Options.getLocalhost());
|
||||
|
||||
Entity entity = getLatestTableStats(ourKind, namespace, datastore);
|
||||
return entity.getProperties().get("count").getIntegerValue();
|
||||
} catch (Exception e) {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public PCollection<Entity> expand(PCollection<String> gqlQueries) {
|
||||
checkArgument(getProjectId() != null, "projectId cannot be null");
|
||||
|
||||
V1Options v1Options = V1Options.from(getProjectId(), getNamespace(), getLocalhost());
|
||||
|
||||
/*
|
||||
* This composite transform involves the following steps:
|
||||
* 1. Apply a {@link ParDo} that translates each query in {@code gqlQueries} into a {@code
|
||||
* query}.
|
||||
*
|
||||
* 2. A {@link ParDo} splits the resulting query into {@code numQuerySplits} and
|
||||
* assign each split query a unique {@code Integer} as the key. The resulting output is
|
||||
* of the type {@code PCollection<KV<Integer, Query>>}.
|
||||
*
|
||||
* If the value of {@code numQuerySplits} is less than or equal to 0, then the number of
|
||||
* splits will be computed dynamically based on the size of the data for the {@code query}.
|
||||
*
|
||||
* 3. The resulting {@code PCollection} is sharded using a {@link GroupByKey} operation. The
|
||||
* queries are extracted from they {@code KV<Integer, Iterable<Query>>} and flattened to
|
||||
* output a {@code PCollection<Query>}.
|
||||
*
|
||||
* 4. In the third step, a {@code ParDo} reads entities for each query and outputs
|
||||
* a {@code PCollection<Entity>}.
|
||||
*/
|
||||
|
||||
PCollection<Query> inputQuery =
|
||||
gqlQueries.apply(ParDo.of(new GqlQueryTranslateFn(v1Options)));
|
||||
|
||||
return inputQuery
|
||||
.apply("Split", ParDo.of(new SplitQueryFn(v1Options, getNumQuerySplits())))
|
||||
.apply("Reshuffle", Reshuffle.viaRandomKey())
|
||||
.apply("Read", ParDo.of(new ReadFn(v1Options)));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void populateDisplayData(DisplayData.Builder builder) {
|
||||
super.populateDisplayData(builder);
|
||||
builder
|
||||
.addIfNotNull(DisplayData.item("projectId", getProjectId()).withLabel("ProjectId"))
|
||||
.addIfNotNull(DisplayData.item("namespace", getNamespace()).withLabel("Namespace"));
|
||||
}
|
||||
|
||||
private static class V1Options implements HasDisplayData, Serializable {
|
||||
private final String project;
|
||||
private final @Nullable String namespace;
|
||||
private final @Nullable String localhost;
|
||||
|
||||
private V1Options(String project, @Nullable String namespace, @Nullable String localhost) {
|
||||
this.project = project;
|
||||
this.namespace = namespace;
|
||||
this.localhost = localhost;
|
||||
}
|
||||
|
||||
public static V1Options from(
|
||||
String projectId, @Nullable String namespace, @Nullable String localhost) {
|
||||
return new V1Options(projectId, namespace, localhost);
|
||||
}
|
||||
|
||||
public String getProjectId() {
|
||||
return project;
|
||||
}
|
||||
|
||||
public @Nullable String getNamespace() {
|
||||
return namespace;
|
||||
}
|
||||
|
||||
public @Nullable String getLocalhost() {
|
||||
return localhost;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void populateDisplayData(DisplayData.Builder builder) {
|
||||
builder
|
||||
.addIfNotNull(DisplayData.item("projectId", getProjectId()).withLabel("ProjectId"))
|
||||
.addIfNotNull(DisplayData.item("namespace", getNamespace()).withLabel("Namespace"));
|
||||
}
|
||||
}
|
||||
|
||||
/** A DoFn that translates a Cloud Datastore gql query string to {@code Query}. */
|
||||
static class GqlQueryTranslateFn extends DoFn<String, Query> {
|
||||
private final V1Options v1Options;
|
||||
private transient Datastore datastore;
|
||||
private final V1DatastoreFactory datastoreFactory;
|
||||
|
||||
GqlQueryTranslateFn(V1Options options) {
|
||||
this(options, new V1DatastoreFactory());
|
||||
}
|
||||
|
||||
GqlQueryTranslateFn(V1Options options, V1DatastoreFactory datastoreFactory) {
|
||||
this.v1Options = options;
|
||||
this.datastoreFactory = datastoreFactory;
|
||||
}
|
||||
|
||||
@StartBundle
|
||||
public void startBundle(StartBundleContext c) throws Exception {
|
||||
datastore =
|
||||
datastoreFactory.getDatastore(
|
||||
c.getPipelineOptions(), v1Options.getProjectId(), v1Options.getLocalhost());
|
||||
}
|
||||
|
||||
@ProcessElement
|
||||
public void processElement(ProcessContext c) throws Exception {
|
||||
String gqlQuery = c.element();
|
||||
logger.atInfo().log("User query: '%s'", gqlQuery);
|
||||
Query query =
|
||||
translateGqlQueryWithLimitCheck(gqlQuery, datastore, v1Options.getNamespace());
|
||||
logger.atInfo().log("User gql query translated to Query(%s)", query);
|
||||
c.output(query);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A {@link DoFn} that splits a given query into multiple sub-queries, assigns them unique keys
|
||||
* and outputs them as {@link KV}.
|
||||
*/
|
||||
private static class SplitQueryFn extends DoFn<Query, Query> {
|
||||
private final V1Options options;
|
||||
// number of splits to make for a given query
|
||||
private final int numSplits;
|
||||
|
||||
private final V1DatastoreFactory datastoreFactory;
|
||||
// Datastore client
|
||||
private transient Datastore datastore;
|
||||
// Query splitter
|
||||
private transient QuerySplitter querySplitter;
|
||||
|
||||
public SplitQueryFn(V1Options options, int numSplits) {
|
||||
this(options, numSplits, new V1DatastoreFactory());
|
||||
}
|
||||
|
||||
private SplitQueryFn(V1Options options, int numSplits, V1DatastoreFactory datastoreFactory) {
|
||||
this.options = options;
|
||||
this.numSplits = numSplits;
|
||||
this.datastoreFactory = datastoreFactory;
|
||||
}
|
||||
|
||||
@StartBundle
|
||||
public void startBundle(StartBundleContext c) throws Exception {
|
||||
datastore =
|
||||
datastoreFactory.getDatastore(
|
||||
c.getPipelineOptions(), options.getProjectId(), options.getLocalhost());
|
||||
querySplitter = datastoreFactory.getQuerySplitter();
|
||||
}
|
||||
|
||||
@ProcessElement
|
||||
public void processElement(ProcessContext c) throws Exception {
|
||||
Query query = c.element();
|
||||
|
||||
// If query has a user set limit, then do not split.
|
||||
if (query.hasLimit()) {
|
||||
c.output(query);
|
||||
return;
|
||||
}
|
||||
|
||||
int estimatedNumSplits;
|
||||
// Compute the estimated numSplits if numSplits is not specified by the user.
|
||||
if (numSplits <= 0) {
|
||||
estimatedNumSplits = getEstimatedNumSplits(datastore, query, options.getNamespace());
|
||||
} else {
|
||||
estimatedNumSplits = numSplits;
|
||||
}
|
||||
|
||||
logger.atInfo().log("Splitting the query into %s splits", estimatedNumSplits);
|
||||
List<Query> querySplits;
|
||||
try {
|
||||
querySplits =
|
||||
splitQuery(
|
||||
query, options.getNamespace(), datastore, querySplitter, estimatedNumSplits);
|
||||
} catch (Exception e) {
|
||||
logger.atWarning().log("Unable to parallelize the given query: %s", query, e);
|
||||
querySplits = ImmutableList.of(query);
|
||||
}
|
||||
|
||||
// assign unique keys to query splits.
|
||||
for (Query subquery : querySplits) {
|
||||
c.output(subquery);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void populateDisplayData(DisplayData.Builder builder) {
|
||||
super.populateDisplayData(builder);
|
||||
builder.include("options", options);
|
||||
if (numSplits > 0) {
|
||||
builder.add(
|
||||
DisplayData.item("numQuerySplits", numSplits)
|
||||
.withLabel("Requested number of Query splits"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** A {@link DoFn} that reads entities from Cloud Datastore for each query. */
|
||||
private static class ReadFn extends DoFn<Query, Entity> {
|
||||
private final V1Options options;
|
||||
private final V1DatastoreFactory datastoreFactory;
|
||||
// Datastore client
|
||||
private transient Datastore datastore;
|
||||
private final Counter rpcErrors = Metrics.counter(ReadFn.class, "datastoreRpcErrors");
|
||||
private final Counter rpcSuccesses = Metrics.counter(ReadFn.class, "datastoreRpcSuccesses");
|
||||
private static final int MAX_RETRIES = 5;
|
||||
private static final FluentBackoff RUNQUERY_BACKOFF =
|
||||
FluentBackoff.DEFAULT
|
||||
.withMaxRetries(MAX_RETRIES)
|
||||
.withInitialBackoff(Duration.standardSeconds(5));
|
||||
|
||||
public ReadFn(V1Options options) {
|
||||
this(options, new V1DatastoreFactory());
|
||||
}
|
||||
|
||||
private ReadFn(V1Options options, V1DatastoreFactory datastoreFactory) {
|
||||
this.options = options;
|
||||
this.datastoreFactory = datastoreFactory;
|
||||
}
|
||||
|
||||
@StartBundle
|
||||
public void startBundle(StartBundleContext c) throws Exception {
|
||||
datastore =
|
||||
datastoreFactory.getDatastore(
|
||||
c.getPipelineOptions(), options.getProjectId(), options.getLocalhost());
|
||||
}
|
||||
|
||||
private RunQueryResponse runQueryWithRetries(RunQueryRequest request) throws Exception {
|
||||
Sleeper sleeper = Sleeper.DEFAULT;
|
||||
BackOff backoff = RUNQUERY_BACKOFF.backoff();
|
||||
while (true) {
|
||||
try {
|
||||
RunQueryResponse response = datastore.runQuery(request);
|
||||
rpcSuccesses.inc();
|
||||
return response;
|
||||
} catch (DatastoreException exception) {
|
||||
rpcErrors.inc();
|
||||
|
||||
if (NON_RETRYABLE_ERRORS.contains(exception.getCode())) {
|
||||
throw exception;
|
||||
}
|
||||
if (!BackOffUtils.next(sleeper, backoff)) {
|
||||
logger.atSevere().log("Aborting after %s retries.", MAX_RETRIES);
|
||||
throw exception;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Read and output entities for the given query. */
|
||||
@ProcessElement
|
||||
public void processElement(ProcessContext context) throws Exception {
|
||||
Query query = context.element();
|
||||
String namespace = options.getNamespace();
|
||||
int userLimit = query.hasLimit() ? query.getLimit().getValue() : Integer.MAX_VALUE;
|
||||
|
||||
boolean moreResults = true;
|
||||
QueryResultBatch currentBatch = null;
|
||||
|
||||
while (moreResults) {
|
||||
Query.Builder queryBuilder = query.toBuilder();
|
||||
queryBuilder.setLimit(
|
||||
Int32Value.newBuilder().setValue(Math.min(userLimit, QUERY_BATCH_LIMIT)));
|
||||
|
||||
if (currentBatch != null && !currentBatch.getEndCursor().isEmpty()) {
|
||||
queryBuilder.setStartCursor(currentBatch.getEndCursor());
|
||||
}
|
||||
|
||||
RunQueryRequest request = makeRequest(queryBuilder.build(), namespace);
|
||||
RunQueryResponse response = runQueryWithRetries(request);
|
||||
|
||||
currentBatch = response.getBatch();
|
||||
|
||||
// MORE_RESULTS_AFTER_LIMIT is not implemented yet:
|
||||
// https://groups.google.com/forum/#!topic/gcd-discuss/iNs6M1jA2Vw, so
|
||||
// use result count to determine if more results might exist.
|
||||
int numFetch = currentBatch.getEntityResultsCount();
|
||||
if (query.hasLimit()) {
|
||||
verify(
|
||||
userLimit >= numFetch,
|
||||
"Expected userLimit %s >= numFetch %s, because query limit %s must be <= userLimit",
|
||||
userLimit,
|
||||
numFetch,
|
||||
query.getLimit());
|
||||
userLimit -= numFetch;
|
||||
}
|
||||
|
||||
// output all the entities from the current batch.
|
||||
for (EntityResult entityResult : currentBatch.getEntityResultsList()) {
|
||||
context.output(entityResult.getEntity());
|
||||
}
|
||||
|
||||
// Check if we have more entities to be read.
|
||||
moreResults =
|
||||
// User-limit does not exist (so userLimit == MAX_VALUE) and/or has not been satisfied
|
||||
(userLimit > 0)
|
||||
// All indications from the API are that there are/may be more results.
|
||||
&& ((numFetch == QUERY_BATCH_LIMIT)
|
||||
|| (currentBatch.getMoreResults() == NOT_FINISHED));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void populateDisplayData(DisplayData.Builder builder) {
|
||||
super.populateDisplayData(builder);
|
||||
builder.include("options", options);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A wrapper factory class for Cloud Datastore singleton classes {@link DatastoreFactory} and
|
||||
* {@link QuerySplitter}
|
||||
*
|
||||
* <p>{@link DatastoreFactory} and {@link QuerySplitter} are not java serializable, hence wrapping
|
||||
* them under this class, which implements {@link Serializable}.
|
||||
*/
|
||||
private static class V1DatastoreFactory implements Serializable {
|
||||
|
||||
/** Builds a Cloud Datastore client for the given pipeline options and project. */
|
||||
public Datastore getDatastore(PipelineOptions pipelineOptions, String projectId) {
|
||||
return getDatastore(pipelineOptions, projectId, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a Cloud Datastore client for the given pipeline options, project and an optional
|
||||
* locahost.
|
||||
*/
|
||||
public Datastore getDatastore(
|
||||
PipelineOptions pipelineOptions, String projectId, @Nullable String localhost) {
|
||||
Credentials credential = pipelineOptions.as(GcpOptions.class).getGcpCredential();
|
||||
HttpRequestInitializer initializer;
|
||||
if (credential != null) {
|
||||
initializer =
|
||||
new ChainingHttpRequestInitializer(
|
||||
new HttpCredentialsAdapter(credential), new RetryHttpRequestInitializer());
|
||||
} else {
|
||||
initializer = new RetryHttpRequestInitializer();
|
||||
}
|
||||
|
||||
DatastoreOptions.Builder builder =
|
||||
new DatastoreOptions.Builder().projectId(projectId).initializer(initializer);
|
||||
|
||||
if (localhost != null) {
|
||||
builder.localHost(localhost);
|
||||
} else {
|
||||
builder.host("batch-datastore.googleapis.com");
|
||||
}
|
||||
|
||||
return DatastoreFactory.get().create(builder.build());
|
||||
}
|
||||
|
||||
/** Builds a Cloud Datastore {@link QuerySplitter}. */
|
||||
public QuerySplitter getQuerySplitter() {
|
||||
return DatastoreHelper.getQuerySplitter();
|
||||
}
|
||||
}
|
||||
}
|
||||
-5
@@ -25,11 +25,6 @@
|
||||
<property name="tld" direction="asc"/>
|
||||
<property name="creationTime" direction="desc"/>
|
||||
</datastore-index>
|
||||
<!-- For finding non-autorenewing domains to be deleted. -->
|
||||
<datastore-index kind="DomainBase" ancestor="false" source="manual">
|
||||
<property name="autorenewEndTime" direction="asc"/>
|
||||
<property name="deletionTime" direction="asc"/>
|
||||
</datastore-index>
|
||||
<!-- For finding host resources by registrar. -->
|
||||
<datastore-index kind="HostResource" ancestor="false" source="manual">
|
||||
<property name="currentSponsorClientId" direction="asc"/>
|
||||
|
||||
@@ -22,5 +22,6 @@ public enum EppRequestSource {
|
||||
TLS,
|
||||
TOOL,
|
||||
CHECK_API,
|
||||
UNIT_TEST
|
||||
UNIT_TEST,
|
||||
BACKEND
|
||||
}
|
||||
|
||||
@@ -104,11 +104,14 @@ public final class ExtensionManager {
|
||||
clientId, flowClass.getSimpleName(), undeclaredUris);
|
||||
}
|
||||
|
||||
private static final ImmutableSet<EppRequestSource> ALLOWED_METADATA_EPP_REQUEST_SOURCES =
|
||||
ImmutableSet.of(EppRequestSource.TOOL, EppRequestSource.BACKEND);
|
||||
|
||||
private void checkForRestrictedExtensions(
|
||||
ImmutableSet<Class<? extends CommandExtension>> suppliedExtensions)
|
||||
throws OnlyToolCanPassMetadataException, UnauthorizedForSuperuserExtensionException {
|
||||
if (suppliedExtensions.contains(MetadataExtension.class)
|
||||
&& !eppRequestSource.equals(EppRequestSource.TOOL)) {
|
||||
&& !ALLOWED_METADATA_EPP_REQUEST_SOURCES.contains(eppRequestSource)) {
|
||||
throw new OnlyToolCanPassMetadataException();
|
||||
}
|
||||
// Can't use suppliedExtension.contains() here because the SuperuserExtension has child classes.
|
||||
|
||||
@@ -16,6 +16,7 @@ package google.registry.flows;
|
||||
|
||||
import static com.google.common.base.MoreObjects.toStringHelper;
|
||||
import static google.registry.request.RequestParameters.extractOptionalHeader;
|
||||
import static google.registry.util.X509Utils.loadCertificate;
|
||||
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
import com.google.common.collect.ImmutableList;
|
||||
@@ -25,11 +26,18 @@ import com.google.common.net.InetAddresses;
|
||||
import dagger.Module;
|
||||
import dagger.Provides;
|
||||
import google.registry.config.RegistryConfig.Config;
|
||||
import google.registry.config.RegistryEnvironment;
|
||||
import google.registry.flows.EppException.AuthenticationErrorException;
|
||||
import google.registry.flows.certs.CertificateChecker;
|
||||
import google.registry.flows.certs.CertificateChecker.InsecureCertificateException;
|
||||
import google.registry.model.registrar.Registrar;
|
||||
import google.registry.request.Header;
|
||||
import google.registry.util.CidrAddressBlock;
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.net.InetAddress;
|
||||
import java.security.cert.CertificateException;
|
||||
import java.security.cert.X509Certificate;
|
||||
import java.util.Base64;
|
||||
import java.util.Optional;
|
||||
import javax.inject.Inject;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
@@ -42,6 +50,9 @@ import javax.servlet.http.HttpServletRequest;
|
||||
* <dl>
|
||||
* <dt>X-SSL-Certificate
|
||||
* <dd>This field should contain a base64 encoded digest of the client's TLS certificate. It is
|
||||
* used only if the validation of the full certificate fails.
|
||||
* <dt>X-SSL-Full-Certificate
|
||||
* <dd>This field should contain a base64 encoding of the client's TLS certificate. It is
|
||||
* validated during an EPP login command against a known good value that is transmitted out of
|
||||
* band.
|
||||
* <dt>X-Forwarded-For
|
||||
@@ -55,16 +66,22 @@ public class TlsCredentials implements TransportCredentials {
|
||||
|
||||
private final boolean requireSslCertificates;
|
||||
private final Optional<String> clientCertificateHash;
|
||||
private final Optional<String> clientCertificate;
|
||||
private final Optional<InetAddress> clientInetAddr;
|
||||
private final CertificateChecker certificateChecker;
|
||||
|
||||
@Inject
|
||||
public TlsCredentials(
|
||||
@Config("requireSslCertificates") boolean requireSslCertificates,
|
||||
@Header("X-SSL-Certificate") Optional<String> clientCertificateHash,
|
||||
@Header("X-Forwarded-For") Optional<String> clientAddress) {
|
||||
@Header("X-SSL-Full-Certificate") Optional<String> clientCertificate,
|
||||
@Header("X-Forwarded-For") Optional<String> clientAddress,
|
||||
CertificateChecker certificateChecker) {
|
||||
this.requireSslCertificates = requireSslCertificates;
|
||||
this.clientCertificateHash = clientCertificateHash;
|
||||
this.clientCertificate = clientCertificate;
|
||||
this.clientInetAddr = clientAddress.map(TlsCredentials::parseInetAddress);
|
||||
this.certificateChecker = certificateChecker;
|
||||
}
|
||||
|
||||
static InetAddress parseInetAddress(String asciiAddr) {
|
||||
@@ -91,7 +108,7 @@ public class TlsCredentials implements TransportCredentials {
|
||||
ImmutableList<CidrAddressBlock> ipAddressAllowList = registrar.getIpAddressAllowList();
|
||||
if (ipAddressAllowList.isEmpty()) {
|
||||
logger.atInfo().log(
|
||||
"Skipping IP allow list check because %s doesn't have an IP allow list",
|
||||
"Skipping IP allow list check because %s doesn't have an IP allow list.",
|
||||
registrar.getClientId());
|
||||
return;
|
||||
}
|
||||
@@ -115,11 +132,80 @@ public class TlsCredentials implements TransportCredentials {
|
||||
/**
|
||||
* Verifies client SSL certificate is permitted to issue commands as {@code registrar}.
|
||||
*
|
||||
* @throws MissingRegistrarCertificateException if frontend didn't send certificate hash header
|
||||
* @throws MissingRegistrarCertificateException if frontend didn't send certificate header
|
||||
* @throws BadRegistrarCertificateException if registrar requires certificate and it didn't match
|
||||
*/
|
||||
@VisibleForTesting
|
||||
void validateCertificate(Registrar registrar) throws AuthenticationErrorException {
|
||||
// Check that certificate is present in registrar object
|
||||
if (!registrar.getClientCertificate().isPresent()
|
||||
&& !registrar.getFailoverClientCertificate().isPresent()) {
|
||||
// Log an error and validate using certificate hash instead
|
||||
// TODO(sarahbot): throw a RegistrarCertificateNotConfiguredException once hash is no longer
|
||||
// used as failover
|
||||
logger.atWarning().log(
|
||||
"There is no certificate configured for registrar %s.", registrar.getClientId());
|
||||
} else if (!clientCertificate.isPresent()) {
|
||||
// Check that the request included the full certificate
|
||||
// Log an error and validate using certificate hash instead
|
||||
// TODO(sarahbot): throw a MissingRegistrarCertificateException once hash is no longer used as
|
||||
// failover
|
||||
logger.atWarning().log(
|
||||
"Request from registrar %s did not include X-SSL-Full-Certificate.",
|
||||
registrar.getClientId());
|
||||
} else {
|
||||
X509Certificate passedCert;
|
||||
Optional<X509Certificate> storedCert;
|
||||
Optional<X509Certificate> storedFailoverCert;
|
||||
|
||||
try {
|
||||
storedCert = deserializePemCert(registrar.getClientCertificate());
|
||||
storedFailoverCert = deserializePemCert(registrar.getFailoverClientCertificate());
|
||||
passedCert = decodeCertString(clientCertificate.get());
|
||||
} catch (Exception e) {
|
||||
// TODO(Sarahbot@): remove this catch once we know it's working
|
||||
logger.atWarning().log(
|
||||
"Error converting certificate string to certificate for %s: %s",
|
||||
registrar.getClientId(), e);
|
||||
validateCertificateHash(registrar);
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if the certificate is equal to the one on file for the registrar.
|
||||
if (passedCert.equals(storedCert.orElse(null))
|
||||
|| passedCert.equals(storedFailoverCert.orElse(null))) {
|
||||
// Check certificate for any requirement violations
|
||||
// TODO(Sarahbot@): Throw exceptions instead of just logging once requirement enforcement
|
||||
// begins
|
||||
try {
|
||||
certificateChecker.validateCertificate(passedCert);
|
||||
} catch (InsecureCertificateException e) {
|
||||
// throw exception in unit tests and Sandbox
|
||||
if (RegistryEnvironment.get().equals(RegistryEnvironment.UNITTEST)
|
||||
|| RegistryEnvironment.get().equals(RegistryEnvironment.SANDBOX)) {
|
||||
throw new CertificateContainsSecurityViolationsException(e);
|
||||
}
|
||||
logger.atWarning().log(
|
||||
"Registrar certificate used for %s does not meet certificate requirements: %s",
|
||||
registrar.getClientId(), e.getMessage());
|
||||
} catch (Exception e) {
|
||||
logger.atWarning().log(
|
||||
"Error validating certificate for %s: %s", registrar.getClientId(), e);
|
||||
}
|
||||
// successfully validated, return here since hash validation is not necessary
|
||||
return;
|
||||
}
|
||||
// Log an error and validate using certificate hash instead
|
||||
// TODO(sarahbot): throw a BadRegistrarCertificateException once hash is no longer used as
|
||||
// failover
|
||||
logger.atWarning().log("Non-matching certificate for registrar %s.", registrar.getClientId());
|
||||
}
|
||||
validateCertificateHash(registrar);
|
||||
}
|
||||
|
||||
private void validateCertificateHash(Registrar registrar) throws AuthenticationErrorException {
|
||||
// Check the certificate hash as a failover
|
||||
// TODO(sarahbot): Remove hash checks once certificate checks are working.
|
||||
if (!registrar.getClientCertificateHash().isPresent()
|
||||
&& !registrar.getFailoverClientCertificateHash().isPresent()) {
|
||||
if (requireSslCertificates) {
|
||||
@@ -130,14 +216,17 @@ public class TlsCredentials implements TransportCredentials {
|
||||
return;
|
||||
}
|
||||
}
|
||||
// Check that the request included the certificate hash
|
||||
if (!clientCertificateHash.isPresent()) {
|
||||
logger.atInfo().log("Request did not include X-SSL-Certificate");
|
||||
logger.atInfo().log(
|
||||
"Request from registrar %s did not include X-SSL-Certificate.", registrar.getClientId());
|
||||
throw new MissingRegistrarCertificateException();
|
||||
}
|
||||
// Check if the certificate hash is equal to the one on file for the registrar.
|
||||
if (!clientCertificateHash.equals(registrar.getClientCertificateHash())
|
||||
&& !clientCertificateHash.equals(registrar.getFailoverClientCertificateHash())) {
|
||||
logger.atWarning().log(
|
||||
"bad certificate hash (%s) for %s, wanted either %s or %s",
|
||||
"Non-matching certificate hash (%s) for %s, wanted either %s or %s.",
|
||||
clientCertificateHash,
|
||||
registrar.getClientId(),
|
||||
registrar.getClientCertificateHash(),
|
||||
@@ -153,9 +242,26 @@ public class TlsCredentials implements TransportCredentials {
|
||||
}
|
||||
}
|
||||
|
||||
// Converts a PEM formatted certificate string into an X509Certificate
|
||||
private Optional<X509Certificate> deserializePemCert(Optional<String> certificateString)
|
||||
throws CertificateException {
|
||||
if (certificateString.isPresent()) {
|
||||
return Optional.of(loadCertificate(certificateString.get()));
|
||||
}
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
// Decodes the string representation of an encoded certificate back into an X509Certificate
|
||||
private X509Certificate decodeCertString(String encodedCertString) throws CertificateException {
|
||||
byte decodedCert[] = Base64.getDecoder().decode(encodedCertString);
|
||||
ByteArrayInputStream inputStream = new ByteArrayInputStream(decodedCert);
|
||||
return loadCertificate(inputStream);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return toStringHelper(getClass())
|
||||
.add("clientCertificate", clientCertificate.orElse(null))
|
||||
.add("clientCertificateHash", clientCertificateHash.orElse(null))
|
||||
.add("clientAddress", clientInetAddr.orElse(null))
|
||||
.toString();
|
||||
@@ -168,6 +274,20 @@ public class TlsCredentials implements TransportCredentials {
|
||||
}
|
||||
}
|
||||
|
||||
/** Registrar certificate contains the following security violations: ... */
|
||||
public static class CertificateContainsSecurityViolationsException
|
||||
extends AuthenticationErrorException {
|
||||
InsecureCertificateException exception;
|
||||
|
||||
CertificateContainsSecurityViolationsException(InsecureCertificateException exception) {
|
||||
super(
|
||||
String.format(
|
||||
"Registrar certificate contains the following security violations:\n%s",
|
||||
exception.getMessage()));
|
||||
this.exception = exception;
|
||||
}
|
||||
}
|
||||
|
||||
/** Registrar certificate not present. */
|
||||
public static class MissingRegistrarCertificateException extends AuthenticationErrorException {
|
||||
MissingRegistrarCertificateException() {
|
||||
@@ -202,6 +322,14 @@ public class TlsCredentials implements TransportCredentials {
|
||||
return extractOptionalHeader(req, "X-SSL-Certificate");
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Header("X-SSL-Full-Certificate")
|
||||
static Optional<String> provideClientCertificate(HttpServletRequest req) {
|
||||
// Note: This header is actually required, we just want to handle its absence explicitly
|
||||
// by throwing an EPP exception rather than a generic Bad Request exception.
|
||||
return extractOptionalHeader(req, "X-SSL-Full-Certificate");
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Header("X-Forwarded-For")
|
||||
static Optional<String> provideForwardedFor(HttpServletRequest req) {
|
||||
|
||||
@@ -89,14 +89,26 @@ public class CertificateChecker {
|
||||
* Checks the given certificate string for violations and throws an exception if any violations
|
||||
* exist.
|
||||
*/
|
||||
public void validateCertificate(String certificateString) {
|
||||
ImmutableSet<CertificateViolation> violations = checkCertificate(certificateString);
|
||||
public void validateCertificate(String certificateString) throws InsecureCertificateException {
|
||||
handleCertViolations(checkCertificate(certificateString));
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks the given certificate string for violations and throws an exception if any violations
|
||||
* exist.
|
||||
*/
|
||||
public void validateCertificate(X509Certificate certificate) throws InsecureCertificateException {
|
||||
handleCertViolations(checkCertificate(certificate));
|
||||
}
|
||||
|
||||
private void handleCertViolations(ImmutableSet<CertificateViolation> violations)
|
||||
throws InsecureCertificateException {
|
||||
if (!violations.isEmpty()) {
|
||||
String displayMessages =
|
||||
violations.stream()
|
||||
.map(violation -> getViolationDisplayMessage(violation))
|
||||
.collect(Collectors.joining("\n"));
|
||||
throw new IllegalArgumentException(displayMessages);
|
||||
throw new InsecureCertificateException(violations, displayMessages);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -258,4 +270,14 @@ public class CertificateChecker {
|
||||
return certificateChecker.getViolationDisplayMessage(this);
|
||||
}
|
||||
}
|
||||
|
||||
/** Exception to throw when a certificate has security violations. */
|
||||
public static class InsecureCertificateException extends Exception {
|
||||
ImmutableSet<CertificateViolation> violations;
|
||||
|
||||
InsecureCertificateException(ImmutableSet<CertificateViolation> violations, String message) {
|
||||
super(message);
|
||||
this.violations = violations;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,9 +22,9 @@ import static google.registry.flows.ResourceFlowUtils.verifyResourceOwnership;
|
||||
import static google.registry.flows.contact.ContactFlowUtils.createGainingTransferPollMessage;
|
||||
import static google.registry.flows.contact.ContactFlowUtils.createTransferResponse;
|
||||
import static google.registry.model.ResourceTransferUtils.approvePendingTransfer;
|
||||
import static google.registry.model.ofy.ObjectifyService.ofy;
|
||||
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
|
||||
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
import com.googlecode.objectify.Key;
|
||||
import google.registry.flows.EppException;
|
||||
import google.registry.flows.ExtensionManager;
|
||||
@@ -94,7 +94,8 @@ public final class ContactTransferApproveFlow implements TransactionalFlow {
|
||||
// Create a poll message for the gaining client.
|
||||
PollMessage gainingPollMessage =
|
||||
createGainingTransferPollMessage(targetId, newContact.getTransferData(), historyEntry);
|
||||
ofy().save().<Object>entities(newContact, historyEntry, gainingPollMessage);
|
||||
tm().insertAll(ImmutableSet.of(historyEntry.toChildHistoryEntity(), gainingPollMessage));
|
||||
tm().update(newContact);
|
||||
// Delete the billing event and poll messages that were written in case the transfer would have
|
||||
// been implicitly server approved.
|
||||
tm().delete(existingContact.getTransferData().getServerApproveEntities());
|
||||
|
||||
@@ -22,9 +22,9 @@ import static google.registry.flows.ResourceFlowUtils.verifyTransferInitiator;
|
||||
import static google.registry.flows.contact.ContactFlowUtils.createLosingTransferPollMessage;
|
||||
import static google.registry.flows.contact.ContactFlowUtils.createTransferResponse;
|
||||
import static google.registry.model.ResourceTransferUtils.denyPendingTransfer;
|
||||
import static google.registry.model.ofy.ObjectifyService.ofy;
|
||||
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
|
||||
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
import com.googlecode.objectify.Key;
|
||||
import google.registry.flows.EppException;
|
||||
import google.registry.flows.ExtensionManager;
|
||||
@@ -90,7 +90,8 @@ public final class ContactTransferCancelFlow implements TransactionalFlow {
|
||||
// Create a poll message for the losing client.
|
||||
PollMessage losingPollMessage =
|
||||
createLosingTransferPollMessage(targetId, newContact.getTransferData(), historyEntry);
|
||||
ofy().save().<Object>entities(newContact, historyEntry, losingPollMessage);
|
||||
tm().insertAll(ImmutableSet.of(historyEntry.toChildHistoryEntity(), losingPollMessage));
|
||||
tm().update(newContact);
|
||||
// Delete the billing event and poll messages that were written in case the transfer would have
|
||||
// been implicitly server approved.
|
||||
tm().delete(existingContact.getTransferData().getServerApproveEntities());
|
||||
|
||||
@@ -22,9 +22,9 @@ import static google.registry.flows.ResourceFlowUtils.verifyResourceOwnership;
|
||||
import static google.registry.flows.contact.ContactFlowUtils.createGainingTransferPollMessage;
|
||||
import static google.registry.flows.contact.ContactFlowUtils.createTransferResponse;
|
||||
import static google.registry.model.ResourceTransferUtils.denyPendingTransfer;
|
||||
import static google.registry.model.ofy.ObjectifyService.ofy;
|
||||
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
|
||||
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
import com.googlecode.objectify.Key;
|
||||
import google.registry.flows.EppException;
|
||||
import google.registry.flows.ExtensionManager;
|
||||
@@ -87,7 +87,8 @@ public final class ContactTransferRejectFlow implements TransactionalFlow {
|
||||
.build();
|
||||
PollMessage gainingPollMessage =
|
||||
createGainingTransferPollMessage(targetId, newContact.getTransferData(), historyEntry);
|
||||
ofy().save().<Object>entities(newContact, historyEntry, gainingPollMessage);
|
||||
tm().insertAll(ImmutableSet.of(historyEntry.toChildHistoryEntity(), gainingPollMessage));
|
||||
tm().update(newContact);
|
||||
// Delete the billing event and poll messages that were written in case the transfer would have
|
||||
// been implicitly server approved.
|
||||
tm().delete(existingContact.getTransferData().getServerApproveEntities());
|
||||
|
||||
@@ -23,7 +23,6 @@ import static google.registry.flows.contact.ContactFlowUtils.createGainingTransf
|
||||
import static google.registry.flows.contact.ContactFlowUtils.createLosingTransferPollMessage;
|
||||
import static google.registry.flows.contact.ContactFlowUtils.createTransferResponse;
|
||||
import static google.registry.model.eppoutput.Result.Code.SUCCESS_WITH_ACTION_PENDING;
|
||||
import static google.registry.model.ofy.ObjectifyService.ofy;
|
||||
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
|
||||
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
@@ -145,12 +144,13 @@ public final class ContactTransferRequestFlow implements TransactionalFlow {
|
||||
.setTransferData(pendingTransferData)
|
||||
.addStatusValue(StatusValue.PENDING_TRANSFER)
|
||||
.build();
|
||||
ofy().save().<Object>entities(
|
||||
newContact,
|
||||
historyEntry,
|
||||
requestPollMessage,
|
||||
serverApproveGainingPollMessage,
|
||||
serverApproveLosingPollMessage);
|
||||
tm().update(newContact);
|
||||
tm().insertAll(
|
||||
ImmutableSet.of(
|
||||
historyEntry.toChildHistoryEntity(),
|
||||
requestPollMessage,
|
||||
serverApproveGainingPollMessage,
|
||||
serverApproveLosingPollMessage));
|
||||
return responseBuilder
|
||||
.setResultFromCode(SUCCESS_WITH_ACTION_PENDING)
|
||||
.setResData(createTransferResponse(targetId, newContact.getTransferData()))
|
||||
|
||||
@@ -154,7 +154,7 @@ public class AllocationTokenFlowUtils {
|
||||
}
|
||||
Optional<AllocationToken> maybeTokenEntity =
|
||||
tm().loadByKeyIfPresent(VKey.create(AllocationToken.class, token));
|
||||
if (maybeTokenEntity.isEmpty()) {
|
||||
if (!maybeTokenEntity.isPresent()) {
|
||||
throw new InvalidAllocationTokenException();
|
||||
}
|
||||
if (maybeTokenEntity.get().isRedeemed()) {
|
||||
|
||||
@@ -148,7 +148,7 @@ public abstract class EppResource extends BackupGroupRoot implements Buildable {
|
||||
*
|
||||
* @see google.registry.model.translators.CommitLogRevisionsTranslatorFactory
|
||||
*/
|
||||
@Transient
|
||||
@Transient @DoNotCompare
|
||||
ImmutableSortedMap<DateTime, Key<CommitLogManifest>> revisions = ImmutableSortedMap.of();
|
||||
|
||||
public String getRepoId() {
|
||||
|
||||
@@ -54,9 +54,37 @@ public abstract class ImmutableObject implements Cloneable {
|
||||
@Target(FIELD)
|
||||
public @interface DoNotHydrate {}
|
||||
|
||||
@Ignore
|
||||
@XmlTransient
|
||||
Integer hashCode;
|
||||
/**
|
||||
* Indicates that the field should be ignored when comparing an object in the datastore to the
|
||||
* corresponding object in Cloud SQL.
|
||||
*/
|
||||
@Documented
|
||||
@Retention(RUNTIME)
|
||||
@Target(FIELD)
|
||||
public @interface DoNotCompare {}
|
||||
|
||||
/**
|
||||
* Indicates that the field stores a null value to indicate an empty set. This is also used in
|
||||
* object comparison.
|
||||
*/
|
||||
@Documented
|
||||
@Retention(RUNTIME)
|
||||
@Target(FIELD)
|
||||
public @interface EmptySetToNull {}
|
||||
|
||||
/**
|
||||
* Indicates that the field does not take part in the immutability contract.
|
||||
*
|
||||
* <p>Certain fields currently get modified by hibernate and there is nothing we can do about it.
|
||||
* As well as violating immutability, this breaks hashing and equality comparisons, so we mark
|
||||
* these fields with this annotation to exclude them from most operations.
|
||||
*/
|
||||
@Documented
|
||||
@Retention(RUNTIME)
|
||||
@Target(FIELD)
|
||||
public @interface Insignificant {}
|
||||
|
||||
@Ignore @XmlTransient protected Integer hashCode;
|
||||
|
||||
private boolean equalsImmutableObject(ImmutableObject other) {
|
||||
return getClass().equals(other.getClass())
|
||||
@@ -71,7 +99,14 @@ public abstract class ImmutableObject implements Cloneable {
|
||||
* <p>Isolated into a method so that derived classes can override it.
|
||||
*/
|
||||
protected Map<Field, Object> getSignificantFields() {
|
||||
return ModelUtils.getFieldValues(this);
|
||||
// Can't use streams or ImmutableMap because we can have null values.
|
||||
Map<Field, Object> result = new LinkedHashMap();
|
||||
for (Map.Entry<Field, Object> entry : ModelUtils.getFieldValues(this).entrySet()) {
|
||||
if (!entry.getKey().isAnnotationPresent(Insignificant.class)) {
|
||||
result.put(entry.getKey(), entry.getValue());
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
|
||||
package google.registry.model.domain;
|
||||
|
||||
import static com.google.common.base.MoreObjects.firstNonNull;
|
||||
import static com.google.common.base.Preconditions.checkArgument;
|
||||
import static com.google.common.base.Strings.emptyToNull;
|
||||
import static com.google.common.collect.ImmutableSet.toImmutableSet;
|
||||
@@ -65,6 +66,7 @@ import google.registry.model.transfer.DomainTransferData;
|
||||
import google.registry.model.transfer.TransferStatus;
|
||||
import google.registry.persistence.VKey;
|
||||
import google.registry.util.CollectionUtils;
|
||||
import google.registry.util.DateTimeUtils;
|
||||
import java.util.HashSet;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
@@ -132,7 +134,7 @@ public class DomainContent extends EppResource
|
||||
@Index String tld;
|
||||
|
||||
/** References to hosts that are the nameservers for the domain. */
|
||||
@Index @Transient Set<VKey<HostResource>> nsHosts;
|
||||
@EmptySetToNull @Index @Transient Set<VKey<HostResource>> nsHosts;
|
||||
|
||||
/**
|
||||
* The union of the contacts visible via {@link #getContacts} and {@link #getRegistrant}.
|
||||
@@ -283,9 +285,10 @@ public class DomainContent extends EppResource
|
||||
/**
|
||||
* When the domain's autorenewal status will expire.
|
||||
*
|
||||
* <p>This will be null for the vast majority of domains because all domains autorenew
|
||||
* indefinitely by default and autorenew can only be countermanded by administrators, typically
|
||||
* for reasons of the URS process or termination of a registrar for nonpayment.
|
||||
* <p>This will be {@link DateTimeUtils#END_OF_TIME} for the vast majority of domains because all
|
||||
* domains autorenew indefinitely by default and autorenew can only be countermanded by
|
||||
* administrators, typically for reasons of the URS process or termination of a registrar for
|
||||
* nonpayment.
|
||||
*
|
||||
* <p>When a domain is scheduled to not autorenew, this field is set to the current value of its
|
||||
* {@link #registrationExpirationTime}, after which point the next invocation of a periodic
|
||||
@@ -294,10 +297,16 @@ public class DomainContent extends EppResource
|
||||
* difference domains that have reached their life and must be deleted now, and domains that
|
||||
* happen to be in the autorenew grace period now but should be deleted in roughly a year.
|
||||
*/
|
||||
@Nullable @Index DateTime autorenewEndTime;
|
||||
@Index DateTime autorenewEndTime;
|
||||
|
||||
@OnLoad
|
||||
void load() {
|
||||
// Back fill with correct END_OF_TIME sentinel value.
|
||||
// TODO(mcilwain): Remove this once back-filling is complete.
|
||||
if (autorenewEndTime == null) {
|
||||
autorenewEndTime = END_OF_TIME;
|
||||
}
|
||||
|
||||
// Reconstitute all of the contacts so that they have VKeys.
|
||||
allContacts =
|
||||
allContacts.stream().map(DesignatedContact::reconstitute).collect(toImmutableSet());
|
||||
@@ -319,6 +328,11 @@ public class DomainContent extends EppResource
|
||||
autorenewPollMessageHistoryId = getHistoryId(autorenewPollMessage);
|
||||
autorenewBillingEventHistoryId = getHistoryId(autorenewBillingEvent);
|
||||
deletePollMessageHistoryId = getHistoryId(deletePollMessage);
|
||||
|
||||
// Fix PollMessage VKeys.
|
||||
autorenewPollMessage = PollMessage.Autorenew.convertVKey(autorenewPollMessage);
|
||||
deletePollMessage = PollMessage.OneTime.convertVKey(deletePollMessage);
|
||||
|
||||
dsData =
|
||||
nullToEmptyImmutableCopy(dsData).stream()
|
||||
.map(dsData -> dsData.cloneWithDomainRepoId(getRepoId()))
|
||||
@@ -397,8 +411,20 @@ public class DomainContent extends EppResource
|
||||
return smdId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the autorenew end time if there is one, otherwise empty.
|
||||
*
|
||||
* <p>Note that {@link DateTimeUtils#END_OF_TIME} is used as a sentinel value in the database
|
||||
* representation to signify that autorenew doesn't end, and is mapped to empty here for the
|
||||
* purposes of more legible business logic.
|
||||
*/
|
||||
public Optional<DateTime> getAutorenewEndTime() {
|
||||
return Optional.ofNullable(autorenewEndTime);
|
||||
// TODO(mcilwain): Remove null handling for autorenewEndTime once data migration away from null
|
||||
// is complete.
|
||||
return Optional.ofNullable(
|
||||
(autorenewEndTime == null || autorenewEndTime.equals(END_OF_TIME))
|
||||
? null
|
||||
: autorenewEndTime);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -773,6 +799,8 @@ public class DomainContent extends EppResource
|
||||
} else { // There are nameservers, so make sure INACTIVE isn't there.
|
||||
removeStatusValue(StatusValue.INACTIVE);
|
||||
}
|
||||
// If there is no autorenew end time, set it to END_OF_TIME.
|
||||
instance.autorenewEndTime = firstNonNull(getInstance().autorenewEndTime, END_OF_TIME);
|
||||
|
||||
checkArgumentNotNull(emptyToNull(instance.fullyQualifiedDomainName), "Missing domainName");
|
||||
if (instance.getRegistrant() == null
|
||||
@@ -952,8 +980,15 @@ public class DomainContent extends EppResource
|
||||
return thisCastToDerived();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the autorenew end time, or clears it if empty is passed.
|
||||
*
|
||||
* <p>Note that {@link DateTimeUtils#END_OF_TIME} is used as a sentinel value in the database
|
||||
* representation to signify that autorenew doesn't end, and is mapped to empty here for the
|
||||
* purposes of more legible business logic.
|
||||
*/
|
||||
public B setAutorenewEndTime(Optional<DateTime> autorenewEndTime) {
|
||||
getInstance().autorenewEndTime = autorenewEndTime.orElse(null);
|
||||
getInstance().autorenewEndTime = autorenewEndTime.orElse(END_OF_TIME);
|
||||
return thisCastToDerived();
|
||||
}
|
||||
|
||||
|
||||
@@ -104,6 +104,7 @@ public class DomainHistory extends HistoryEntry implements SqlEntity {
|
||||
"domain_history_history_revision_id,domain_history_domain_repo_id,host_repo_id",
|
||||
unique = true),
|
||||
})
|
||||
@ImmutableObject.EmptySetToNull
|
||||
@Column(name = "host_repo_id")
|
||||
Set<VKey<HostResource>> nsHosts;
|
||||
|
||||
@@ -180,7 +181,9 @@ public class DomainHistory extends HistoryEntry implements SqlEntity {
|
||||
* #getDomainTransactionRecords()}.
|
||||
*/
|
||||
@Access(AccessType.PROPERTY)
|
||||
@OneToMany(cascade = {CascadeType.ALL})
|
||||
@OneToMany(
|
||||
cascade = {CascadeType.ALL},
|
||||
fetch = FetchType.EAGER)
|
||||
@JoinColumn(name = "historyRevisionId", referencedColumnName = "historyRevisionId")
|
||||
@JoinColumn(name = "domainRepoId", referencedColumnName = "domainRepoId")
|
||||
@SuppressWarnings("unused")
|
||||
|
||||
@@ -193,19 +193,6 @@ public class GracePeriod extends GracePeriodBase implements DatastoreAndSqlEntit
|
||||
return clone;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a clone of this {@link GracePeriod} with prepopulated {@link #gracePeriodId} generated
|
||||
* by {@link ObjectifyService#allocateId()}.
|
||||
*
|
||||
* <p>TODO(shicong): Figure out how to generate the id only when the entity is used for Cloud SQL.
|
||||
*/
|
||||
@VisibleForTesting
|
||||
public GracePeriod cloneWithPrepopulatedId() {
|
||||
GracePeriod clone = clone(this);
|
||||
clone.gracePeriodId = ObjectifyService.allocateId();
|
||||
return clone;
|
||||
}
|
||||
|
||||
/** Entity class to represent a historic {@link GracePeriod}. */
|
||||
@Entity(name = "GracePeriodHistory")
|
||||
@Table(indexes = @Index(columnList = "domainRepoId"))
|
||||
|
||||
@@ -35,7 +35,7 @@ import javax.xml.bind.annotation.XmlType;
|
||||
*
|
||||
* @see <a href="http://tools.ietf.org/html/rfc5910">RFC 5910</a>
|
||||
* @see <a href="http://tools.ietf.org/html/rfc4034">RFC 4034</a>
|
||||
* <p>TODO(shicong): Rename this class to DomainDsData.
|
||||
* <p>TODO(b/177567432): Rename this class to DomainDsData.
|
||||
*/
|
||||
@Embed
|
||||
@XmlType(name = "dsData")
|
||||
|
||||
@@ -59,8 +59,8 @@ import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter;
|
||||
public class Address extends ImmutableObject implements Jsonifiable {
|
||||
|
||||
/** The schema validation will enforce that this has 3 lines at most. */
|
||||
// TODO(shicong): Remove this field after migration. We need to figure out how to generate same
|
||||
// XML from streetLine[1,2,3].
|
||||
// TODO(b/177569726): Remove this field after migration. We need to figure out how to generate
|
||||
// same XML from streetLine[1,2,3].
|
||||
@XmlJavaTypeAdapter(NormalizedStringAdapter.class)
|
||||
@Transient
|
||||
List<String> street;
|
||||
@@ -175,7 +175,7 @@ public class Address extends ImmutableObject implements Jsonifiable {
|
||||
* entity from Datastore.
|
||||
*
|
||||
* <p>This callback method is used by Objectify to set streetLine[1,2,3] fields as they are not
|
||||
* persisted in the Datastore. TODO(shicong): Delete this method after database migration.
|
||||
* persisted in the Datastore.
|
||||
*/
|
||||
void onLoad(@AlsoLoad("street") List<String> street) {
|
||||
mapStreetListToIndividualFields(street);
|
||||
|
||||
@@ -30,6 +30,7 @@ import com.google.common.collect.Streams;
|
||||
import com.googlecode.objectify.Key;
|
||||
import com.googlecode.objectify.Result;
|
||||
import google.registry.model.contact.ContactHistory;
|
||||
import google.registry.model.domain.DomainHistory;
|
||||
import google.registry.model.host.HostHistory;
|
||||
import google.registry.model.reporting.HistoryEntry;
|
||||
import google.registry.persistence.VKey;
|
||||
@@ -292,6 +293,21 @@ public class DatastoreTransactionManager implements TransactionManager {
|
||||
getOfy().clearSessionCache();
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes the given {@link Result} instance synchronously if not in a transaction.
|
||||
*
|
||||
* <p>The {@link Result} instance contains a task that will be executed by Objectify
|
||||
* asynchronously. If it is in a transaction, we don't need to execute the task immediately
|
||||
* because it is guaranteed to be done by the end of the transaction. However, if it is not in a
|
||||
* transaction, we need to execute it in case the following code expects that happens before
|
||||
* themselves.
|
||||
*/
|
||||
private void syncIfTransactionless(Result<?> result) {
|
||||
if (!inTransaction()) {
|
||||
result.now();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The following three methods exist due to the migration to Cloud SQL.
|
||||
*
|
||||
@@ -309,34 +325,23 @@ public class DatastoreTransactionManager implements TransactionManager {
|
||||
syncIfTransactionless(getOfy().save().entity(entity));
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private <T> T toChildHistoryEntryIfPossible(@Nullable T obj) {
|
||||
// NB: The Key of the object in question may not necessarily be the resulting class that we
|
||||
// wish to have. Because all *History classes are @EntitySubclasses, their Keys will have type
|
||||
// HistoryEntry -- even if you create them based off the *History class.
|
||||
if (obj != null && HistoryEntry.class.isAssignableFrom(obj.getClass())) {
|
||||
return (T) ((HistoryEntry) obj).toChildHistoryEntity();
|
||||
}
|
||||
return obj;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private <T> T loadNullable(VKey<T> key) {
|
||||
return toChildHistoryEntryIfPossible(getOfy().load().key(key.getOfyKey()).now());
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes the given {@link Result} instance synchronously if not in a transaction.
|
||||
*
|
||||
* <p>The {@link Result} instance contains a task that will be executed by Objectify
|
||||
* asynchronously. If it is in a transaction, we don't need to execute the task immediately
|
||||
* because it is guaranteed to be done by the end of the transaction. However, if it is not in a
|
||||
* transaction, we need to execute it in case the following code expects that happens before
|
||||
* themselves.
|
||||
*/
|
||||
private void syncIfTransactionless(Result<?> result) {
|
||||
if (!inTransaction()) {
|
||||
result.now();
|
||||
/** Converts a nonnull {@link HistoryEntry} to the child format, e.g. {@link DomainHistory} */
|
||||
@SuppressWarnings("unchecked")
|
||||
public static <T> T toChildHistoryEntryIfPossible(@Nullable T obj) {
|
||||
// NB: The Key of the object in question may not necessarily be the resulting class that we
|
||||
// wish to have. Because all *History classes are @EntitySubclasses, their Keys will have type
|
||||
// HistoryEntry -- even if you create them based off the *History class.
|
||||
if (obj instanceof HistoryEntry
|
||||
&& !(obj instanceof ContactHistory)
|
||||
&& !(obj instanceof DomainHistory)
|
||||
&& !(obj instanceof HostHistory)) {
|
||||
return (T) ((HistoryEntry) obj).toChildHistoryEntity();
|
||||
}
|
||||
return obj;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,7 +14,18 @@
|
||||
|
||||
package google.registry.model.ofy;
|
||||
|
||||
import static google.registry.model.ofy.EntityWritePriorities.getEntityPriority;
|
||||
import static google.registry.model.ofy.ObjectifyService.ofy;
|
||||
import static google.registry.persistence.transaction.TransactionManagerFactory.jpaTm;
|
||||
|
||||
import com.google.common.collect.ImmutableMap;
|
||||
import com.googlecode.objectify.Key;
|
||||
import google.registry.config.RegistryEnvironment;
|
||||
import google.registry.model.UpdateAutoTimestamp;
|
||||
import google.registry.persistence.VKey;
|
||||
import google.registry.schema.replay.DatastoreEntity;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentLinkedQueue;
|
||||
|
||||
/**
|
||||
@@ -24,23 +35,79 @@ import java.util.concurrent.ConcurrentLinkedQueue;
|
||||
*/
|
||||
public class ReplayQueue {
|
||||
|
||||
static ConcurrentLinkedQueue<TransactionInfo> queue =
|
||||
new ConcurrentLinkedQueue<TransactionInfo>();
|
||||
static ConcurrentLinkedQueue<ImmutableMap<Key<?>, Object>> queue =
|
||||
new ConcurrentLinkedQueue<ImmutableMap<Key<?>, Object>>();
|
||||
|
||||
static void addInTests(TransactionInfo info) {
|
||||
if (RegistryEnvironment.get() == RegistryEnvironment.UNITTEST) {
|
||||
queue.add(info);
|
||||
// Transform the entities to be persisted to the set of values as they were actually
|
||||
// persisted.
|
||||
ImmutableMap.Builder<Key<?>, Object> builder = new ImmutableMap.Builder<Key<?>, Object>();
|
||||
for (ImmutableMap.Entry<Key<?>, Object> entry : info.getChanges().entrySet()) {
|
||||
if (entry.getValue().equals(TransactionInfo.Delete.SENTINEL)) {
|
||||
builder.put(entry.getKey(), entry.getValue());
|
||||
} else {
|
||||
// The value is an entity object that has not yet been persisted, and thus some of the
|
||||
// special transformations that we do (notably the auto-timestamp transformations) have
|
||||
// not been applied. Converting the object to an entity and then back again performs
|
||||
// those transformations so that we persist the same values to SQL that we have in
|
||||
// Datastore.
|
||||
builder.put(entry.getKey(), ofy().toPojo(ofy().toEntity(entry.getValue())));
|
||||
}
|
||||
}
|
||||
queue.add(builder.build());
|
||||
}
|
||||
}
|
||||
|
||||
public static void replay() {
|
||||
TransactionInfo info;
|
||||
while ((info = queue.poll()) != null) {
|
||||
info.saveToJpa();
|
||||
/** Replay all transactions, return the set of keys that were replayed. */
|
||||
public static ImmutableMap<Key<?>, Object> replay() {
|
||||
// We can't use an ImmutableMap.Builder here, we need to be able to overwrite existing values
|
||||
// and the builder doesn't support that.
|
||||
Map<Key<?>, Object> result = new HashMap<Key<?>, Object>();
|
||||
ImmutableMap<Key<?>, Object> changes;
|
||||
while ((changes = queue.poll()) != null) {
|
||||
saveToJpa(changes);
|
||||
result.putAll(changes);
|
||||
}
|
||||
|
||||
return ImmutableMap.copyOf(result);
|
||||
}
|
||||
|
||||
public static void clear() {
|
||||
queue.clear();
|
||||
}
|
||||
|
||||
/** Returns the priority of the entity type in the map entry. */
|
||||
private static int getPriority(ImmutableMap.Entry<Key<?>, Object> entry) {
|
||||
return getEntityPriority(
|
||||
entry.getKey().getKind(), entry.getValue().equals(TransactionInfo.Delete.SENTINEL));
|
||||
}
|
||||
|
||||
private static int compareByPriority(
|
||||
ImmutableMap.Entry<Key<?>, Object> a, ImmutableMap.Entry<Key<?>, Object> b) {
|
||||
return getPriority(a) - getPriority(b);
|
||||
}
|
||||
|
||||
private static void saveToJpa(ImmutableMap<Key<?>, Object> changes) {
|
||||
try (UpdateAutoTimestamp.DisableAutoUpdateResource disabler =
|
||||
UpdateAutoTimestamp.disableAutoUpdate()) {
|
||||
// Sort the changes into an order that will work for insertion into the database.
|
||||
jpaTm()
|
||||
.transact(
|
||||
() -> {
|
||||
changes.entrySet().stream()
|
||||
.sorted(ReplayQueue::compareByPriority)
|
||||
.forEach(
|
||||
entry -> {
|
||||
if (entry.getValue().equals(TransactionInfo.Delete.SENTINEL)) {
|
||||
jpaTm().delete(VKey.from(entry.getKey()));
|
||||
} else {
|
||||
((DatastoreEntity) entry.getValue())
|
||||
.toSqlEntity()
|
||||
.ifPresent(jpaTm()::put);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,24 +20,20 @@ import static com.google.common.collect.ImmutableSet.toImmutableSet;
|
||||
import static com.google.common.collect.Maps.filterValues;
|
||||
import static com.google.common.collect.Maps.toMap;
|
||||
import static google.registry.model.ofy.CommitLogBucket.getArbitraryBucketId;
|
||||
import static google.registry.model.ofy.EntityWritePriorities.getEntityPriority;
|
||||
import static google.registry.model.ofy.ObjectifyService.ofy;
|
||||
import static google.registry.persistence.transaction.TransactionManagerFactory.jpaTm;
|
||||
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
import com.google.common.collect.ImmutableMap;
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
import com.googlecode.objectify.Key;
|
||||
import google.registry.persistence.VKey;
|
||||
import google.registry.schema.replay.DatastoreEntity;
|
||||
import java.util.Map;
|
||||
import org.joda.time.DateTime;
|
||||
|
||||
/** Metadata for an {@link Ofy} transaction that saves commit logs. */
|
||||
class TransactionInfo {
|
||||
public class TransactionInfo {
|
||||
|
||||
@VisibleForTesting
|
||||
enum Delete {
|
||||
public enum Delete {
|
||||
SENTINEL
|
||||
}
|
||||
|
||||
@@ -87,6 +83,10 @@ class TransactionInfo {
|
||||
return ImmutableSet.copyOf(changesBuilder.build().keySet());
|
||||
}
|
||||
|
||||
ImmutableMap<Key<?>, Object> getChanges() {
|
||||
return changesBuilder.build();
|
||||
}
|
||||
|
||||
ImmutableSet<Key<?>> getDeletes() {
|
||||
return ImmutableSet.copyOf(
|
||||
filterValues(changesBuilder.build(), Delete.SENTINEL::equals).keySet());
|
||||
@@ -100,35 +100,4 @@ class TransactionInfo {
|
||||
.filter(not(Delete.SENTINEL::equals))
|
||||
.collect(toImmutableSet());
|
||||
}
|
||||
|
||||
/** Returns the weight of the entity type in the map entry. */
|
||||
@VisibleForTesting
|
||||
static int getWeight(ImmutableMap.Entry<Key<?>, Object> entry) {
|
||||
return getEntityPriority(entry.getKey().getKind(), entry.getValue().equals(Delete.SENTINEL));
|
||||
}
|
||||
|
||||
private static int compareByWeight(
|
||||
ImmutableMap.Entry<Key<?>, Object> a, ImmutableMap.Entry<Key<?>, Object> b) {
|
||||
return getWeight(a) - getWeight(b);
|
||||
}
|
||||
|
||||
void saveToJpa() {
|
||||
// Sort the changes into an order that will work for insertion into the database.
|
||||
jpaTm()
|
||||
.transact(
|
||||
() -> {
|
||||
changesBuilder.build().entrySet().stream()
|
||||
.sorted(TransactionInfo::compareByWeight)
|
||||
.forEach(
|
||||
entry -> {
|
||||
if (entry.getValue().equals(Delete.SENTINEL)) {
|
||||
jpaTm().delete(VKey.from(entry.getKey()));
|
||||
} else {
|
||||
((DatastoreEntity) entry.getValue())
|
||||
.toSqlEntity()
|
||||
.ifPresent(jpaTm()::put);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ package google.registry.model.poll;
|
||||
|
||||
import static com.google.common.collect.ImmutableList.toImmutableList;
|
||||
import static google.registry.util.CollectionUtils.forceEmptyToNull;
|
||||
import static google.registry.util.CollectionUtils.isNullOrEmpty;
|
||||
import static google.registry.util.CollectionUtils.nullToEmpty;
|
||||
import static google.registry.util.DateTimeUtils.END_OF_TIME;
|
||||
import static google.registry.util.PreconditionsUtils.checkArgumentNotNull;
|
||||
@@ -52,6 +53,7 @@ import google.registry.persistence.WithLongVKey;
|
||||
import google.registry.schema.replay.DatastoreAndSqlEntity;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import javax.annotation.Nullable;
|
||||
import javax.persistence.AttributeOverride;
|
||||
import javax.persistence.AttributeOverrides;
|
||||
import javax.persistence.Column;
|
||||
@@ -185,6 +187,7 @@ public abstract class PollMessage extends ImmutableObject
|
||||
@Override
|
||||
public abstract VKey<? extends PollMessage> createVKey();
|
||||
|
||||
/** Static VKey factory method for use by VKeyTranslatorFactory. */
|
||||
public static VKey<PollMessage> createVKey(Key<PollMessage> key) {
|
||||
return VKey.create(PollMessage.class, key.getId(), key);
|
||||
}
|
||||
@@ -289,7 +292,7 @@ public abstract class PollMessage extends ImmutableObject
|
||||
|
||||
@Transient List<ContactTransferResponse> contactTransferResponses;
|
||||
|
||||
@Transient
|
||||
@Transient @ImmutableObject.DoNotCompare
|
||||
List<DomainPendingActionNotificationResponse> domainPendingActionNotificationResponses;
|
||||
|
||||
@Transient List<DomainTransferResponse> domainTransferResponses;
|
||||
@@ -355,6 +358,11 @@ public abstract class PollMessage extends ImmutableObject
|
||||
return VKey.create(OneTime.class, getId(), Key.create(this));
|
||||
}
|
||||
|
||||
/** Converts an unspecialized VKey<PollMessage> to a VKey of the derived class. */
|
||||
public static @Nullable VKey<OneTime> convertVKey(@Nullable VKey<OneTime> key) {
|
||||
return key == null ? null : VKey.create(OneTime.class, key.getSqlKey(), key.getOfyKey());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Builder asBuilder() {
|
||||
return new Builder(clone(this));
|
||||
@@ -371,6 +379,47 @@ public abstract class PollMessage extends ImmutableObject
|
||||
.build();
|
||||
}
|
||||
|
||||
@Override
|
||||
@OnLoad
|
||||
void onLoad() {
|
||||
super.onLoad();
|
||||
if (!isNullOrEmpty(contactPendingActionNotificationResponses)) {
|
||||
pendingActionNotificationResponse = contactPendingActionNotificationResponses.get(0);
|
||||
}
|
||||
if (!isNullOrEmpty(contactTransferResponses)) {
|
||||
contactId = contactTransferResponses.get(0).getContactId();
|
||||
transferResponse = contactTransferResponses.get(0);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@PostLoad
|
||||
void postLoad() {
|
||||
super.postLoad();
|
||||
if (pendingActionNotificationResponse != null) {
|
||||
contactPendingActionNotificationResponses =
|
||||
ImmutableList.of(
|
||||
ContactPendingActionNotificationResponse.create(
|
||||
pendingActionNotificationResponse.nameOrId.value,
|
||||
pendingActionNotificationResponse.getActionResult(),
|
||||
pendingActionNotificationResponse.getTrid(),
|
||||
pendingActionNotificationResponse.processedDate));
|
||||
}
|
||||
if (contactId != null && transferResponse != null) {
|
||||
contactTransferResponses =
|
||||
ImmutableList.of(
|
||||
new ContactTransferResponse.Builder()
|
||||
.setContactId(contactId)
|
||||
.setGainingClientId(transferResponse.getGainingClientId())
|
||||
.setLosingClientId(transferResponse.getLosingClientId())
|
||||
.setTransferStatus(transferResponse.getTransferStatus())
|
||||
.setTransferRequestTime(transferResponse.getTransferRequestTime())
|
||||
.setPendingTransferExpirationTime(
|
||||
transferResponse.getPendingTransferExpirationTime())
|
||||
.build());
|
||||
}
|
||||
}
|
||||
|
||||
/** A builder for {@link OneTime} since it is immutable. */
|
||||
public static class Builder extends PollMessage.Builder<OneTime, Builder> {
|
||||
|
||||
@@ -389,6 +438,10 @@ public abstract class PollMessage extends ImmutableObject
|
||||
.filter(ContactPendingActionNotificationResponse.class::isInstance)
|
||||
.map(ContactPendingActionNotificationResponse.class::cast)
|
||||
.collect(toImmutableList()));
|
||||
if (getInstance().contactPendingActionNotificationResponses != null) {
|
||||
getInstance().pendingActionNotificationResponse =
|
||||
getInstance().contactPendingActionNotificationResponses.get(0);
|
||||
}
|
||||
getInstance().contactTransferResponses =
|
||||
forceEmptyToNull(
|
||||
responseData
|
||||
@@ -396,6 +449,11 @@ public abstract class PollMessage extends ImmutableObject
|
||||
.filter(ContactTransferResponse.class::isInstance)
|
||||
.map(ContactTransferResponse.class::cast)
|
||||
.collect(toImmutableList()));
|
||||
if (getInstance().contactTransferResponses != null) {
|
||||
getInstance().contactId = getInstance().contactTransferResponses.get(0).getContactId();
|
||||
getInstance().transferResponse = getInstance().contactTransferResponses.get(0);
|
||||
}
|
||||
|
||||
getInstance().domainPendingActionNotificationResponses =
|
||||
forceEmptyToNull(
|
||||
responseData
|
||||
@@ -456,6 +514,11 @@ public abstract class PollMessage extends ImmutableObject
|
||||
return VKey.create(Autorenew.class, getId(), Key.create(this));
|
||||
}
|
||||
|
||||
/** Converts an unspecialized VKey<PollMessage> to a VKey of the derived class. */
|
||||
public static @Nullable VKey<Autorenew> convertVKey(VKey<Autorenew> key) {
|
||||
return key == null ? null : VKey.create(Autorenew.class, key.getSqlKey(), key.getOfyKey());
|
||||
}
|
||||
|
||||
@Override
|
||||
public ImmutableList<ResponseData> getResponseData() {
|
||||
// Note that the event time is when the auto-renew occured, so the expiration time in the
|
||||
|
||||
@@ -234,7 +234,7 @@ public class Registrar extends ImmutableObject
|
||||
* Unique registrar client id. Must conform to "clIDType" as defined in RFC5730.
|
||||
*
|
||||
* @see <a href="http://tools.ietf.org/html/rfc5730#section-4.2">Shared Structure Schema</a>
|
||||
* <p>TODO(shicong): Rename this field to clientId
|
||||
* <p>TODO(b/177568946): Rename this field to registrarId.
|
||||
*/
|
||||
@Id
|
||||
@javax.persistence.Id
|
||||
|
||||
@@ -49,7 +49,9 @@ public class DomainTransactionRecord extends ImmutableObject
|
||||
|
||||
@Id
|
||||
@Ignore
|
||||
@ImmutableObject.DoNotCompare
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
@ImmutableObject.Insignificant
|
||||
Long id;
|
||||
|
||||
/** The TLD this record operates on. */
|
||||
|
||||
@@ -198,6 +198,7 @@ public class HistoryEntry extends ImmutableObject implements Buildable, Datastor
|
||||
* transaction counts (such as contact or host mutations).
|
||||
*/
|
||||
@Transient // domain-specific
|
||||
@ImmutableObject.EmptySetToNull
|
||||
protected Set<DomainTransactionRecord> domainTransactionRecords;
|
||||
|
||||
public long getId() {
|
||||
|
||||
@@ -0,0 +1,144 @@
|
||||
// Copyright 2020 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.model.reporting;
|
||||
|
||||
import static com.google.common.collect.ImmutableList.toImmutableList;
|
||||
import static google.registry.model.ofy.ObjectifyService.ofy;
|
||||
import static google.registry.persistence.transaction.TransactionManagerFactory.jpaTm;
|
||||
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
|
||||
import static google.registry.util.DateTimeUtils.END_OF_TIME;
|
||||
import static google.registry.util.DateTimeUtils.START_OF_TIME;
|
||||
|
||||
import com.google.common.collect.Iterables;
|
||||
import google.registry.model.EppResource;
|
||||
import google.registry.model.contact.ContactHistory;
|
||||
import google.registry.model.contact.ContactResource;
|
||||
import google.registry.model.domain.DomainBase;
|
||||
import google.registry.model.domain.DomainHistory;
|
||||
import google.registry.model.host.HostHistory;
|
||||
import google.registry.model.host.HostResource;
|
||||
import google.registry.persistence.VKey;
|
||||
import java.util.Comparator;
|
||||
import javax.persistence.EntityManager;
|
||||
import org.joda.time.DateTime;
|
||||
|
||||
/**
|
||||
* Retrieves {@link HistoryEntry} descendants (e.g. {@link DomainHistory}).
|
||||
*
|
||||
* <p>This class is configured to retrieve either from Datastore or SQL, depending on which database
|
||||
* is currently considered the primary database.
|
||||
*/
|
||||
public class HistoryEntryDao {
|
||||
|
||||
/** Loads all history objects in the times specified, including all types. */
|
||||
public static Iterable<? extends HistoryEntry> loadAllHistoryObjects(
|
||||
DateTime afterTime, DateTime beforeTime) {
|
||||
if (tm().isOfy()) {
|
||||
return ofy()
|
||||
.load()
|
||||
.type(HistoryEntry.class)
|
||||
.order("modificationTime")
|
||||
.filter("modificationTime >=", afterTime)
|
||||
.filter("modificationTime <=", beforeTime);
|
||||
} else {
|
||||
return jpaTm()
|
||||
.transact(
|
||||
() ->
|
||||
Iterables.concat(
|
||||
loadAllHistoryObjectsFromSql(ContactHistory.class, afterTime, beforeTime),
|
||||
loadAllHistoryObjectsFromSql(DomainHistory.class, afterTime, beforeTime),
|
||||
loadAllHistoryObjectsFromSql(HostHistory.class, afterTime, beforeTime)));
|
||||
}
|
||||
}
|
||||
|
||||
/** Loads all history objects corresponding to the given {@link EppResource}. */
|
||||
public static Iterable<? extends HistoryEntry> loadHistoryObjectsForResource(
|
||||
VKey<? extends EppResource> parentKey) {
|
||||
return loadHistoryObjectsForResource(parentKey, START_OF_TIME, END_OF_TIME);
|
||||
}
|
||||
|
||||
/** Loads all history objects in the time period specified for the given {@link EppResource}. */
|
||||
public static Iterable<? extends HistoryEntry> loadHistoryObjectsForResource(
|
||||
VKey<? extends EppResource> parentKey, DateTime afterTime, DateTime beforeTime) {
|
||||
if (tm().isOfy()) {
|
||||
return ofy()
|
||||
.load()
|
||||
.type(HistoryEntry.class)
|
||||
.ancestor(parentKey.getOfyKey())
|
||||
.order("modificationTime")
|
||||
.filter("modificationTime >=", afterTime)
|
||||
.filter("modificationTime <=", beforeTime);
|
||||
} else {
|
||||
return jpaTm()
|
||||
.transact(() -> loadHistoryObjectsForResourceFromSql(parentKey, afterTime, beforeTime));
|
||||
}
|
||||
}
|
||||
|
||||
private static Iterable<? extends HistoryEntry> loadHistoryObjectsForResourceFromSql(
|
||||
VKey<? extends EppResource> parentKey, DateTime afterTime, DateTime beforeTime) {
|
||||
Class<? extends HistoryEntry> historyClass = getHistoryClassFromParent(parentKey.getKind());
|
||||
String repoIdFieldName = getRepoIdFieldNameFromHistoryClass(historyClass);
|
||||
EntityManager entityManager = jpaTm().getEntityManager();
|
||||
String tableName = entityManager.getMetamodel().entity(historyClass).getName();
|
||||
String queryString =
|
||||
String.format(
|
||||
"SELECT entry FROM %s entry WHERE entry.modificationTime >= :afterTime AND "
|
||||
+ "entry.modificationTime <= :beforeTime AND entry.%s = :parentKey",
|
||||
tableName, repoIdFieldName);
|
||||
return entityManager
|
||||
.createQuery(queryString, historyClass)
|
||||
.setParameter("afterTime", afterTime)
|
||||
.setParameter("beforeTime", beforeTime)
|
||||
.setParameter("parentKey", parentKey.getSqlKey().toString())
|
||||
.getResultStream()
|
||||
.sorted(Comparator.comparing(HistoryEntry::getModificationTime))
|
||||
.collect(toImmutableList());
|
||||
}
|
||||
|
||||
private static Class<? extends HistoryEntry> getHistoryClassFromParent(
|
||||
Class<? extends EppResource> parent) {
|
||||
if (parent.equals(ContactResource.class)) {
|
||||
return ContactHistory.class;
|
||||
} else if (parent.equals(DomainBase.class)) {
|
||||
return DomainHistory.class;
|
||||
} else if (parent.equals(HostResource.class)) {
|
||||
return HostHistory.class;
|
||||
}
|
||||
throw new IllegalArgumentException(
|
||||
String.format("Unknown history type for parent %s", parent.getName()));
|
||||
}
|
||||
|
||||
private static String getRepoIdFieldNameFromHistoryClass(
|
||||
Class<? extends HistoryEntry> historyClass) {
|
||||
return historyClass.equals(ContactHistory.class)
|
||||
? "contactRepoId"
|
||||
: historyClass.equals(DomainHistory.class) ? "domainRepoId" : "hostRepoId";
|
||||
}
|
||||
|
||||
private static Iterable<? extends HistoryEntry> loadAllHistoryObjectsFromSql(
|
||||
Class<? extends HistoryEntry> historyClass, DateTime afterTime, DateTime beforeTime) {
|
||||
EntityManager entityManager = jpaTm().getEntityManager();
|
||||
return entityManager
|
||||
.createQuery(
|
||||
String.format(
|
||||
"SELECT entry FROM %s entry WHERE entry.modificationTime >= :afterTime AND "
|
||||
+ "entry.modificationTime <= :beforeTime",
|
||||
entityManager.getMetamodel().entity(historyClass).getName()),
|
||||
historyClass)
|
||||
.setParameter("afterTime", afterTime)
|
||||
.setParameter("beforeTime", beforeTime)
|
||||
.getResultList();
|
||||
}
|
||||
}
|
||||
@@ -58,7 +58,7 @@ public class ClaimsListDao {
|
||||
|
||||
/**
|
||||
* Returns the most recent revision of the {@link ClaimsListShard} in Cloud SQL, if it exists.
|
||||
* TODO(shicong): Change this method to package level access after dual-read phase.
|
||||
* TODO(b/177569979): Change this method to package level access after dual-read phase.
|
||||
* ClaimsListShard uses this method to retrieve claims list in Cloud SQL for the comparison, and
|
||||
* ClaimsListShard is not in this package.
|
||||
*/
|
||||
|
||||
@@ -14,15 +14,25 @@
|
||||
|
||||
package google.registry.model.transfer;
|
||||
|
||||
import static com.google.common.collect.ImmutableList.toImmutableList;
|
||||
import static google.registry.util.CollectionUtils.isNullOrEmpty;
|
||||
import static google.registry.util.CollectionUtils.nullToEmpty;
|
||||
import static google.registry.util.CollectionUtils.nullToEmptyImmutableCopy;
|
||||
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
import com.googlecode.objectify.Key;
|
||||
import com.googlecode.objectify.annotation.AlsoLoad;
|
||||
import com.googlecode.objectify.annotation.Ignore;
|
||||
import com.googlecode.objectify.annotation.IgnoreSave;
|
||||
import com.googlecode.objectify.condition.IfNull;
|
||||
import google.registry.model.Buildable;
|
||||
import google.registry.model.EppResource;
|
||||
import google.registry.model.contact.ContactResource;
|
||||
import google.registry.model.domain.DomainBase;
|
||||
import google.registry.model.eppcommon.Trid;
|
||||
import google.registry.model.poll.PollMessage;
|
||||
import google.registry.model.reporting.HistoryEntry;
|
||||
import google.registry.persistence.VKey;
|
||||
import google.registry.util.TypeUtils.TypeInstantiator;
|
||||
import java.util.Set;
|
||||
@@ -32,6 +42,7 @@ import javax.persistence.AttributeOverrides;
|
||||
import javax.persistence.Column;
|
||||
import javax.persistence.Embedded;
|
||||
import javax.persistence.MappedSuperclass;
|
||||
import javax.persistence.PostLoad;
|
||||
import javax.persistence.Transient;
|
||||
|
||||
/**
|
||||
@@ -68,15 +79,27 @@ public abstract class TransferData<
|
||||
@IgnoreSave(IfNull.class)
|
||||
Set<VKey<? extends TransferServerApproveEntity>> serverApproveEntities;
|
||||
|
||||
// The following 3 fields are the replacement for serverApproveEntities in Cloud SQL.
|
||||
// TODO(shicong): Add getter/setter for these 3 fields and use them in the application code.
|
||||
@Ignore
|
||||
@Column(name = "transfer_gaining_poll_message_id")
|
||||
Long gainingTransferPollMessageId;
|
||||
@Column(name = "transfer_repo_id")
|
||||
String repoId;
|
||||
|
||||
@Ignore
|
||||
@Column(name = "transfer_losing_poll_message_id")
|
||||
Long losingTransferPollMessageId;
|
||||
@Column(name = "transfer_history_entry_id")
|
||||
Long historyEntryId;
|
||||
|
||||
// The pollMessageId1 and pollMessageId2 are used to store the IDs for gaining and losing poll
|
||||
// messages in Cloud SQL, and they are added to replace the VKeys in serverApproveEntities.
|
||||
// Although we can distinguish which is which when we construct the TransferData instance from
|
||||
// the transfer request flow, when the instance is loaded from Datastore, we cannot make this
|
||||
// distinction because they are just VKeys. Also, the only way we use serverApproveEntities is to
|
||||
// just delete all the entities referenced by the VKeys, so we don't need to make the distinction.
|
||||
@Ignore
|
||||
@Column(name = "transfer_poll_message_id_1")
|
||||
Long pollMessageId1;
|
||||
|
||||
@Ignore
|
||||
@Column(name = "transfer_poll_message_id_2")
|
||||
Long pollMessageId2;
|
||||
|
||||
public abstract boolean isEmpty();
|
||||
|
||||
@@ -116,6 +139,83 @@ public abstract class TransferData<
|
||||
return newBuilder;
|
||||
}
|
||||
|
||||
void onLoad(
|
||||
@AlsoLoad("serverApproveEntities")
|
||||
Set<VKey<? extends TransferServerApproveEntity>> serverApproveEntities) {
|
||||
mapServerApproveEntitiesToFields(serverApproveEntities, this);
|
||||
}
|
||||
|
||||
@PostLoad
|
||||
void postLoad() {
|
||||
mapFieldsToServerApproveEntities();
|
||||
}
|
||||
|
||||
/**
|
||||
* Reconstructs serverApproveEntities set from the individual fields, e.g. repoId, historyEntryId,
|
||||
* pollMessageId1.
|
||||
*/
|
||||
void mapFieldsToServerApproveEntities() {
|
||||
if (repoId == null) {
|
||||
return;
|
||||
}
|
||||
Key<? extends EppResource> eppKey;
|
||||
if (getClass().equals(DomainBase.class)) {
|
||||
eppKey = Key.create(DomainBase.class, repoId);
|
||||
} else {
|
||||
eppKey = Key.create(ContactResource.class, repoId);
|
||||
}
|
||||
Key<HistoryEntry> historyEntryKey = Key.create(eppKey, HistoryEntry.class, historyEntryId);
|
||||
ImmutableSet.Builder<VKey<? extends TransferServerApproveEntity>> entityKeysBuilder =
|
||||
new ImmutableSet.Builder<>();
|
||||
if (pollMessageId1 != null) {
|
||||
Key<PollMessage> ofyKey = Key.create(historyEntryKey, PollMessage.class, pollMessageId1);
|
||||
entityKeysBuilder.add(PollMessage.createVKey(ofyKey));
|
||||
}
|
||||
if (pollMessageId2 != null) {
|
||||
Key<PollMessage> ofyKey = Key.create(historyEntryKey, PollMessage.class, pollMessageId2);
|
||||
entityKeysBuilder.add(PollMessage.createVKey(ofyKey));
|
||||
}
|
||||
serverApproveEntities = entityKeysBuilder.build();
|
||||
}
|
||||
|
||||
/** Maps serverApproveEntities set to the individual fields. */
|
||||
static void mapServerApproveEntitiesToFields(
|
||||
Set<VKey<? extends TransferServerApproveEntity>> serverApproveEntities,
|
||||
TransferData transferData) {
|
||||
if (isNullOrEmpty(serverApproveEntities)) {
|
||||
transferData.historyEntryId = null;
|
||||
transferData.repoId = null;
|
||||
transferData.pollMessageId1 = null;
|
||||
transferData.pollMessageId2 = null;
|
||||
return;
|
||||
}
|
||||
// Each element in serverApproveEntities should have the exact same Key<HistoryEntry> as its
|
||||
// parent. So, we can use any to set historyEntryId and repoId.
|
||||
Key<?> key = serverApproveEntities.iterator().next().getOfyKey();
|
||||
transferData.historyEntryId = key.getParent().getId();
|
||||
transferData.repoId = key.getParent().getParent().getName();
|
||||
|
||||
ImmutableList<Long> sortedPollMessageIds = getSortedPollMessageIds(serverApproveEntities);
|
||||
if (sortedPollMessageIds.size() >= 1) {
|
||||
transferData.pollMessageId1 = sortedPollMessageIds.get(0);
|
||||
}
|
||||
if (sortedPollMessageIds.size() >= 2) {
|
||||
transferData.pollMessageId2 = sortedPollMessageIds.get(1);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets poll message IDs from the given serverApproveEntities and sorted the IDs in natural order.
|
||||
*/
|
||||
private static ImmutableList<Long> getSortedPollMessageIds(
|
||||
Set<VKey<? extends TransferServerApproveEntity>> serverApproveEntities) {
|
||||
return nullToEmpty(serverApproveEntities).stream()
|
||||
.filter(vKey -> PollMessage.class.isAssignableFrom(vKey.getKind()))
|
||||
.map(vKey -> (long) vKey.getSqlKey())
|
||||
.sorted()
|
||||
.collect(toImmutableList());
|
||||
}
|
||||
|
||||
/** Builder for {@link TransferData} because it is immutable. */
|
||||
public abstract static class Builder<T extends TransferData, B extends Builder<T, B>>
|
||||
extends BaseTransferObject.Builder<T, B> {
|
||||
@@ -141,6 +241,7 @@ public abstract class TransferData<
|
||||
|
||||
@Override
|
||||
public T build() {
|
||||
mapServerApproveEntitiesToFields(getInstance().serverApproveEntities, getInstance());
|
||||
return super.build();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -351,8 +351,8 @@ public abstract class PersistenceModule {
|
||||
@interface BeamPipelineCloudSqlConfigs {}
|
||||
|
||||
/** Dagger qualifier for the default Hibernate configurations. */
|
||||
// TODO(shicong): Change annotations in this class to none public or put them in a top level
|
||||
// package
|
||||
// TODO(b/177568911): Change annotations in this class to none public or put them in a top level
|
||||
// package.
|
||||
@Qualifier
|
||||
@Documented
|
||||
public @interface DefaultHibernateConfigs {}
|
||||
|
||||
-1
@@ -20,7 +20,6 @@ import javax.persistence.Converter;
|
||||
|
||||
/**
|
||||
* JPA {@link AttributeConverter} for storing/retrieving {@code List<CidrAddressBlock>} objects.
|
||||
* TODO(shicong): Investigate if we can have one converter for any List type
|
||||
*/
|
||||
@Converter(autoApply = true)
|
||||
public class CidrAddressBlockListConverter extends StringListConverterBase<CidrAddressBlock> {
|
||||
|
||||
+28
-11
@@ -18,6 +18,7 @@ import static com.google.common.base.Preconditions.checkArgument;
|
||||
import static com.google.common.collect.ImmutableList.toImmutableList;
|
||||
import static com.google.common.collect.ImmutableMap.toImmutableMap;
|
||||
import static com.google.common.collect.ImmutableSet.toImmutableSet;
|
||||
import static google.registry.model.ofy.DatastoreTransactionManager.toChildHistoryEntryIfPossible;
|
||||
import static google.registry.util.PreconditionsUtils.checkArgumentNotNull;
|
||||
import static java.util.AbstractMap.SimpleEntry;
|
||||
import static java.util.stream.Collectors.joining;
|
||||
@@ -34,6 +35,7 @@ import google.registry.model.index.EppResourceIndex;
|
||||
import google.registry.model.index.ForeignKeyIndex.ForeignKeyContactIndex;
|
||||
import google.registry.model.index.ForeignKeyIndex.ForeignKeyDomainIndex;
|
||||
import google.registry.model.index.ForeignKeyIndex.ForeignKeyHostIndex;
|
||||
import google.registry.model.ofy.DatastoreTransactionManager;
|
||||
import google.registry.persistence.JpaRetries;
|
||||
import google.registry.persistence.VKey;
|
||||
import google.registry.util.Clock;
|
||||
@@ -76,8 +78,9 @@ public class JpaTransactionManagerImpl implements JpaTransactionManager {
|
||||
// EntityManagerFactory is thread safe.
|
||||
private final EntityManagerFactory emf;
|
||||
private final Clock clock;
|
||||
// TODO(shicong): Investigate alternatives for managing transaction information. ThreadLocal adds
|
||||
// an unnecessary restriction that each request has to be processed by one thread synchronously.
|
||||
// TODO(b/177588434): Investigate alternatives for managing transaction information. ThreadLocal
|
||||
// adds an unnecessary restriction that each request has to be processed by one thread
|
||||
// synchronously.
|
||||
private final ThreadLocal<TransactionInfo> transactionInfo =
|
||||
ThreadLocal.withInitial(TransactionInfo::new);
|
||||
|
||||
@@ -115,8 +118,6 @@ public class JpaTransactionManagerImpl implements JpaTransactionManager {
|
||||
|
||||
@Override
|
||||
public <T> T transact(Supplier<T> work) {
|
||||
// TODO(shicong): Investigate removing transactNew functionality after migration as it may
|
||||
// be same as this one.
|
||||
return retrier.callWithRetry(
|
||||
() -> {
|
||||
if (inTransaction()) {
|
||||
@@ -196,6 +197,8 @@ public class JpaTransactionManagerImpl implements JpaTransactionManager {
|
||||
});
|
||||
}
|
||||
|
||||
// TODO(b/177674699): Remove all transactNew methods as they are same as transact after the
|
||||
// database migration.
|
||||
@Override
|
||||
public <T> T transactNew(Supplier<T> work) {
|
||||
return transact(work);
|
||||
@@ -249,8 +252,10 @@ public class JpaTransactionManagerImpl implements JpaTransactionManager {
|
||||
return;
|
||||
}
|
||||
assertInTransaction();
|
||||
getEntityManager().persist(entity);
|
||||
transactionInfo.get().addUpdate(entity);
|
||||
// Necessary due to the changes in HistoryEntry representation during the migration to SQL
|
||||
Object toPersist = toChildHistoryEntryIfPossible(entity);
|
||||
getEntityManager().persist(toPersist);
|
||||
transactionInfo.get().addUpdate(toPersist);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -277,8 +282,10 @@ public class JpaTransactionManagerImpl implements JpaTransactionManager {
|
||||
return;
|
||||
}
|
||||
assertInTransaction();
|
||||
getEntityManager().merge(entity);
|
||||
transactionInfo.get().addUpdate(entity);
|
||||
// Necessary due to the changes in HistoryEntry representation during the migration to SQL
|
||||
Object toPersist = toChildHistoryEntryIfPossible(entity);
|
||||
getEntityManager().merge(toPersist);
|
||||
transactionInfo.get().addUpdate(toPersist);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -306,8 +313,10 @@ public class JpaTransactionManagerImpl implements JpaTransactionManager {
|
||||
}
|
||||
assertInTransaction();
|
||||
checkArgument(exists(entity), "Given entity does not exist");
|
||||
getEntityManager().merge(entity);
|
||||
transactionInfo.get().addUpdate(entity);
|
||||
// Necessary due to the changes in HistoryEntry representation during the migration to SQL
|
||||
Object toPersist = toChildHistoryEntryIfPossible(entity);
|
||||
getEntityManager().merge(toPersist);
|
||||
transactionInfo.get().addUpdate(toPersist);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -338,6 +347,7 @@ public class JpaTransactionManagerImpl implements JpaTransactionManager {
|
||||
@Override
|
||||
public boolean exists(Object entity) {
|
||||
checkArgumentNotNull(entity, "entity must be specified");
|
||||
entity = toChildHistoryEntryIfPossible(entity);
|
||||
EntityType<?> entityType = getEntityType(entity.getClass());
|
||||
ImmutableSet<EntityId> entityIds = getEntityIdsFromEntity(entityType, entity);
|
||||
return exists(entityType.getName(), entityIds);
|
||||
@@ -381,6 +391,7 @@ public class JpaTransactionManagerImpl implements JpaTransactionManager {
|
||||
@Override
|
||||
public <T> ImmutableList<T> loadByEntitiesIfPresent(Iterable<T> entities) {
|
||||
return Streams.stream(entities)
|
||||
.map(DatastoreTransactionManager::toChildHistoryEntryIfPossible)
|
||||
.filter(this::exists)
|
||||
.map(this::loadByEntity)
|
||||
.collect(toImmutableList());
|
||||
@@ -415,9 +426,14 @@ public class JpaTransactionManagerImpl implements JpaTransactionManager {
|
||||
public <T> T loadByEntity(T entity) {
|
||||
checkArgumentNotNull(entity, "entity must be specified");
|
||||
assertInTransaction();
|
||||
entity = toChildHistoryEntryIfPossible(entity);
|
||||
// If the caller requested a HistoryEntry, load the corresponding *History class
|
||||
T possibleChild = toChildHistoryEntryIfPossible(entity);
|
||||
return (T)
|
||||
loadByKey(
|
||||
VKey.createSql(entity.getClass(), emf.getPersistenceUnitUtil().getIdentifier(entity)));
|
||||
VKey.createSql(
|
||||
possibleChild.getClass(),
|
||||
emf.getPersistenceUnitUtil().getIdentifier(possibleChild)));
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -468,6 +484,7 @@ public class JpaTransactionManagerImpl implements JpaTransactionManager {
|
||||
return;
|
||||
}
|
||||
assertInTransaction();
|
||||
entity = toChildHistoryEntryIfPossible(entity);
|
||||
Object managedEntity = entity;
|
||||
if (!getEntityManager().contains(entity)) {
|
||||
managedEntity = getEntityManager().merge(entity);
|
||||
|
||||
@@ -52,6 +52,7 @@ import google.registry.model.registrar.Registrar;
|
||||
import google.registry.model.registrar.RegistrarAddress;
|
||||
import google.registry.model.registrar.RegistrarContact;
|
||||
import google.registry.model.reporting.HistoryEntry;
|
||||
import google.registry.model.reporting.HistoryEntryDao;
|
||||
import google.registry.persistence.VKey;
|
||||
import google.registry.rdap.RdapDataStructures.Event;
|
||||
import google.registry.rdap.RdapDataStructures.EventAction;
|
||||
@@ -880,8 +881,9 @@ public class RdapJsonFormatter {
|
||||
// 2.3.2.3 An event of *eventAction* type *transfer*, with the last date and time that the
|
||||
// domain was transferred. The event of *eventAction* type *transfer* MUST be omitted if the
|
||||
// domain name has not been transferred since it was created.
|
||||
for (HistoryEntry historyEntry :
|
||||
ofy().load().type(HistoryEntry.class).ancestor(resource).order("modificationTime")) {
|
||||
Iterable<? extends HistoryEntry> historyEntries =
|
||||
HistoryEntryDao.loadHistoryObjectsForResource(resource.createVKey());
|
||||
for (HistoryEntry historyEntry : historyEntries) {
|
||||
EventAction rdapEventAction =
|
||||
HISTORY_ENTRY_TYPE_TO_RDAP_EVENT_ACTION_MAP.get(historyEntry.getType());
|
||||
// Only save the historyEntries if this is a type we care about.
|
||||
@@ -930,13 +932,9 @@ public class RdapJsonFormatter {
|
||||
return eventsBuilder.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an RDAP event object as defined by RFC 7483.
|
||||
*/
|
||||
/** Creates an RDAP event object as defined by RFC 7483. */
|
||||
private static Event makeEvent(
|
||||
EventAction eventAction,
|
||||
@Nullable String eventActor,
|
||||
DateTime eventDate) {
|
||||
EventAction eventAction, @Nullable String eventActor, DateTime eventDate) {
|
||||
Event.Builder builder = Event.builder()
|
||||
.setEventAction(eventAction)
|
||||
.setEventDate(eventDate);
|
||||
|
||||
@@ -76,7 +76,7 @@ final class DeleteAllocationTokensCommand extends UpdateOrDeleteAllocationTokens
|
||||
// since the query ran. This also filters out per-domain tokens if they're not to be deleted.
|
||||
ImmutableSet<VKey<AllocationToken>> tokensToDelete =
|
||||
tm().loadByKeys(batch).values().stream()
|
||||
.filter(t -> withDomains || t.getDomainName().isEmpty())
|
||||
.filter(t -> withDomains || !t.getDomainName().isPresent())
|
||||
.filter(t -> SINGLE_USE.equals(t.getTokenType()))
|
||||
.filter(t -> !t.isRedeemed())
|
||||
.map(AllocationToken::createVKey)
|
||||
|
||||
@@ -59,7 +59,7 @@ final class GetAllocationTokenCommand implements CommandWithRemoteApi {
|
||||
if (loadedTokens.containsKey(token)) {
|
||||
AllocationToken loadedToken = loadedTokens.get(token);
|
||||
System.out.println(loadedToken.toString());
|
||||
if (loadedToken.getRedemptionHistoryEntry().isEmpty()) {
|
||||
if (!loadedToken.getRedemptionHistoryEntry().isPresent()) {
|
||||
System.out.printf("Token %s was not redeemed.\n", token);
|
||||
} else {
|
||||
Key<DomainBase> domainOfyKey =
|
||||
|
||||
@@ -15,7 +15,6 @@
|
||||
package google.registry.tools;
|
||||
|
||||
import static com.google.common.base.Preconditions.checkArgument;
|
||||
import static google.registry.model.ofy.ObjectifyService.ofy;
|
||||
import static google.registry.util.DateTimeUtils.END_OF_TIME;
|
||||
import static google.registry.util.DateTimeUtils.START_OF_TIME;
|
||||
import static google.registry.util.PreconditionsUtils.checkArgumentNotNull;
|
||||
@@ -25,14 +24,16 @@ import com.beust.jcommander.Parameter;
|
||||
import com.beust.jcommander.Parameters;
|
||||
import google.registry.model.EppResource;
|
||||
import google.registry.model.reporting.HistoryEntry;
|
||||
import google.registry.model.reporting.HistoryEntryDao;
|
||||
import google.registry.persistence.VKey;
|
||||
import google.registry.tools.CommandUtilities.ResourceType;
|
||||
import google.registry.xml.XmlTransformer;
|
||||
import org.joda.time.DateTime;
|
||||
|
||||
/** Command to show history entries. */
|
||||
@Parameters(separators = " =",
|
||||
commandDescription = "Show history entries that occurred in a given time range")
|
||||
@Parameters(
|
||||
separators = " =",
|
||||
commandDescription = "Show history entries that occurred in a given time range")
|
||||
final class GetHistoryEntriesCommand implements CommandWithRemoteApi {
|
||||
|
||||
@Parameter(
|
||||
@@ -45,33 +46,26 @@ final class GetHistoryEntriesCommand implements CommandWithRemoteApi {
|
||||
description = "Only show history entries that occurred at or before this time")
|
||||
private DateTime before = END_OF_TIME;
|
||||
|
||||
@Parameter(
|
||||
names = "--type",
|
||||
description = "Resource type.")
|
||||
@Parameter(names = "--type", description = "Resource type.")
|
||||
private ResourceType type;
|
||||
|
||||
@Parameter(
|
||||
names = "--id",
|
||||
description = "Foreign key of the resource.")
|
||||
@Parameter(names = "--id", description = "Foreign key of the resource.")
|
||||
private String uniqueId;
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
VKey<? extends EppResource> parentKey = null;
|
||||
Iterable<? extends HistoryEntry> historyEntries;
|
||||
if (type != null || uniqueId != null) {
|
||||
checkArgument(
|
||||
type != null && uniqueId != null,
|
||||
"If either of 'type' or 'id' are set then both must be");
|
||||
parentKey = type.getKey(uniqueId, DateTime.now(UTC));
|
||||
VKey<? extends EppResource> parentKey = type.getKey(uniqueId, DateTime.now(UTC));
|
||||
checkArgumentNotNull(parentKey, "Invalid resource ID");
|
||||
historyEntries = HistoryEntryDao.loadHistoryObjectsForResource(parentKey, after, before);
|
||||
} else {
|
||||
historyEntries = HistoryEntryDao.loadAllHistoryObjects(after, before);
|
||||
}
|
||||
for (HistoryEntry entry :
|
||||
(parentKey == null
|
||||
? ofy().load().type(HistoryEntry.class)
|
||||
: ofy().load().type(HistoryEntry.class).ancestor(parentKey.getOfyKey()))
|
||||
.order("modificationTime")
|
||||
.filter("modificationTime >=", after)
|
||||
.filter("modificationTime <=", before)) {
|
||||
for (HistoryEntry entry : historyEntries) {
|
||||
System.out.printf(
|
||||
"Client: %s\nTime: %s\nClient TRID: %s\nServer TRID: %s\n%s\n",
|
||||
entry.getClientId(),
|
||||
|
||||
@@ -168,6 +168,8 @@ interface RegistryToolComponent {
|
||||
|
||||
void inject(ValidateEscrowDepositCommand command);
|
||||
|
||||
void inject(ValidateLoginCredentialsCommand command);
|
||||
|
||||
void inject(WhoisQueryCommand command);
|
||||
|
||||
AppEngineConnection appEngineConnection();
|
||||
|
||||
@@ -18,6 +18,7 @@ import static com.google.common.base.Preconditions.checkArgument;
|
||||
import static com.google.common.collect.ImmutableSet.toImmutableSet;
|
||||
import static google.registry.model.EppResourceUtils.loadByForeignKey;
|
||||
import static google.registry.model.domain.rgp.GracePeriodStatus.AUTO_RENEW;
|
||||
import static google.registry.model.eppcommon.StatusValue.PENDING_DELETE;
|
||||
import static google.registry.model.eppcommon.StatusValue.SERVER_UPDATE_PROHIBITED;
|
||||
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
|
||||
import static google.registry.util.PreconditionsUtils.checkArgumentPresent;
|
||||
@@ -139,6 +140,11 @@ final class UpdateDomainCommand extends CreateOrUpdateDomainCommand {
|
||||
+ " deleted at the end of its current registration period.")
|
||||
Boolean autorenews;
|
||||
|
||||
@Parameter(
|
||||
names = {"--force_in_pending_delete"},
|
||||
description = "Force a superuser update even on domains that are in pending delete")
|
||||
boolean forceInPendingDelete;
|
||||
|
||||
@Override
|
||||
protected void initMutatingEppToolCommand() {
|
||||
if (!nameservers.isEmpty()) {
|
||||
@@ -186,6 +192,12 @@ final class UpdateDomainCommand extends CreateOrUpdateDomainCommand {
|
||||
"The domain '%s' has status SERVER_UPDATE_PROHIBITED. Verify that you are allowed "
|
||||
+ "to make updates, and if so, use the domain_unlock command to enable updates.",
|
||||
domain);
|
||||
checkArgument(
|
||||
!domainBase.getStatusValues().contains(PENDING_DELETE) || forceInPendingDelete,
|
||||
"The domain '%s' has status PENDING_DELETE. Verify that you really are intending to "
|
||||
+ "update a domain in pending delete (this is uncommon), and if so, pass the "
|
||||
+ "--force_in_pending_delete parameter to allow this update.",
|
||||
domain);
|
||||
|
||||
// Use TreeSets so that the results are always in the same order (this makes testing easier).
|
||||
Set<String> addAdminsThisDomain = new TreeSet<>(addAdmins);
|
||||
|
||||
@@ -18,6 +18,7 @@ import static com.google.common.base.Preconditions.checkArgument;
|
||||
import static com.google.common.base.Preconditions.checkState;
|
||||
import static com.google.common.base.Strings.isNullOrEmpty;
|
||||
import static google.registry.util.PreconditionsUtils.checkArgumentPresent;
|
||||
import static google.registry.util.X509Utils.encodeX509CertificateFromPemString;
|
||||
import static google.registry.util.X509Utils.getCertificateHash;
|
||||
import static google.registry.util.X509Utils.loadCertificate;
|
||||
import static java.nio.charset.StandardCharsets.US_ASCII;
|
||||
@@ -25,12 +26,14 @@ import static java.nio.charset.StandardCharsets.US_ASCII;
|
||||
import com.beust.jcommander.Parameter;
|
||||
import com.beust.jcommander.Parameters;
|
||||
import google.registry.flows.TlsCredentials;
|
||||
import google.registry.flows.certs.CertificateChecker;
|
||||
import google.registry.model.registrar.Registrar;
|
||||
import google.registry.tools.params.PathParameter;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Optional;
|
||||
import javax.annotation.Nullable;
|
||||
import javax.inject.Inject;
|
||||
|
||||
/** A command to test registrar login credentials. */
|
||||
@Parameters(separators = " =", commandDescription = "Test registrar login credentials")
|
||||
@@ -55,6 +58,7 @@ final class ValidateLoginCredentialsCommand implements CommandWithRemoteApi {
|
||||
validateWith = PathParameter.InputFile.class)
|
||||
private Path clientCertificatePath;
|
||||
|
||||
// TODO(sarahbot@): Remove this after hash fallback is removed
|
||||
@Nullable
|
||||
@Parameter(
|
||||
names = {"-h", "--cert_hash"},
|
||||
@@ -67,20 +71,28 @@ final class ValidateLoginCredentialsCommand implements CommandWithRemoteApi {
|
||||
description = "Client ip address to pretend to use")
|
||||
private String clientIpAddress = "10.0.0.1";
|
||||
|
||||
@Inject CertificateChecker certificateChecker;
|
||||
|
||||
@Override
|
||||
public void run() throws Exception {
|
||||
checkArgument(
|
||||
clientCertificatePath == null || isNullOrEmpty(clientCertificateHash),
|
||||
"Can't specify both --cert_hash and --cert_file");
|
||||
String encodedCertificate = "";
|
||||
if (clientCertificatePath != null) {
|
||||
clientCertificateHash = getCertificateHash(
|
||||
loadCertificate(new String(Files.readAllBytes(clientCertificatePath), US_ASCII)));
|
||||
String certificateString = new String(Files.readAllBytes(clientCertificatePath), US_ASCII);
|
||||
encodedCertificate = encodeX509CertificateFromPemString(certificateString);
|
||||
clientCertificateHash = getCertificateHash(loadCertificate(clientCertificatePath));
|
||||
}
|
||||
Registrar registrar =
|
||||
checkArgumentPresent(
|
||||
Registrar.loadByClientId(clientId), "Registrar %s not found", clientId);
|
||||
new TlsCredentials(
|
||||
true, Optional.ofNullable(clientCertificateHash), Optional.ofNullable(clientIpAddress))
|
||||
true,
|
||||
Optional.ofNullable(clientCertificateHash),
|
||||
Optional.ofNullable(encodedCertificate),
|
||||
Optional.ofNullable(clientIpAddress),
|
||||
certificateChecker)
|
||||
.validate(registrar, password);
|
||||
checkState(
|
||||
registrar.isLive(), "Registrar %s has non-live state: %s", clientId, registrar.getState());
|
||||
|
||||
+16
-16
@@ -16,20 +16,24 @@ package google.registry.tools.javascrap;
|
||||
|
||||
import static com.google.common.base.Preconditions.checkArgument;
|
||||
import static com.google.common.collect.ImmutableList.toImmutableList;
|
||||
import static google.registry.model.ofy.ObjectifyService.ofy;
|
||||
import static google.registry.persistence.transaction.TransactionManagerFactory.jpaTm;
|
||||
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
|
||||
import static google.registry.persistence.transaction.TransactionManagerUtil.transactIfJpaTm;
|
||||
import static google.registry.tools.LockOrUnlockDomainCommand.REGISTRY_LOCK_STATUSES;
|
||||
|
||||
import com.beust.jcommander.Parameter;
|
||||
import com.beust.jcommander.Parameters;
|
||||
import com.google.common.collect.ImmutableCollection;
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
import com.google.common.collect.Streams;
|
||||
import com.google.common.flogger.FluentLogger;
|
||||
import com.googlecode.objectify.Key;
|
||||
import google.registry.config.RegistryConfig.Config;
|
||||
import google.registry.model.domain.DomainBase;
|
||||
import google.registry.model.registry.RegistryLockDao;
|
||||
import google.registry.model.reporting.HistoryEntry;
|
||||
import google.registry.model.reporting.HistoryEntryDao;
|
||||
import google.registry.persistence.VKey;
|
||||
import google.registry.schema.domain.RegistryLock;
|
||||
import google.registry.tools.CommandWithRemoteApi;
|
||||
import google.registry.tools.ConfirmingCommand;
|
||||
@@ -130,7 +134,7 @@ public class BackfillRegistryLocksCommand extends ConfirmingCommand
|
||||
private DateTime getLockCompletionTimestamp(DomainBase domainBase, DateTime now) {
|
||||
// Best-effort, if a domain was URS-locked we should use that time
|
||||
// If we can't find that, return now.
|
||||
return ofy().load().type(HistoryEntry.class).ancestor(domainBase).list().stream()
|
||||
return Streams.stream(HistoryEntryDao.loadHistoryObjectsForResource(domainBase.createVKey()))
|
||||
// sort by modification time descending so we get the most recent one if it was locked twice
|
||||
.sorted(Comparator.comparing(HistoryEntry::getModificationTime).reversed())
|
||||
.filter(entry -> entry.getReason().equals("Uniform Rapid Suspension"))
|
||||
@@ -140,18 +144,14 @@ public class BackfillRegistryLocksCommand extends ConfirmingCommand
|
||||
}
|
||||
|
||||
private ImmutableList<DomainBase> getLockedDomainsWithoutLocks(DateTime now) {
|
||||
return ImmutableList.copyOf(
|
||||
ofy()
|
||||
.load()
|
||||
.keys(
|
||||
roids.stream()
|
||||
.map(roid -> Key.create(DomainBase.class, roid))
|
||||
.collect(toImmutableList()))
|
||||
.values()
|
||||
.stream()
|
||||
.filter(d -> d.getDeletionTime().isAfter(now))
|
||||
.filter(d -> d.getStatusValues().containsAll(REGISTRY_LOCK_STATUSES))
|
||||
.filter(d -> !RegistryLockDao.getMostRecentByRepoId(d.getRepoId()).isPresent())
|
||||
.collect(toImmutableList()));
|
||||
ImmutableList<VKey<DomainBase>> domainKeys =
|
||||
roids.stream().map(roid -> VKey.create(DomainBase.class, roid)).collect(toImmutableList());
|
||||
ImmutableCollection<DomainBase> domains =
|
||||
transactIfJpaTm(() -> tm().loadByKeys(domainKeys)).values();
|
||||
return domains.stream()
|
||||
.filter(d -> d.getDeletionTime().isAfter(now))
|
||||
.filter(d -> d.getStatusValues().containsAll(REGISTRY_LOCK_STATUSES))
|
||||
.filter(d -> RegistryLockDao.getMostRecentByRepoId(d.getRepoId()).isEmpty())
|
||||
.collect(toImmutableList());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,6 +39,7 @@ import com.google.common.collect.Streams;
|
||||
import com.google.common.flogger.FluentLogger;
|
||||
import google.registry.config.RegistryEnvironment;
|
||||
import google.registry.flows.certs.CertificateChecker;
|
||||
import google.registry.flows.certs.CertificateChecker.InsecureCertificateException;
|
||||
import google.registry.model.registrar.Registrar;
|
||||
import google.registry.model.registrar.RegistrarContact;
|
||||
import google.registry.model.registrar.RegistrarContact.Type;
|
||||
@@ -337,7 +338,11 @@ public class RegistrarSettingsAction implements Runnable, JsonActionRunner.JsonA
|
||||
private boolean validateCertificate(
|
||||
Optional<String> existingCertificate, String certificateString) {
|
||||
if (!existingCertificate.isPresent() || !existingCertificate.get().equals(certificateString)) {
|
||||
try {
|
||||
certificateChecker.validateCertificate(certificateString);
|
||||
} catch (InsecureCertificateException e) {
|
||||
throw new IllegalArgumentException(e.getMessage());
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
|
||||
@@ -0,0 +1,183 @@
|
||||
// Copyright 2021 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.eppcommon.StatusValue.PENDING_DELETE;
|
||||
import static google.registry.model.reporting.HistoryEntry.Type.DOMAIN_CREATE;
|
||||
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
|
||||
import static google.registry.testing.DatabaseHelper.createTld;
|
||||
import static google.registry.testing.DatabaseHelper.newDomainBase;
|
||||
import static google.registry.testing.DatabaseHelper.persistActiveDomain;
|
||||
import static google.registry.testing.DatabaseHelper.persistResource;
|
||||
import static google.registry.util.DateTimeUtils.END_OF_TIME;
|
||||
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
import google.registry.flows.DaggerEppTestComponent;
|
||||
import google.registry.flows.EppController;
|
||||
import google.registry.flows.EppTestComponent.FakesAndMocksModule;
|
||||
import google.registry.model.billing.BillingEvent;
|
||||
import google.registry.model.billing.BillingEvent.Flag;
|
||||
import google.registry.model.billing.BillingEvent.Reason;
|
||||
import google.registry.model.domain.DomainBase;
|
||||
import google.registry.model.ofy.Ofy;
|
||||
import google.registry.model.poll.PollMessage;
|
||||
import google.registry.model.reporting.HistoryEntry;
|
||||
import google.registry.monitoring.whitebox.EppMetric;
|
||||
import google.registry.testing.AppEngineExtension;
|
||||
import google.registry.testing.FakeClock;
|
||||
import google.registry.testing.FakeLockHandler;
|
||||
import google.registry.testing.FakeResponse;
|
||||
import google.registry.testing.InjectExtension;
|
||||
import java.util.Optional;
|
||||
import org.joda.time.DateTime;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.RegisterExtension;
|
||||
|
||||
/** Unit tests for {@link DeleteExpiredDomainsAction}. */
|
||||
class DeleteExpiredDomainsActionTest {
|
||||
|
||||
@RegisterExtension
|
||||
public final AppEngineExtension appEngine =
|
||||
AppEngineExtension.builder().withDatastoreAndCloudSql().withTaskQueue().build();
|
||||
|
||||
@RegisterExtension public final InjectExtension inject = new InjectExtension();
|
||||
|
||||
private final FakeClock clock = new FakeClock(DateTime.parse("2016-06-13T20:21:22Z"));
|
||||
private final FakeResponse response = new FakeResponse();
|
||||
private DeleteExpiredDomainsAction action;
|
||||
|
||||
@BeforeEach
|
||||
void beforeEach() {
|
||||
inject.setStaticField(Ofy.class, "clock", clock);
|
||||
createTld("tld");
|
||||
EppController eppController =
|
||||
DaggerEppTestComponent.builder()
|
||||
.fakesAndMocksModule(
|
||||
FakesAndMocksModule.create(clock, EppMetric.builderForRequest(clock)))
|
||||
.build()
|
||||
.startRequest()
|
||||
.eppController();
|
||||
action =
|
||||
new DeleteExpiredDomainsAction(
|
||||
eppController, "NewRegistrar", clock, new FakeLockHandler(true), response);
|
||||
}
|
||||
|
||||
@Test
|
||||
void test_deletesOnlyExpiredDomain() {
|
||||
// A normal, active autorenewing domain that shouldn't be touched.
|
||||
DomainBase activeDomain = persistActiveDomain("foo.tld");
|
||||
clock.advanceOneMilli();
|
||||
|
||||
// A non-autorenewing domain that is already pending delete and shouldn't be touched.
|
||||
DomainBase alreadyDeletedDomain =
|
||||
persistResource(
|
||||
newDomainBase("bar.tld")
|
||||
.asBuilder()
|
||||
.setAutorenewEndTime(Optional.of(clock.nowUtc().minusDays(10)))
|
||||
.setDeletionTime(clock.nowUtc().plusDays(17))
|
||||
.build());
|
||||
clock.advanceOneMilli();
|
||||
|
||||
// A non-autorenewing domain that hasn't reached its expiration time and shouldn't be touched.
|
||||
DomainBase notYetExpiredDomain =
|
||||
persistResource(
|
||||
newDomainBase("baz.tld")
|
||||
.asBuilder()
|
||||
.setAutorenewEndTime(Optional.of(clock.nowUtc().plusDays(15)))
|
||||
.build());
|
||||
clock.advanceOneMilli();
|
||||
|
||||
// A non-autorenewing domain that is past its expiration time and should be deleted.
|
||||
// (This is the only one that needs a full set of subsidiary resources, for the delete flow to
|
||||
// to operate on.)
|
||||
DomainBase pendingExpirationDomain = persistNonAutorenewingDomain("fizz.tld");
|
||||
|
||||
assertThat(tm().loadByEntity(pendingExpirationDomain).getStatusValues())
|
||||
.doesNotContain(PENDING_DELETE);
|
||||
action.run();
|
||||
|
||||
DomainBase reloadedActiveDomain = tm().loadByEntity(activeDomain);
|
||||
assertThat(reloadedActiveDomain).isEqualTo(activeDomain);
|
||||
assertThat(reloadedActiveDomain.getStatusValues()).doesNotContain(PENDING_DELETE);
|
||||
assertThat(tm().loadByEntity(alreadyDeletedDomain)).isEqualTo(alreadyDeletedDomain);
|
||||
assertThat(tm().loadByEntity(notYetExpiredDomain)).isEqualTo(notYetExpiredDomain);
|
||||
DomainBase reloadedExpiredDomain = tm().loadByEntity(pendingExpirationDomain);
|
||||
assertThat(reloadedExpiredDomain.getStatusValues()).contains(PENDING_DELETE);
|
||||
assertThat(reloadedExpiredDomain.getDeletionTime()).isEqualTo(clock.nowUtc().plusDays(35));
|
||||
}
|
||||
|
||||
@Test
|
||||
void test_deletesThreeDomainsInOneRun() {
|
||||
DomainBase domain1 = persistNonAutorenewingDomain("ecck1.tld");
|
||||
DomainBase domain2 = persistNonAutorenewingDomain("veee2.tld");
|
||||
DomainBase domain3 = persistNonAutorenewingDomain("tarm3.tld");
|
||||
|
||||
action.run();
|
||||
|
||||
assertThat(tm().loadByEntity(domain1).getStatusValues()).contains(PENDING_DELETE);
|
||||
assertThat(tm().loadByEntity(domain2).getStatusValues()).contains(PENDING_DELETE);
|
||||
assertThat(tm().loadByEntity(domain3).getStatusValues()).contains(PENDING_DELETE);
|
||||
}
|
||||
|
||||
private DomainBase persistNonAutorenewingDomain(String domainName) {
|
||||
DomainBase pendingExpirationDomain = persistActiveDomain(domainName);
|
||||
HistoryEntry createHistoryEntry =
|
||||
persistResource(
|
||||
new HistoryEntry.Builder()
|
||||
.setType(DOMAIN_CREATE)
|
||||
.setParent(pendingExpirationDomain)
|
||||
.setModificationTime(clock.nowUtc().minusMonths(9))
|
||||
.build());
|
||||
BillingEvent.Recurring autorenewBillingEvent =
|
||||
persistResource(createAutorenewBillingEvent(createHistoryEntry).build());
|
||||
PollMessage.Autorenew autorenewPollMessage =
|
||||
persistResource(createAutorenewPollMessage(createHistoryEntry).build());
|
||||
pendingExpirationDomain =
|
||||
persistResource(
|
||||
pendingExpirationDomain
|
||||
.asBuilder()
|
||||
.setAutorenewEndTime(Optional.of(clock.nowUtc().minusDays(10)))
|
||||
.setAutorenewBillingEvent(autorenewBillingEvent.createVKey())
|
||||
.setAutorenewPollMessage(autorenewPollMessage.createVKey())
|
||||
.build());
|
||||
clock.advanceOneMilli();
|
||||
|
||||
return pendingExpirationDomain;
|
||||
}
|
||||
|
||||
private BillingEvent.Recurring.Builder createAutorenewBillingEvent(
|
||||
HistoryEntry createHistoryEntry) {
|
||||
return new BillingEvent.Recurring.Builder()
|
||||
.setReason(Reason.RENEW)
|
||||
.setFlags(ImmutableSet.of(Flag.AUTO_RENEW))
|
||||
.setTargetId("fizz.tld")
|
||||
.setClientId("TheRegistrar")
|
||||
.setEventTime(clock.nowUtc().plusYears(1))
|
||||
.setRecurrenceEndTime(END_OF_TIME)
|
||||
.setParent(createHistoryEntry);
|
||||
}
|
||||
|
||||
private PollMessage.Autorenew.Builder createAutorenewPollMessage(
|
||||
HistoryEntry createHistoryEntry) {
|
||||
return new PollMessage.Autorenew.Builder()
|
||||
.setTargetId("fizz.tld")
|
||||
.setClientId("TheRegistrar")
|
||||
.setEventTime(clock.nowUtc().plusYears(1))
|
||||
.setAutorenewEndTime(END_OF_TIME)
|
||||
.setParent(createHistoryEntry);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,152 @@
|
||||
// Copyright 2020 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.datastore;
|
||||
|
||||
import static google.registry.beam.datastore.BulkDeletePipeline.discoverEntityKinds;
|
||||
import static google.registry.beam.datastore.BulkDeletePipeline.getDeletionTags;
|
||||
import static google.registry.beam.datastore.BulkDeletePipeline.getOneDeletionTag;
|
||||
|
||||
import com.google.common.base.Verify;
|
||||
import com.google.common.collect.ImmutableMap;
|
||||
import com.google.common.collect.Iterables;
|
||||
import com.google.datastore.v1.Entity;
|
||||
import com.google.datastore.v1.Key;
|
||||
import com.google.datastore.v1.Key.PathElement;
|
||||
import google.registry.beam.TestPipelineExtension;
|
||||
import google.registry.beam.datastore.BulkDeletePipeline.GenerateQueries;
|
||||
import google.registry.beam.datastore.BulkDeletePipeline.SplitEntities;
|
||||
import java.io.Serializable;
|
||||
import java.util.Map;
|
||||
import org.apache.beam.sdk.testing.PAssert;
|
||||
import org.apache.beam.sdk.transforms.Count;
|
||||
import org.apache.beam.sdk.transforms.Create;
|
||||
import org.apache.beam.sdk.transforms.ParDo;
|
||||
import org.apache.beam.sdk.transforms.View;
|
||||
import org.apache.beam.sdk.values.KV;
|
||||
import org.apache.beam.sdk.values.PCollection;
|
||||
import org.apache.beam.sdk.values.PCollectionTuple;
|
||||
import org.apache.beam.sdk.values.PCollectionView;
|
||||
import org.apache.beam.sdk.values.TupleTag;
|
||||
import org.apache.beam.sdk.values.TupleTagList;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.condition.EnabledIfSystemProperty;
|
||||
import org.junit.jupiter.api.extension.RegisterExtension;
|
||||
|
||||
/** Unit tests for {@link BulkDeletePipeline}. */
|
||||
class BulkDeletePipelineTest implements Serializable {
|
||||
|
||||
@RegisterExtension
|
||||
final transient TestPipelineExtension testPipeline =
|
||||
TestPipelineExtension.create().enableAbandonedNodeEnforcement(true);
|
||||
|
||||
@Test
|
||||
void generateQueries() {
|
||||
PCollection<String> queries =
|
||||
testPipeline
|
||||
.apply("InjectKinds", Create.of("A", "B"))
|
||||
.apply("GenerateQueries", ParDo.of(new GenerateQueries()));
|
||||
|
||||
PAssert.that(queries).containsInAnyOrder("select __key__ from `A`", "select __key__ from `B`");
|
||||
testPipeline.run();
|
||||
}
|
||||
|
||||
@Test
|
||||
void mapKindsToTags() {
|
||||
TupleTagList tags = getDeletionTags(2);
|
||||
PCollection<String> kinds = testPipeline.apply("InjectKinds", Create.of("A", "B"));
|
||||
PCollection<KV<String, TupleTag<Entity>>> kindToTagMapping =
|
||||
BulkDeletePipeline.mapKindsToDeletionTags(kinds, tags);
|
||||
PAssert.thatMap(kindToTagMapping)
|
||||
.isEqualTo(
|
||||
ImmutableMap.of(
|
||||
"A", new TupleTag<Entity>("0"),
|
||||
"B", new TupleTag<Entity>("1")));
|
||||
testPipeline.run();
|
||||
}
|
||||
|
||||
@Test
|
||||
void mapKindsToTags_fewerKindsThanTags() {
|
||||
TupleTagList tags = getDeletionTags(3);
|
||||
PCollection<String> kinds = testPipeline.apply("InjectKinds", Create.of("A", "B"));
|
||||
PCollection<KV<String, TupleTag<Entity>>> kindToTagMapping =
|
||||
BulkDeletePipeline.mapKindsToDeletionTags(kinds, tags);
|
||||
PAssert.thatMap(kindToTagMapping)
|
||||
.isEqualTo(
|
||||
ImmutableMap.of(
|
||||
"A", new TupleTag<Entity>("0"),
|
||||
"B", new TupleTag<Entity>("1")));
|
||||
testPipeline.run();
|
||||
}
|
||||
|
||||
@Test
|
||||
void mapKindsToTags_moreKindsThanTags() {
|
||||
TupleTagList tags = getDeletionTags(2);
|
||||
PCollection<String> kinds = testPipeline.apply("InjectKinds", Create.of("A", "B", "C"));
|
||||
PCollection<KV<String, TupleTag<Entity>>> kindToTagMapping =
|
||||
BulkDeletePipeline.mapKindsToDeletionTags(kinds, tags);
|
||||
PAssert.thatMap(kindToTagMapping)
|
||||
.isEqualTo(
|
||||
ImmutableMap.of(
|
||||
"A", new TupleTag<Entity>("0"),
|
||||
"B", new TupleTag<Entity>("1"),
|
||||
"C", new TupleTag<Entity>("0")));
|
||||
testPipeline.run();
|
||||
}
|
||||
|
||||
@Test
|
||||
void splitEntitiesByKind() {
|
||||
TupleTagList tags = getDeletionTags(2);
|
||||
PCollection<String> kinds = testPipeline.apply("InjectKinds", Create.of("A", "B"));
|
||||
PCollectionView<Map<String, TupleTag<Entity>>> kindToTagMapping =
|
||||
BulkDeletePipeline.mapKindsToDeletionTags(kinds, tags).apply(View.asMap());
|
||||
Entity entityA = createTestEntity("A", 1);
|
||||
Entity entityB = createTestEntity("B", 2);
|
||||
PCollection<Entity> entities =
|
||||
testPipeline.apply("InjectEntities", Create.of(entityA, entityB));
|
||||
PCollectionTuple allCollections =
|
||||
entities.apply(
|
||||
"SplitByKind",
|
||||
ParDo.of(new SplitEntities(kindToTagMapping))
|
||||
.withSideInputs(kindToTagMapping)
|
||||
.withOutputTags(getOneDeletionTag("placeholder"), tags));
|
||||
PAssert.that(allCollections.get((TupleTag<Entity>) tags.get(0))).containsInAnyOrder(entityA);
|
||||
PAssert.that(allCollections.get((TupleTag<Entity>) tags.get(1))).containsInAnyOrder(entityB);
|
||||
testPipeline.run();
|
||||
}
|
||||
|
||||
private static Entity createTestEntity(String kind, long id) {
|
||||
return Entity.newBuilder()
|
||||
.setKey(Key.newBuilder().addPath(PathElement.newBuilder().setId(id).setKind(kind)))
|
||||
.build();
|
||||
}
|
||||
|
||||
@Test
|
||||
@EnabledIfSystemProperty(named = "test.gcp_integration.env", matches = "\\S+")
|
||||
void discoverKindsFromDatastore() {
|
||||
String environmentName = System.getProperty("test.gcp_integration.env");
|
||||
String project = "domain-registry-" + environmentName;
|
||||
|
||||
PCollection<String> kinds =
|
||||
testPipeline.apply("DiscoverEntityKinds", discoverEntityKinds(project));
|
||||
|
||||
PAssert.that(kinds.apply(Count.globally()))
|
||||
.satisfies(
|
||||
longs -> {
|
||||
Verify.verify(Iterables.size(longs) == 1 && Iterables.getFirst(longs, -1L) > 0);
|
||||
return null;
|
||||
});
|
||||
testPipeline.run();
|
||||
}
|
||||
}
|
||||
@@ -92,16 +92,10 @@ public final class InitSqlTestUtils {
|
||||
* VersionedEntities} contains exactly the same elements in the {@code expected} array.
|
||||
*
|
||||
* <p>This method makes assertions in the pipeline and only use {@link PAssert} on the result.
|
||||
* This has two advantages over {@code PAssert}:
|
||||
*
|
||||
* <ul>
|
||||
* <li>It supports assertions on 'containsExactlyElementsIn', which is not available in {@code
|
||||
* PAssert}.
|
||||
* <li>It supports assertions on Objectify entities, which {@code PAssert} cannot not do.
|
||||
* Compared with PAssert-compatible options like {@code google.registry.tools.EntityWrapper}
|
||||
* or {@link EntityProto}, Objectify entities in Java give better-formatted error messages
|
||||
* when assertions fail.
|
||||
* </ul>
|
||||
* This way it supports assertions on Objectify entities, which {@code PAssert} cannot do ( since
|
||||
* we have not implemented Coders for them). Compared with PAssert-compatible options like {@code
|
||||
* google.registry.tools.EntityWrapper} or {@link EntityProto}, Objectify entities in Java give
|
||||
* better-formatted error messages when assertions fail.
|
||||
*
|
||||
* <p>Each {@code expected} {@link KV key-value pair} refers to a versioned state of an Ofy
|
||||
* entity. The {@link KV#getKey key} is the timestamp, while the {@link KV#getValue value} is
|
||||
|
||||
@@ -16,16 +16,36 @@ package google.registry.flows;
|
||||
|
||||
import static google.registry.testing.DatabaseHelper.loadRegistrar;
|
||||
import static google.registry.testing.DatabaseHelper.persistResource;
|
||||
import static google.registry.testing.LogsSubject.assertAboutLogs;
|
||||
import static google.registry.util.DateTimeUtils.START_OF_TIME;
|
||||
import static google.registry.util.X509Utils.encodeX509Certificate;
|
||||
import static google.registry.util.X509Utils.encodeX509CertificateFromPemString;
|
||||
import static org.joda.time.DateTimeZone.UTC;
|
||||
|
||||
import com.google.common.collect.ImmutableMap;
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
import com.google.common.collect.ImmutableSortedMap;
|
||||
import com.google.common.testing.TestLogHandler;
|
||||
import google.registry.config.RegistryEnvironment;
|
||||
import google.registry.flows.certs.CertificateChecker;
|
||||
import google.registry.testing.AppEngineExtension;
|
||||
import google.registry.testing.CertificateSamples;
|
||||
import google.registry.testing.SystemPropertyExtension;
|
||||
import google.registry.util.SelfSignedCaCertificate;
|
||||
import java.io.StringWriter;
|
||||
import java.security.cert.CertificateException;
|
||||
import java.security.cert.X509Certificate;
|
||||
import java.util.Optional;
|
||||
import java.util.logging.Level;
|
||||
import java.util.logging.Logger;
|
||||
import org.joda.time.DateTime;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Order;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.RegisterExtension;
|
||||
import org.testcontainers.shaded.org.bouncycastle.openssl.jcajce.JcaMiscPEMGenerator;
|
||||
import org.testcontainers.shaded.org.bouncycastle.util.io.pem.PemObjectGenerator;
|
||||
import org.testcontainers.shaded.org.bouncycastle.util.io.pem.PemWriter;
|
||||
|
||||
/** Test logging in with TLS credentials. */
|
||||
class EppLoginTlsTest extends EppTestCase {
|
||||
@@ -34,18 +54,40 @@ class EppLoginTlsTest extends EppTestCase {
|
||||
final AppEngineExtension appEngine =
|
||||
AppEngineExtension.builder().withDatastoreAndCloudSql().build();
|
||||
|
||||
void setClientCertificateHash(String clientCertificateHash) {
|
||||
@RegisterExtension
|
||||
@Order(value = Integer.MAX_VALUE)
|
||||
final SystemPropertyExtension systemPropertyExtension = new SystemPropertyExtension();
|
||||
|
||||
private final CertificateChecker certificateChecker =
|
||||
new CertificateChecker(
|
||||
ImmutableSortedMap.of(START_OF_TIME, 825, DateTime.parse("2020-09-01T00:00:00Z"), 398),
|
||||
30,
|
||||
2048,
|
||||
ImmutableSet.of("secp256r1", "secp384r1"),
|
||||
clock);
|
||||
|
||||
private final Logger loggerToIntercept =
|
||||
Logger.getLogger(TlsCredentials.class.getCanonicalName());
|
||||
private final TestLogHandler handler = new TestLogHandler();
|
||||
|
||||
private String encodedCertString;
|
||||
|
||||
void setCredentials(String clientCertificateHash, String clientCertificate) {
|
||||
setTransportCredentials(
|
||||
new TlsCredentials(
|
||||
true, Optional.ofNullable(clientCertificateHash), Optional.of("192.168.1.100:54321")));
|
||||
true,
|
||||
Optional.ofNullable(clientCertificateHash),
|
||||
Optional.ofNullable(clientCertificate),
|
||||
Optional.of("192.168.1.100:54321"),
|
||||
certificateChecker));
|
||||
}
|
||||
|
||||
@BeforeEach
|
||||
void beforeEach() {
|
||||
void beforeEach() throws CertificateException {
|
||||
persistResource(
|
||||
loadRegistrar("NewRegistrar")
|
||||
.asBuilder()
|
||||
.setClientCertificate(CertificateSamples.SAMPLE_CERT, DateTime.now(UTC))
|
||||
.setClientCertificate(CertificateSamples.SAMPLE_CERT3, DateTime.now(UTC))
|
||||
.build());
|
||||
// Set a cert for the second registrar, or else any cert will be allowed for login.
|
||||
persistResource(
|
||||
@@ -53,18 +95,20 @@ class EppLoginTlsTest extends EppTestCase {
|
||||
.asBuilder()
|
||||
.setClientCertificate(CertificateSamples.SAMPLE_CERT2, DateTime.now(UTC))
|
||||
.build());
|
||||
loggerToIntercept.addHandler(handler);
|
||||
encodedCertString = encodeX509CertificateFromPemString(CertificateSamples.SAMPLE_CERT3);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testLoginLogout() throws Exception {
|
||||
setClientCertificateHash(CertificateSamples.SAMPLE_CERT_HASH);
|
||||
setCredentials(null, encodedCertString);
|
||||
assertThatLoginSucceeds("NewRegistrar", "foo-BAR2");
|
||||
assertThatLogoutSucceeds();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testLogin_wrongPasswordFails() throws Exception {
|
||||
setClientCertificateHash(CertificateSamples.SAMPLE_CERT_HASH);
|
||||
setCredentials(CertificateSamples.SAMPLE_CERT3_HASH, encodedCertString);
|
||||
// For TLS login, we also check the epp xml password.
|
||||
assertThatLogin("NewRegistrar", "incorrect")
|
||||
.hasResponse(
|
||||
@@ -74,7 +118,7 @@ class EppLoginTlsTest extends EppTestCase {
|
||||
|
||||
@Test
|
||||
void testMultiLogin() throws Exception {
|
||||
setClientCertificateHash(CertificateSamples.SAMPLE_CERT_HASH);
|
||||
setCredentials(CertificateSamples.SAMPLE_CERT3_HASH, encodedCertString);
|
||||
assertThatLoginSucceeds("NewRegistrar", "foo-BAR2");
|
||||
assertThatLogoutSucceeds();
|
||||
assertThatLoginSucceeds("NewRegistrar", "foo-BAR2");
|
||||
@@ -88,7 +132,7 @@ class EppLoginTlsTest extends EppTestCase {
|
||||
|
||||
@Test
|
||||
void testNonAuthedLogin_fails() throws Exception {
|
||||
setClientCertificateHash(CertificateSamples.SAMPLE_CERT_HASH);
|
||||
setCredentials(CertificateSamples.SAMPLE_CERT3_HASH, encodedCertString);
|
||||
assertThatLogin("TheRegistrar", "password2")
|
||||
.hasResponse(
|
||||
"response_error.xml",
|
||||
@@ -98,7 +142,7 @@ class EppLoginTlsTest extends EppTestCase {
|
||||
|
||||
@Test
|
||||
void testBadCertificate_failsBadCertificate2200() throws Exception {
|
||||
setClientCertificateHash("laffo");
|
||||
setCredentials("laffo", "cert");
|
||||
assertThatLogin("NewRegistrar", "foo-BAR2")
|
||||
.hasResponse(
|
||||
"response_error.xml",
|
||||
@@ -108,7 +152,7 @@ class EppLoginTlsTest extends EppTestCase {
|
||||
|
||||
@Test
|
||||
void testGfeDidntProvideClientCertificate_failsMissingCertificate2200() throws Exception {
|
||||
setClientCertificateHash(null);
|
||||
setCredentials(null, null);
|
||||
assertThatLogin("NewRegistrar", "foo-BAR2")
|
||||
.hasResponse(
|
||||
"response_error.xml",
|
||||
@@ -117,12 +161,12 @@ class EppLoginTlsTest extends EppTestCase {
|
||||
|
||||
@Test
|
||||
void testGoodPrimaryCertificate() throws Exception {
|
||||
setClientCertificateHash(CertificateSamples.SAMPLE_CERT_HASH);
|
||||
setCredentials(null, encodedCertString);
|
||||
DateTime now = DateTime.now(UTC);
|
||||
persistResource(
|
||||
loadRegistrar("NewRegistrar")
|
||||
.asBuilder()
|
||||
.setClientCertificate(CertificateSamples.SAMPLE_CERT, now)
|
||||
.setClientCertificate(CertificateSamples.SAMPLE_CERT3, now)
|
||||
.setFailoverClientCertificate(CertificateSamples.SAMPLE_CERT2, now)
|
||||
.build());
|
||||
assertThatLoginSucceeds("NewRegistrar", "foo-BAR2");
|
||||
@@ -130,33 +174,33 @@ class EppLoginTlsTest extends EppTestCase {
|
||||
|
||||
@Test
|
||||
void testGoodFailoverCertificate() throws Exception {
|
||||
setClientCertificateHash(CertificateSamples.SAMPLE_CERT2_HASH);
|
||||
setCredentials(null, encodedCertString);
|
||||
DateTime now = DateTime.now(UTC);
|
||||
persistResource(
|
||||
loadRegistrar("NewRegistrar")
|
||||
.asBuilder()
|
||||
.setClientCertificate(CertificateSamples.SAMPLE_CERT, now)
|
||||
.setFailoverClientCertificate(CertificateSamples.SAMPLE_CERT2, now)
|
||||
.setFailoverClientCertificate(CertificateSamples.SAMPLE_CERT3, now)
|
||||
.build());
|
||||
assertThatLoginSucceeds("NewRegistrar", "foo-BAR2");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testMissingPrimaryCertificateButHasFailover_usesFailover() throws Exception {
|
||||
setClientCertificateHash(CertificateSamples.SAMPLE_CERT2_HASH);
|
||||
setCredentials(null, encodedCertString);
|
||||
DateTime now = DateTime.now(UTC);
|
||||
persistResource(
|
||||
loadRegistrar("NewRegistrar")
|
||||
.asBuilder()
|
||||
.setClientCertificate(null, now)
|
||||
.setFailoverClientCertificate(CertificateSamples.SAMPLE_CERT2, now)
|
||||
.setFailoverClientCertificate(CertificateSamples.SAMPLE_CERT3, now)
|
||||
.build());
|
||||
assertThatLoginSucceeds("NewRegistrar", "foo-BAR2");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testRegistrarHasNoCertificatesOnFile_fails() throws Exception {
|
||||
setClientCertificateHash("laffo");
|
||||
setCredentials("laffo", "cert");
|
||||
DateTime now = DateTime.now(UTC);
|
||||
persistResource(
|
||||
loadRegistrar("NewRegistrar")
|
||||
@@ -169,4 +213,145 @@ class EppLoginTlsTest extends EppTestCase {
|
||||
"response_error.xml",
|
||||
ImmutableMap.of("CODE", "2200", "MSG", "Registrar certificate is not configured"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testCertificateDoesNotMeetRequirements_fails() throws Exception {
|
||||
String proxyEncoded = encodeX509CertificateFromPemString(CertificateSamples.SAMPLE_CERT);
|
||||
// SAMPLE_CERT has a validity period that is too long
|
||||
setCredentials(CertificateSamples.SAMPLE_CERT_HASH, proxyEncoded);
|
||||
persistResource(
|
||||
loadRegistrar("NewRegistrar")
|
||||
.asBuilder()
|
||||
.setClientCertificate(CertificateSamples.SAMPLE_CERT, clock.nowUtc())
|
||||
.setFailoverClientCertificate(CertificateSamples.SAMPLE_CERT2, clock.nowUtc())
|
||||
.build());
|
||||
assertThatLogin("NewRegistrar", "foo-BAR2")
|
||||
.hasResponse(
|
||||
"response_error.xml",
|
||||
ImmutableMap.of(
|
||||
"CODE",
|
||||
"2200",
|
||||
"MSG",
|
||||
"Registrar certificate contains the following security violations:\n"
|
||||
+ "Certificate validity period is too long; it must be less than or equal to"
|
||||
+ " 398 days."));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testCertificateDoesNotMeetMultipleRequirements_fails() throws Exception {
|
||||
X509Certificate certificate =
|
||||
SelfSignedCaCertificate.create(
|
||||
"test", clock.nowUtc().plusDays(100), clock.nowUtc().plusDays(5000))
|
||||
.cert();
|
||||
|
||||
StringWriter sw = new StringWriter();
|
||||
try (PemWriter pw = new PemWriter(sw)) {
|
||||
PemObjectGenerator generator = new JcaMiscPEMGenerator(certificate);
|
||||
pw.writeObject(generator);
|
||||
}
|
||||
|
||||
String proxyEncoded = encodeX509Certificate(certificate);
|
||||
|
||||
// SAMPLE_CERT has a validity period that is too long
|
||||
setCredentials(null, proxyEncoded);
|
||||
persistResource(
|
||||
loadRegistrar("NewRegistrar")
|
||||
.asBuilder()
|
||||
.setClientCertificate(sw.toString(), clock.nowUtc())
|
||||
.setFailoverClientCertificate(CertificateSamples.SAMPLE_CERT2, clock.nowUtc())
|
||||
.build());
|
||||
assertThatLogin("NewRegistrar", "foo-BAR2")
|
||||
.hasResponse(
|
||||
"response_error.xml",
|
||||
ImmutableMap.of(
|
||||
"CODE",
|
||||
"2200",
|
||||
"MSG",
|
||||
"Registrar certificate contains the following security violations:\n"
|
||||
+ "Certificate is expired.\n"
|
||||
+ "Certificate validity period is too long; it must be less than or equal to"
|
||||
+ " 398 days."));
|
||||
}
|
||||
|
||||
@Test
|
||||
// TODO(sarahbot@): Remove this test once requirements are enforced in production
|
||||
void testCertificateDoesNotMeetRequirementsInProduction_succeeds() throws Exception {
|
||||
RegistryEnvironment.PRODUCTION.setup(systemPropertyExtension);
|
||||
// SAMPLE_CERT has a validity period that is too long
|
||||
String proxyEncoded = encodeX509CertificateFromPemString(CertificateSamples.SAMPLE_CERT);
|
||||
setCredentials(null, proxyEncoded);
|
||||
persistResource(
|
||||
loadRegistrar("NewRegistrar")
|
||||
.asBuilder()
|
||||
.setClientCertificate(CertificateSamples.SAMPLE_CERT, clock.nowUtc())
|
||||
.setFailoverClientCertificate(CertificateSamples.SAMPLE_CERT2, clock.nowUtc())
|
||||
.build());
|
||||
// Even though the certificate contains security violations, the login will succeed in
|
||||
// production
|
||||
assertThatLoginSucceeds("NewRegistrar", "foo-BAR2");
|
||||
assertAboutLogs()
|
||||
.that(handler)
|
||||
.hasLogAtLevelWithMessage(
|
||||
Level.WARNING,
|
||||
"Registrar certificate used for NewRegistrar does not meet certificate requirements:"
|
||||
+ " Certificate validity period is too long; it must be less than or equal to 398"
|
||||
+ " days.");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testRegistrarCertificateContainsExtraMetadata_succeeds() throws Exception {
|
||||
String certPem =
|
||||
String.format(
|
||||
"Bag Attributes\n"
|
||||
+ " localKeyID: 1F 1C 3A 3A 4C 03 EC C4 BC 7A C3 21 A9 F2 13 66 21 B8 7B 26 \n"
|
||||
+ "subject=/C=US/ST=New York/L=New"
|
||||
+ " York/O=Test/OU=ABC/CN=tester.test/emailAddress=test-certificate@test.test\n"
|
||||
+ "issuer=/C=US/ST=NY/L=NYC/O=ABC/OU=TEST CA/CN=TEST"
|
||||
+ " CA/emailAddress=testing@test.test\n"
|
||||
+ "%s",
|
||||
CertificateSamples.SAMPLE_CERT3);
|
||||
|
||||
setCredentials(null, encodeX509CertificateFromPemString(certPem));
|
||||
DateTime now = DateTime.now(UTC);
|
||||
persistResource(
|
||||
loadRegistrar("NewRegistrar")
|
||||
.asBuilder()
|
||||
.setClientCertificate(certPem, now)
|
||||
.setFailoverClientCertificate(CertificateSamples.SAMPLE_CERT2, now)
|
||||
.build());
|
||||
assertThatLoginSucceeds("NewRegistrar", "foo-BAR2");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testRegistrarCertificateContainsExtraMetadataAndViolations_fails() throws Exception {
|
||||
String certPem =
|
||||
String.format(
|
||||
"Bag Attributes\n"
|
||||
+ " localKeyID: 1F 1C 3A 3A 4C 03 EC C4 BC 7A C3 21 A9 F2 13 66 21 B8 7B 26 \n"
|
||||
+ "subject=/C=US/ST=New York/L=New"
|
||||
+ " York/O=Test/OU=ABC/CN=tester.test/emailAddress=test-certificate@test.test\n"
|
||||
+ "issuer=/C=US/ST=NY/L=NYC/O=ABC/OU=TEST CA/CN=TEST"
|
||||
+ " CA/emailAddress=testing@test.test\n"
|
||||
+ "%s",
|
||||
CertificateSamples.SAMPLE_CERT);
|
||||
|
||||
setCredentials(null, encodeX509CertificateFromPemString(certPem));
|
||||
DateTime now = DateTime.now(UTC);
|
||||
persistResource(
|
||||
loadRegistrar("NewRegistrar")
|
||||
.asBuilder()
|
||||
.setClientCertificate(certPem, now)
|
||||
.setFailoverClientCertificate(CertificateSamples.SAMPLE_CERT2, now)
|
||||
.build());
|
||||
assertThatLogin("NewRegistrar", "foo-BAR2")
|
||||
.hasResponse(
|
||||
"response_error.xml",
|
||||
ImmutableMap.of(
|
||||
"CODE",
|
||||
"2200",
|
||||
"MSG",
|
||||
"Registrar certificate contains the following security violations:\n"
|
||||
+ "Certificate validity period is too long; it must be less than or equal to"
|
||||
+ " 398 days."));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,12 +45,8 @@ import javax.inject.Singleton;
|
||||
|
||||
/** Dagger component for running EPP tests. */
|
||||
@Singleton
|
||||
@Component(
|
||||
modules = {
|
||||
ConfigModule.class,
|
||||
EppTestComponent.FakesAndMocksModule.class
|
||||
})
|
||||
interface EppTestComponent {
|
||||
@Component(modules = {ConfigModule.class, EppTestComponent.FakesAndMocksModule.class})
|
||||
public interface EppTestComponent {
|
||||
|
||||
RequestComponent startRequest();
|
||||
|
||||
|
||||
@@ -19,6 +19,7 @@ import static com.google.common.truth.Truth.assertWithMessage;
|
||||
import static com.google.common.truth.Truth8.assertThat;
|
||||
import static google.registry.testing.TestDataHelper.loadFile;
|
||||
import static google.registry.testing.TestLogHandlerUtils.findFirstLogMessageByPrefix;
|
||||
import static google.registry.util.DateTimeUtils.START_OF_TIME;
|
||||
import static java.nio.charset.StandardCharsets.UTF_8;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.never;
|
||||
@@ -27,8 +28,10 @@ import static org.mockito.Mockito.verify;
|
||||
import com.google.common.base.Joiner;
|
||||
import com.google.common.base.Splitter;
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
import com.google.common.collect.ImmutableSortedMap;
|
||||
import com.google.common.flogger.LoggerConfig;
|
||||
import com.google.common.testing.TestLogHandler;
|
||||
import google.registry.flows.certs.CertificateChecker;
|
||||
import google.registry.model.eppcommon.Trid;
|
||||
import google.registry.model.eppoutput.EppOutput.ResponseOrGreeting;
|
||||
import google.registry.model.eppoutput.EppResponse;
|
||||
@@ -38,6 +41,7 @@ import google.registry.testing.FakeClock;
|
||||
import google.registry.testing.FakeHttpSession;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import org.joda.time.DateTime;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.RegisterExtension;
|
||||
@@ -54,6 +58,16 @@ class FlowRunnerTest {
|
||||
|
||||
private final TestLogHandler handler = new TestLogHandler();
|
||||
|
||||
protected final FakeClock clock = new FakeClock();
|
||||
|
||||
private final CertificateChecker certificateChecker =
|
||||
new CertificateChecker(
|
||||
ImmutableSortedMap.of(START_OF_TIME, 825, DateTime.parse("2020-09-01T00:00:00Z"), 398),
|
||||
30,
|
||||
2048,
|
||||
ImmutableSet.of("secp256r1", "secp384r1"),
|
||||
clock);
|
||||
|
||||
static class TestCommandFlow implements Flow {
|
||||
@Override
|
||||
public ResponseOrGreeting run() {
|
||||
@@ -138,10 +152,17 @@ class FlowRunnerTest {
|
||||
@Test
|
||||
void testRun_loggingStatement_tlsCredentials() throws Exception {
|
||||
flowRunner.credentials =
|
||||
new TlsCredentials(true, Optional.of("abc123def"), Optional.of("127.0.0.1"));
|
||||
new TlsCredentials(
|
||||
true,
|
||||
Optional.of("abc123def"),
|
||||
Optional.of("cert046F5A3"),
|
||||
Optional.of("127.0.0.1"),
|
||||
certificateChecker);
|
||||
flowRunner.run(eppMetricBuilder);
|
||||
assertThat(Splitter.on("\n\t").split(findFirstLogMessageByPrefix(handler, "EPP Command\n\t")))
|
||||
.contains("TlsCredentials{clientCertificateHash=abc123def, clientAddress=/127.0.0.1}");
|
||||
.contains(
|
||||
"TlsCredentials{clientCertificate=cert046F5A3, clientCertificateHash=abc123def,"
|
||||
+ " clientAddress=/127.0.0.1}");
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@@ -18,16 +18,20 @@ import static com.google.common.truth.Truth8.assertThat;
|
||||
import static google.registry.testing.CertificateSamples.SAMPLE_CERT;
|
||||
import static google.registry.testing.DatabaseHelper.loadRegistrar;
|
||||
import static google.registry.testing.DatabaseHelper.persistResource;
|
||||
import static org.joda.time.DateTimeZone.UTC;
|
||||
import static google.registry.util.DateTimeUtils.START_OF_TIME;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
import com.google.common.collect.ImmutableSortedMap;
|
||||
import google.registry.flows.TlsCredentials.BadRegistrarIpAddressException;
|
||||
import google.registry.flows.TlsCredentials.MissingRegistrarCertificateException;
|
||||
import google.registry.flows.TlsCredentials.RegistrarCertificateNotConfiguredException;
|
||||
import google.registry.flows.certs.CertificateChecker;
|
||||
import google.registry.model.registrar.Registrar;
|
||||
import google.registry.testing.AppEngineExtension;
|
||||
import google.registry.testing.FakeClock;
|
||||
import google.registry.util.CidrAddressBlock;
|
||||
import java.util.Optional;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
@@ -42,6 +46,16 @@ final class TlsCredentialsTest {
|
||||
final AppEngineExtension appEngine =
|
||||
AppEngineExtension.builder().withDatastoreAndCloudSql().build();
|
||||
|
||||
protected final FakeClock clock = new FakeClock();
|
||||
|
||||
private final CertificateChecker certificateChecker =
|
||||
new CertificateChecker(
|
||||
ImmutableSortedMap.of(START_OF_TIME, 825, DateTime.parse("2020-09-01T00:00:00Z"), 398),
|
||||
30,
|
||||
2048,
|
||||
ImmutableSet.of("secp256r1", "secp384r1"),
|
||||
clock);
|
||||
|
||||
@Test
|
||||
void testProvideClientCertificateHash() {
|
||||
HttpServletRequest req = mock(HttpServletRequest.class);
|
||||
@@ -50,12 +64,18 @@ final class TlsCredentialsTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
void testClientCertificateHash_missing() {
|
||||
TlsCredentials tls = new TlsCredentials(true, Optional.empty(), Optional.of("192.168.1.1"));
|
||||
void testClientCertificateAndHash_missing() {
|
||||
TlsCredentials tls =
|
||||
new TlsCredentials(
|
||||
true,
|
||||
Optional.empty(),
|
||||
Optional.empty(),
|
||||
Optional.of("192.168.1.1"),
|
||||
certificateChecker);
|
||||
persistResource(
|
||||
loadRegistrar("TheRegistrar")
|
||||
.asBuilder()
|
||||
.setClientCertificate(SAMPLE_CERT, DateTime.now(UTC))
|
||||
.setClientCertificate(SAMPLE_CERT, clock.nowUtc())
|
||||
.build());
|
||||
assertThrows(
|
||||
MissingRegistrarCertificateException.class,
|
||||
@@ -64,11 +84,13 @@ final class TlsCredentialsTest {
|
||||
|
||||
@Test
|
||||
void test_missingIpAddress_doesntAllowAccess() {
|
||||
TlsCredentials tls = new TlsCredentials(false, Optional.of("certHash"), Optional.empty());
|
||||
TlsCredentials tls =
|
||||
new TlsCredentials(
|
||||
false, Optional.of("certHash"), Optional.empty(), Optional.empty(), certificateChecker);
|
||||
persistResource(
|
||||
loadRegistrar("TheRegistrar")
|
||||
.asBuilder()
|
||||
.setClientCertificate(SAMPLE_CERT, DateTime.now(UTC))
|
||||
.setClientCertificate(SAMPLE_CERT, clock.nowUtc())
|
||||
.setIpAddressAllowList(ImmutableSet.of(CidrAddressBlock.create("3.5.8.13")))
|
||||
.build());
|
||||
assertThrows(
|
||||
@@ -79,12 +101,59 @@ final class TlsCredentialsTest {
|
||||
@Test
|
||||
void test_validateCertificate_canBeConfiguredToBypassCertHashes() throws Exception {
|
||||
TlsCredentials tls =
|
||||
new TlsCredentials(false, Optional.of("certHash"), Optional.of("192.168.1.1"));
|
||||
new TlsCredentials(
|
||||
false,
|
||||
Optional.of("certHash"),
|
||||
Optional.of("cert"),
|
||||
Optional.of("192.168.1.1"),
|
||||
certificateChecker);
|
||||
persistResource(
|
||||
loadRegistrar("TheRegistrar")
|
||||
.asBuilder()
|
||||
.setClientCertificate(null, DateTime.now(UTC))
|
||||
.setFailoverClientCertificate(null, DateTime.now(UTC))
|
||||
.setClientCertificate(null, clock.nowUtc())
|
||||
.setFailoverClientCertificate(null, clock.nowUtc())
|
||||
.build());
|
||||
// This would throw a RegistrarCertificateNotConfiguredException if cert hashes were not
|
||||
// bypassed
|
||||
tls.validateCertificate(Registrar.loadByClientId("TheRegistrar").get());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testProvideClientCertificate() {
|
||||
HttpServletRequest req = mock(HttpServletRequest.class);
|
||||
when(req.getHeader("X-SSL-Full-Certificate")).thenReturn("data");
|
||||
assertThat(TlsCredentials.EppTlsModule.provideClientCertificate(req)).hasValue("data");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testClientCertificate_notConfigured() {
|
||||
TlsCredentials tls =
|
||||
new TlsCredentials(
|
||||
true,
|
||||
Optional.of("hash"),
|
||||
Optional.of(SAMPLE_CERT),
|
||||
Optional.of("192.168.1.1"),
|
||||
certificateChecker);
|
||||
persistResource(loadRegistrar("TheRegistrar").asBuilder().build());
|
||||
assertThrows(
|
||||
RegistrarCertificateNotConfiguredException.class,
|
||||
() -> tls.validateCertificate(Registrar.loadByClientId("TheRegistrar").get()));
|
||||
}
|
||||
|
||||
@Test
|
||||
void test_validateCertificate_canBeConfiguredToBypassCerts() throws Exception {
|
||||
TlsCredentials tls =
|
||||
new TlsCredentials(
|
||||
false,
|
||||
Optional.of("certHash"),
|
||||
Optional.of("cert"),
|
||||
Optional.of("192.168.1.1"),
|
||||
certificateChecker);
|
||||
persistResource(
|
||||
loadRegistrar("TheRegistrar")
|
||||
.asBuilder()
|
||||
.setClientCertificate(null, clock.nowUtc())
|
||||
.setFailoverClientCertificate(null, clock.nowUtc())
|
||||
.build());
|
||||
// This would throw a RegistrarCertificateNotConfiguredException if cert hashes wren't bypassed.
|
||||
tls.validateCertificate(Registrar.loadByClientId("TheRegistrar").get());
|
||||
|
||||
+19
-18
@@ -19,7 +19,6 @@ import static com.google.common.truth.Truth.assertThat;
|
||||
import static google.registry.testing.ContactResourceSubject.assertAboutContacts;
|
||||
import static google.registry.testing.DatabaseHelper.assertNoBillingEvents;
|
||||
import static google.registry.testing.DatabaseHelper.createTld;
|
||||
import static google.registry.testing.DatabaseHelper.deleteResource;
|
||||
import static google.registry.testing.DatabaseHelper.getOnlyPollMessage;
|
||||
import static google.registry.testing.DatabaseHelper.getPollMessages;
|
||||
import static google.registry.testing.DatabaseHelper.persistResource;
|
||||
@@ -41,10 +40,12 @@ import google.registry.model.reporting.HistoryEntry;
|
||||
import google.registry.model.transfer.TransferData;
|
||||
import google.registry.model.transfer.TransferResponse;
|
||||
import google.registry.model.transfer.TransferStatus;
|
||||
import google.registry.testing.DualDatabaseTest;
|
||||
import google.registry.testing.TestOfyAndSql;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
/** Unit tests for {@link ContactTransferApproveFlow}. */
|
||||
@DualDatabaseTest
|
||||
class ContactTransferApproveFlowTest
|
||||
extends ContactTransferFlowTestCase<ContactTransferApproveFlow, ContactResource> {
|
||||
|
||||
@@ -121,24 +122,24 @@ class ContactTransferApproveFlowTest
|
||||
runFlow();
|
||||
}
|
||||
|
||||
@Test
|
||||
@TestOfyAndSql
|
||||
void testDryRun() throws Exception {
|
||||
setEppInput("contact_transfer_approve.xml");
|
||||
dryRunFlowAssertResponse(loadFile("contact_transfer_approve_response.xml"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@TestOfyAndSql
|
||||
void testSuccess() throws Exception {
|
||||
doSuccessfulTest("contact_transfer_approve.xml", "contact_transfer_approve_response.xml");
|
||||
}
|
||||
|
||||
@Test
|
||||
@TestOfyAndSql
|
||||
void testSuccess_withAuthinfo() throws Exception {
|
||||
doSuccessfulTest("contact_transfer_approve_with_authinfo.xml",
|
||||
"contact_transfer_approve_response.xml");
|
||||
}
|
||||
|
||||
@Test
|
||||
@TestOfyAndSql
|
||||
void testFailure_badContactPassword() {
|
||||
// Change the contact's password so it does not match the password in the file.
|
||||
contact = persistResource(
|
||||
@@ -152,7 +153,7 @@ class ContactTransferApproveFlowTest
|
||||
assertAboutEppExceptions().that(thrown).marshalsToXml();
|
||||
}
|
||||
|
||||
@Test
|
||||
@TestOfyAndSql
|
||||
void testFailure_neverBeenTransferred() {
|
||||
changeTransferStatus(null);
|
||||
EppException thrown =
|
||||
@@ -161,7 +162,7 @@ class ContactTransferApproveFlowTest
|
||||
assertAboutEppExceptions().that(thrown).marshalsToXml();
|
||||
}
|
||||
|
||||
@Test
|
||||
@TestOfyAndSql
|
||||
void testFailure_clientApproved() {
|
||||
changeTransferStatus(TransferStatus.CLIENT_APPROVED);
|
||||
EppException thrown =
|
||||
@@ -170,7 +171,7 @@ class ContactTransferApproveFlowTest
|
||||
assertAboutEppExceptions().that(thrown).marshalsToXml();
|
||||
}
|
||||
|
||||
@Test
|
||||
@TestOfyAndSql
|
||||
void testFailure_clientRejected() {
|
||||
changeTransferStatus(TransferStatus.CLIENT_REJECTED);
|
||||
EppException thrown =
|
||||
@@ -179,7 +180,7 @@ class ContactTransferApproveFlowTest
|
||||
assertAboutEppExceptions().that(thrown).marshalsToXml();
|
||||
}
|
||||
|
||||
@Test
|
||||
@TestOfyAndSql
|
||||
void testFailure_clientCancelled() {
|
||||
changeTransferStatus(TransferStatus.CLIENT_CANCELLED);
|
||||
EppException thrown =
|
||||
@@ -188,7 +189,7 @@ class ContactTransferApproveFlowTest
|
||||
assertAboutEppExceptions().that(thrown).marshalsToXml();
|
||||
}
|
||||
|
||||
@Test
|
||||
@TestOfyAndSql
|
||||
void testFailure_serverApproved() {
|
||||
changeTransferStatus(TransferStatus.SERVER_APPROVED);
|
||||
EppException thrown =
|
||||
@@ -197,7 +198,7 @@ class ContactTransferApproveFlowTest
|
||||
assertAboutEppExceptions().that(thrown).marshalsToXml();
|
||||
}
|
||||
|
||||
@Test
|
||||
@TestOfyAndSql
|
||||
void testFailure_serverCancelled() {
|
||||
changeTransferStatus(TransferStatus.SERVER_CANCELLED);
|
||||
EppException thrown =
|
||||
@@ -206,7 +207,7 @@ class ContactTransferApproveFlowTest
|
||||
assertAboutEppExceptions().that(thrown).marshalsToXml();
|
||||
}
|
||||
|
||||
@Test
|
||||
@TestOfyAndSql
|
||||
void testFailure_gainingClient() {
|
||||
setClientIdForFlow("NewRegistrar");
|
||||
EppException thrown =
|
||||
@@ -215,7 +216,7 @@ class ContactTransferApproveFlowTest
|
||||
assertAboutEppExceptions().that(thrown).marshalsToXml();
|
||||
}
|
||||
|
||||
@Test
|
||||
@TestOfyAndSql
|
||||
void testFailure_unrelatedClient() {
|
||||
setClientIdForFlow("ClientZ");
|
||||
EppException thrown =
|
||||
@@ -224,7 +225,7 @@ class ContactTransferApproveFlowTest
|
||||
assertAboutEppExceptions().that(thrown).marshalsToXml();
|
||||
}
|
||||
|
||||
@Test
|
||||
@TestOfyAndSql
|
||||
void testFailure_deletedContact() throws Exception {
|
||||
contact = persistResource(
|
||||
contact.asBuilder().setDeletionTime(clock.nowUtc().minusDays(1)).build());
|
||||
@@ -236,9 +237,9 @@ class ContactTransferApproveFlowTest
|
||||
assertAboutEppExceptions().that(thrown).marshalsToXml();
|
||||
}
|
||||
|
||||
@Test
|
||||
@TestOfyAndSql
|
||||
void testFailure_nonexistentContact() throws Exception {
|
||||
deleteResource(contact);
|
||||
persistResource(contact.asBuilder().setDeletionTime(clock.nowUtc().minusDays(1)).build());
|
||||
contact = persistResource(
|
||||
contact.asBuilder().setDeletionTime(clock.nowUtc().minusDays(1)).build());
|
||||
ResourceDoesNotExistException thrown =
|
||||
@@ -249,7 +250,7 @@ class ContactTransferApproveFlowTest
|
||||
assertAboutEppExceptions().that(thrown).marshalsToXml();
|
||||
}
|
||||
|
||||
@Test
|
||||
@TestOfyAndSql
|
||||
void testIcannActivityReportField_getsLogged() throws Exception {
|
||||
runFlow();
|
||||
assertIcannReportingActivityFieldLogged("srs-cont-transfer-approve");
|
||||
|
||||
+19
-18
@@ -18,7 +18,6 @@ import static com.google.common.collect.MoreCollectors.onlyElement;
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
import static google.registry.testing.ContactResourceSubject.assertAboutContacts;
|
||||
import static google.registry.testing.DatabaseHelper.assertNoBillingEvents;
|
||||
import static google.registry.testing.DatabaseHelper.deleteResource;
|
||||
import static google.registry.testing.DatabaseHelper.getOnlyPollMessage;
|
||||
import static google.registry.testing.DatabaseHelper.getPollMessages;
|
||||
import static google.registry.testing.DatabaseHelper.persistResource;
|
||||
@@ -38,10 +37,12 @@ import google.registry.model.reporting.HistoryEntry;
|
||||
import google.registry.model.transfer.TransferData;
|
||||
import google.registry.model.transfer.TransferResponse;
|
||||
import google.registry.model.transfer.TransferStatus;
|
||||
import google.registry.testing.DualDatabaseTest;
|
||||
import google.registry.testing.TestOfyAndSql;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
/** Unit tests for {@link ContactTransferCancelFlow}. */
|
||||
@DualDatabaseTest
|
||||
class ContactTransferCancelFlowTest
|
||||
extends ContactTransferFlowTestCase<ContactTransferCancelFlow, ContactResource> {
|
||||
|
||||
@@ -105,24 +106,24 @@ class ContactTransferCancelFlowTest
|
||||
runFlow();
|
||||
}
|
||||
|
||||
@Test
|
||||
@TestOfyAndSql
|
||||
void testDryRun() throws Exception {
|
||||
setEppInput("contact_transfer_cancel.xml");
|
||||
dryRunFlowAssertResponse(loadFile("contact_transfer_cancel_response.xml"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@TestOfyAndSql
|
||||
void testSuccess() throws Exception {
|
||||
doSuccessfulTest("contact_transfer_cancel.xml", "contact_transfer_cancel_response.xml");
|
||||
}
|
||||
|
||||
@Test
|
||||
@TestOfyAndSql
|
||||
void testSuccess_withAuthinfo() throws Exception {
|
||||
doSuccessfulTest("contact_transfer_cancel_with_authinfo.xml",
|
||||
"contact_transfer_cancel_response.xml");
|
||||
}
|
||||
|
||||
@Test
|
||||
@TestOfyAndSql
|
||||
void testFailure_badContactPassword() {
|
||||
// Change the contact's password so it does not match the password in the file.
|
||||
contact =
|
||||
@@ -138,7 +139,7 @@ class ContactTransferCancelFlowTest
|
||||
assertAboutEppExceptions().that(thrown).marshalsToXml();
|
||||
}
|
||||
|
||||
@Test
|
||||
@TestOfyAndSql
|
||||
void testFailure_neverBeenTransferred() {
|
||||
changeTransferStatus(null);
|
||||
EppException thrown =
|
||||
@@ -147,7 +148,7 @@ class ContactTransferCancelFlowTest
|
||||
assertAboutEppExceptions().that(thrown).marshalsToXml();
|
||||
}
|
||||
|
||||
@Test
|
||||
@TestOfyAndSql
|
||||
void testFailure_clientApproved() {
|
||||
changeTransferStatus(TransferStatus.CLIENT_APPROVED);
|
||||
EppException thrown =
|
||||
@@ -156,7 +157,7 @@ class ContactTransferCancelFlowTest
|
||||
assertAboutEppExceptions().that(thrown).marshalsToXml();
|
||||
}
|
||||
|
||||
@Test
|
||||
@TestOfyAndSql
|
||||
void testFailure_clientRejected() {
|
||||
changeTransferStatus(TransferStatus.CLIENT_REJECTED);
|
||||
EppException thrown =
|
||||
@@ -165,7 +166,7 @@ class ContactTransferCancelFlowTest
|
||||
assertAboutEppExceptions().that(thrown).marshalsToXml();
|
||||
}
|
||||
|
||||
@Test
|
||||
@TestOfyAndSql
|
||||
void testFailure_clientCancelled() {
|
||||
changeTransferStatus(TransferStatus.CLIENT_CANCELLED);
|
||||
EppException thrown =
|
||||
@@ -174,7 +175,7 @@ class ContactTransferCancelFlowTest
|
||||
assertAboutEppExceptions().that(thrown).marshalsToXml();
|
||||
}
|
||||
|
||||
@Test
|
||||
@TestOfyAndSql
|
||||
void testFailure_serverApproved() {
|
||||
changeTransferStatus(TransferStatus.SERVER_APPROVED);
|
||||
EppException thrown =
|
||||
@@ -183,7 +184,7 @@ class ContactTransferCancelFlowTest
|
||||
assertAboutEppExceptions().that(thrown).marshalsToXml();
|
||||
}
|
||||
|
||||
@Test
|
||||
@TestOfyAndSql
|
||||
void testFailure_serverCancelled() {
|
||||
changeTransferStatus(TransferStatus.SERVER_CANCELLED);
|
||||
EppException thrown =
|
||||
@@ -192,7 +193,7 @@ class ContactTransferCancelFlowTest
|
||||
assertAboutEppExceptions().that(thrown).marshalsToXml();
|
||||
}
|
||||
|
||||
@Test
|
||||
@TestOfyAndSql
|
||||
void testFailure_sponsoringClient() {
|
||||
setClientIdForFlow("TheRegistrar");
|
||||
EppException thrown =
|
||||
@@ -202,7 +203,7 @@ class ContactTransferCancelFlowTest
|
||||
assertAboutEppExceptions().that(thrown).marshalsToXml();
|
||||
}
|
||||
|
||||
@Test
|
||||
@TestOfyAndSql
|
||||
void testFailure_unrelatedClient() {
|
||||
setClientIdForFlow("ClientZ");
|
||||
EppException thrown =
|
||||
@@ -212,7 +213,7 @@ class ContactTransferCancelFlowTest
|
||||
assertAboutEppExceptions().that(thrown).marshalsToXml();
|
||||
}
|
||||
|
||||
@Test
|
||||
@TestOfyAndSql
|
||||
void testFailure_deletedContact() throws Exception {
|
||||
contact =
|
||||
persistResource(contact.asBuilder().setDeletionTime(clock.nowUtc().minusDays(1)).build());
|
||||
@@ -224,9 +225,9 @@ class ContactTransferCancelFlowTest
|
||||
assertAboutEppExceptions().that(thrown).marshalsToXml();
|
||||
}
|
||||
|
||||
@Test
|
||||
@TestOfyAndSql
|
||||
void testFailure_nonexistentContact() throws Exception {
|
||||
deleteResource(contact);
|
||||
persistResource(contact.asBuilder().setDeletionTime(clock.nowUtc().minusDays(1)).build());
|
||||
ResourceDoesNotExistException thrown =
|
||||
assertThrows(
|
||||
ResourceDoesNotExistException.class,
|
||||
@@ -235,7 +236,7 @@ class ContactTransferCancelFlowTest
|
||||
assertAboutEppExceptions().that(thrown).marshalsToXml();
|
||||
}
|
||||
|
||||
@Test
|
||||
@TestOfyAndSql
|
||||
void testIcannActivityReportField_getsLogged() throws Exception {
|
||||
runFlow();
|
||||
assertIcannReportingActivityFieldLogged("srs-cont-transfer-cancel");
|
||||
|
||||
+21
-20
@@ -17,7 +17,6 @@ package google.registry.flows.contact;
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
import static google.registry.testing.ContactResourceSubject.assertAboutContacts;
|
||||
import static google.registry.testing.DatabaseHelper.assertNoBillingEvents;
|
||||
import static google.registry.testing.DatabaseHelper.deleteResource;
|
||||
import static google.registry.testing.DatabaseHelper.persistResource;
|
||||
import static google.registry.testing.EppExceptionSubject.assertAboutEppExceptions;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
@@ -32,11 +31,13 @@ import google.registry.model.contact.ContactResource;
|
||||
import google.registry.model.eppcommon.AuthInfo.PasswordAuth;
|
||||
import google.registry.model.reporting.HistoryEntry;
|
||||
import google.registry.model.transfer.TransferStatus;
|
||||
import google.registry.testing.DualDatabaseTest;
|
||||
import google.registry.testing.TestOfyAndSql;
|
||||
import org.joda.time.DateTime;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
/** Unit tests for {@link ContactTransferQueryFlow}. */
|
||||
@DualDatabaseTest
|
||||
class ContactTransferQueryFlowTest
|
||||
extends ContactTransferFlowTestCase<ContactTransferQueryFlow, ContactResource> {
|
||||
|
||||
@@ -68,65 +69,65 @@ class ContactTransferQueryFlowTest
|
||||
runFlow();
|
||||
}
|
||||
|
||||
@Test
|
||||
@TestOfyAndSql
|
||||
void testSuccess() throws Exception {
|
||||
doSuccessfulTest("contact_transfer_query.xml", "contact_transfer_query_response.xml");
|
||||
}
|
||||
|
||||
@Test
|
||||
@TestOfyAndSql
|
||||
void testSuccess_withContactRoid() throws Exception {
|
||||
doSuccessfulTest("contact_transfer_query_with_roid.xml", "contact_transfer_query_response.xml");
|
||||
}
|
||||
|
||||
@Test
|
||||
@TestOfyAndSql
|
||||
void testSuccess_sponsoringClient() throws Exception {
|
||||
setClientIdForFlow("TheRegistrar");
|
||||
doSuccessfulTest("contact_transfer_query.xml", "contact_transfer_query_response.xml");
|
||||
}
|
||||
|
||||
@Test
|
||||
@TestOfyAndSql
|
||||
void testSuccess_withAuthinfo() throws Exception {
|
||||
setClientIdForFlow("ClientZ");
|
||||
doSuccessfulTest("contact_transfer_query_with_authinfo.xml",
|
||||
"contact_transfer_query_response.xml");
|
||||
}
|
||||
|
||||
@Test
|
||||
@TestOfyAndSql
|
||||
void testSuccess_clientApproved() throws Exception {
|
||||
changeTransferStatus(TransferStatus.CLIENT_APPROVED);
|
||||
doSuccessfulTest("contact_transfer_query.xml",
|
||||
"contact_transfer_query_response_client_approved.xml");
|
||||
}
|
||||
|
||||
@Test
|
||||
@TestOfyAndSql
|
||||
void testSuccess_clientRejected() throws Exception {
|
||||
changeTransferStatus(TransferStatus.CLIENT_REJECTED);
|
||||
doSuccessfulTest("contact_transfer_query.xml",
|
||||
"contact_transfer_query_response_client_rejected.xml");
|
||||
}
|
||||
|
||||
@Test
|
||||
@TestOfyAndSql
|
||||
void testSuccess_clientCancelled() throws Exception {
|
||||
changeTransferStatus(TransferStatus.CLIENT_CANCELLED);
|
||||
doSuccessfulTest("contact_transfer_query.xml",
|
||||
"contact_transfer_query_response_client_cancelled.xml");
|
||||
}
|
||||
|
||||
@Test
|
||||
@TestOfyAndSql
|
||||
void testSuccess_serverApproved() throws Exception {
|
||||
changeTransferStatus(TransferStatus.SERVER_APPROVED);
|
||||
doSuccessfulTest("contact_transfer_query.xml",
|
||||
"contact_transfer_query_response_server_approved.xml");
|
||||
}
|
||||
|
||||
@Test
|
||||
@TestOfyAndSql
|
||||
void testSuccess_serverCancelled() throws Exception {
|
||||
changeTransferStatus(TransferStatus.SERVER_CANCELLED);
|
||||
doSuccessfulTest("contact_transfer_query.xml",
|
||||
"contact_transfer_query_response_server_cancelled.xml");
|
||||
}
|
||||
|
||||
@Test
|
||||
@TestOfyAndSql
|
||||
void testFailure_pendingDeleteContact() throws Exception {
|
||||
changeTransferStatus(TransferStatus.SERVER_CANCELLED);
|
||||
contact = persistResource(
|
||||
@@ -135,7 +136,7 @@ class ContactTransferQueryFlowTest
|
||||
"contact_transfer_query_response_server_cancelled.xml");
|
||||
}
|
||||
|
||||
@Test
|
||||
@TestOfyAndSql
|
||||
void testFailure_badContactPassword() {
|
||||
// Change the contact's password so it does not match the password in the file.
|
||||
contact =
|
||||
@@ -151,7 +152,7 @@ class ContactTransferQueryFlowTest
|
||||
assertAboutEppExceptions().that(thrown).marshalsToXml();
|
||||
}
|
||||
|
||||
@Test
|
||||
@TestOfyAndSql
|
||||
void testFailure_badContactRoid() {
|
||||
// Set the contact to a different ROID, but don't persist it; this is just so the substitution
|
||||
// code above will write the wrong ROID into the file.
|
||||
@@ -163,7 +164,7 @@ class ContactTransferQueryFlowTest
|
||||
assertAboutEppExceptions().that(thrown).marshalsToXml();
|
||||
}
|
||||
|
||||
@Test
|
||||
@TestOfyAndSql
|
||||
void testFailure_neverBeenTransferred() {
|
||||
changeTransferStatus(null);
|
||||
EppException thrown =
|
||||
@@ -173,7 +174,7 @@ class ContactTransferQueryFlowTest
|
||||
assertAboutEppExceptions().that(thrown).marshalsToXml();
|
||||
}
|
||||
|
||||
@Test
|
||||
@TestOfyAndSql
|
||||
void testFailure_unrelatedClient() {
|
||||
setClientIdForFlow("ClientZ");
|
||||
EppException thrown =
|
||||
@@ -183,7 +184,7 @@ class ContactTransferQueryFlowTest
|
||||
assertAboutEppExceptions().that(thrown).marshalsToXml();
|
||||
}
|
||||
|
||||
@Test
|
||||
@TestOfyAndSql
|
||||
void testFailure_deletedContact() throws Exception {
|
||||
contact =
|
||||
persistResource(contact.asBuilder().setDeletionTime(clock.nowUtc().minusDays(1)).build());
|
||||
@@ -194,9 +195,9 @@ class ContactTransferQueryFlowTest
|
||||
assertAboutEppExceptions().that(thrown).marshalsToXml();
|
||||
}
|
||||
|
||||
@Test
|
||||
@TestOfyAndSql
|
||||
void testFailure_nonexistentContact() throws Exception {
|
||||
deleteResource(contact);
|
||||
persistResource(contact.asBuilder().setDeletionTime(clock.nowUtc().minusDays(1)).build());
|
||||
ResourceDoesNotExistException thrown =
|
||||
assertThrows(
|
||||
ResourceDoesNotExistException.class, () -> doFailingTest("contact_transfer_query.xml"));
|
||||
@@ -204,7 +205,7 @@ class ContactTransferQueryFlowTest
|
||||
assertAboutEppExceptions().that(thrown).marshalsToXml();
|
||||
}
|
||||
|
||||
@Test
|
||||
@TestOfyAndSql
|
||||
void testIcannActivityReportField_getsLogged() throws Exception {
|
||||
runFlow();
|
||||
assertIcannReportingActivityFieldLogged("srs-cont-transfer-query");
|
||||
|
||||
+19
-18
@@ -18,7 +18,6 @@ import static com.google.common.collect.MoreCollectors.onlyElement;
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
import static google.registry.testing.ContactResourceSubject.assertAboutContacts;
|
||||
import static google.registry.testing.DatabaseHelper.assertNoBillingEvents;
|
||||
import static google.registry.testing.DatabaseHelper.deleteResource;
|
||||
import static google.registry.testing.DatabaseHelper.getOnlyPollMessage;
|
||||
import static google.registry.testing.DatabaseHelper.getPollMessages;
|
||||
import static google.registry.testing.DatabaseHelper.persistResource;
|
||||
@@ -40,10 +39,12 @@ import google.registry.model.reporting.HistoryEntry;
|
||||
import google.registry.model.transfer.TransferData;
|
||||
import google.registry.model.transfer.TransferResponse;
|
||||
import google.registry.model.transfer.TransferStatus;
|
||||
import google.registry.testing.DualDatabaseTest;
|
||||
import google.registry.testing.TestOfyAndSql;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
/** Unit tests for {@link ContactTransferRejectFlow}. */
|
||||
@DualDatabaseTest
|
||||
class ContactTransferRejectFlowTest
|
||||
extends ContactTransferFlowTestCase<ContactTransferRejectFlow, ContactResource> {
|
||||
|
||||
@@ -120,24 +121,24 @@ class ContactTransferRejectFlowTest
|
||||
runFlow();
|
||||
}
|
||||
|
||||
@Test
|
||||
@TestOfyAndSql
|
||||
void testDryRun() throws Exception {
|
||||
setEppInput("contact_transfer_reject.xml");
|
||||
dryRunFlowAssertResponse(loadFile("contact_transfer_reject_response.xml"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@TestOfyAndSql
|
||||
void testSuccess() throws Exception {
|
||||
doSuccessfulTest("contact_transfer_reject.xml", "contact_transfer_reject_response.xml");
|
||||
}
|
||||
|
||||
@Test
|
||||
@TestOfyAndSql
|
||||
void testSuccess_domainAuthInfo() throws Exception {
|
||||
doSuccessfulTest("contact_transfer_reject_with_authinfo.xml",
|
||||
"contact_transfer_reject_response.xml");
|
||||
}
|
||||
|
||||
@Test
|
||||
@TestOfyAndSql
|
||||
void testFailure_badPassword() {
|
||||
// Change the contact's password so it does not match the password in the file.
|
||||
contact =
|
||||
@@ -153,7 +154,7 @@ class ContactTransferRejectFlowTest
|
||||
assertAboutEppExceptions().that(thrown).marshalsToXml();
|
||||
}
|
||||
|
||||
@Test
|
||||
@TestOfyAndSql
|
||||
void testFailure_neverBeenTransferred() {
|
||||
changeTransferStatus(null);
|
||||
EppException thrown =
|
||||
@@ -162,7 +163,7 @@ class ContactTransferRejectFlowTest
|
||||
assertAboutEppExceptions().that(thrown).marshalsToXml();
|
||||
}
|
||||
|
||||
@Test
|
||||
@TestOfyAndSql
|
||||
void testFailure_clientApproved() {
|
||||
changeTransferStatus(TransferStatus.CLIENT_APPROVED);
|
||||
EppException thrown =
|
||||
@@ -171,7 +172,7 @@ class ContactTransferRejectFlowTest
|
||||
assertAboutEppExceptions().that(thrown).marshalsToXml();
|
||||
}
|
||||
|
||||
@Test
|
||||
@TestOfyAndSql
|
||||
void testFailure_clientRejected() {
|
||||
changeTransferStatus(TransferStatus.CLIENT_REJECTED);
|
||||
EppException thrown =
|
||||
@@ -180,7 +181,7 @@ class ContactTransferRejectFlowTest
|
||||
assertAboutEppExceptions().that(thrown).marshalsToXml();
|
||||
}
|
||||
|
||||
@Test
|
||||
@TestOfyAndSql
|
||||
void testFailure_clientCancelled() {
|
||||
changeTransferStatus(TransferStatus.CLIENT_CANCELLED);
|
||||
EppException thrown =
|
||||
@@ -189,7 +190,7 @@ class ContactTransferRejectFlowTest
|
||||
assertAboutEppExceptions().that(thrown).marshalsToXml();
|
||||
}
|
||||
|
||||
@Test
|
||||
@TestOfyAndSql
|
||||
void testFailure_serverApproved() {
|
||||
changeTransferStatus(TransferStatus.SERVER_APPROVED);
|
||||
EppException thrown =
|
||||
@@ -198,7 +199,7 @@ class ContactTransferRejectFlowTest
|
||||
assertAboutEppExceptions().that(thrown).marshalsToXml();
|
||||
}
|
||||
|
||||
@Test
|
||||
@TestOfyAndSql
|
||||
void testFailure_serverCancelled() {
|
||||
changeTransferStatus(TransferStatus.SERVER_CANCELLED);
|
||||
EppException thrown =
|
||||
@@ -207,7 +208,7 @@ class ContactTransferRejectFlowTest
|
||||
assertAboutEppExceptions().that(thrown).marshalsToXml();
|
||||
}
|
||||
|
||||
@Test
|
||||
@TestOfyAndSql
|
||||
void testFailure_gainingClient() {
|
||||
setClientIdForFlow("NewRegistrar");
|
||||
EppException thrown =
|
||||
@@ -216,7 +217,7 @@ class ContactTransferRejectFlowTest
|
||||
assertAboutEppExceptions().that(thrown).marshalsToXml();
|
||||
}
|
||||
|
||||
@Test
|
||||
@TestOfyAndSql
|
||||
void testFailure_unrelatedClient() {
|
||||
setClientIdForFlow("ClientZ");
|
||||
EppException thrown =
|
||||
@@ -225,7 +226,7 @@ class ContactTransferRejectFlowTest
|
||||
assertAboutEppExceptions().that(thrown).marshalsToXml();
|
||||
}
|
||||
|
||||
@Test
|
||||
@TestOfyAndSql
|
||||
void testFailure_deletedContact() throws Exception {
|
||||
contact =
|
||||
persistResource(contact.asBuilder().setDeletionTime(clock.nowUtc().minusDays(1)).build());
|
||||
@@ -237,9 +238,9 @@ class ContactTransferRejectFlowTest
|
||||
assertAboutEppExceptions().that(thrown).marshalsToXml();
|
||||
}
|
||||
|
||||
@Test
|
||||
@TestOfyAndSql
|
||||
void testFailure_nonexistentContact() throws Exception {
|
||||
deleteResource(contact);
|
||||
persistResource(contact.asBuilder().setDeletionTime(clock.nowUtc().minusDays(1)).build());
|
||||
ResourceDoesNotExistException thrown =
|
||||
assertThrows(
|
||||
ResourceDoesNotExistException.class,
|
||||
@@ -248,7 +249,7 @@ class ContactTransferRejectFlowTest
|
||||
assertAboutEppExceptions().that(thrown).marshalsToXml();
|
||||
}
|
||||
|
||||
@Test
|
||||
@TestOfyAndSql
|
||||
void testIcannActivityReportField_getsLogged() throws Exception {
|
||||
runFlow();
|
||||
assertIcannReportingActivityFieldLogged("srs-cont-transfer-reject");
|
||||
|
||||
+27
-29
@@ -20,7 +20,8 @@ import static com.google.common.collect.Iterables.getOnlyElement;
|
||||
import static com.google.common.collect.MoreCollectors.onlyElement;
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
import static google.registry.config.RegistryConfig.getContactAutomaticTransferLength;
|
||||
import static google.registry.model.ofy.ObjectifyService.ofy;
|
||||
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
|
||||
import static google.registry.persistence.transaction.TransactionManagerUtil.transactIfJpaTm;
|
||||
import static google.registry.testing.ContactResourceSubject.assertAboutContacts;
|
||||
import static google.registry.testing.DatabaseHelper.assertNoBillingEvents;
|
||||
import static google.registry.testing.DatabaseHelper.assertPollMessagesEqual;
|
||||
@@ -29,11 +30,11 @@ import static google.registry.testing.DatabaseHelper.getPollMessages;
|
||||
import static google.registry.testing.DatabaseHelper.persistActiveContact;
|
||||
import static google.registry.testing.DatabaseHelper.persistResource;
|
||||
import static google.registry.testing.EppExceptionSubject.assertAboutEppExceptions;
|
||||
import static google.registry.util.CollectionUtils.forceEmptyToNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.google.common.collect.Iterables;
|
||||
import com.googlecode.objectify.Key;
|
||||
import google.registry.flows.EppException;
|
||||
import google.registry.flows.ResourceFlowUtils.BadAuthInfoForResourceException;
|
||||
import google.registry.flows.ResourceFlowUtils.ResourceDoesNotExistException;
|
||||
@@ -50,12 +51,13 @@ import google.registry.model.poll.PollMessage;
|
||||
import google.registry.model.reporting.HistoryEntry;
|
||||
import google.registry.model.transfer.ContactTransferData;
|
||||
import google.registry.model.transfer.TransferStatus;
|
||||
import google.registry.persistence.VKey;
|
||||
import google.registry.testing.DualDatabaseTest;
|
||||
import google.registry.testing.TestOfyAndSql;
|
||||
import org.joda.time.DateTime;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
/** Unit tests for {@link ContactTransferRequestFlow}. */
|
||||
@DualDatabaseTest
|
||||
class ContactTransferRequestFlowTest
|
||||
extends ContactTransferFlowTestCase<ContactTransferRequestFlow, ContactResource> {
|
||||
|
||||
@@ -102,7 +104,8 @@ class ContactTransferRequestFlowTest
|
||||
.setPendingTransferExpirationTime(afterTransfer)
|
||||
// Make the server-approve entities field a no-op comparison; it's easier to
|
||||
// do this comparison separately below.
|
||||
.setServerApproveEntities(contact.getTransferData().getServerApproveEntities())
|
||||
.setServerApproveEntities(
|
||||
forceEmptyToNull(contact.getTransferData().getServerApproveEntities()))
|
||||
.build());
|
||||
assertNoBillingEvents();
|
||||
assertThat(getPollMessages("TheRegistrar", clock.nowUtc())).hasSize(1);
|
||||
@@ -126,13 +129,8 @@ class ContactTransferRequestFlowTest
|
||||
// poll messages, the approval notice ones for gaining and losing registrars.
|
||||
assertPollMessagesEqual(
|
||||
Iterables.filter(
|
||||
ofy()
|
||||
.load()
|
||||
// Use toArray() to coerce the type to something keys() will accept.
|
||||
.keys(
|
||||
contact.getTransferData().getServerApproveEntities().stream()
|
||||
.map(VKey::getOfyKey)
|
||||
.toArray(Key[]::new))
|
||||
transactIfJpaTm(
|
||||
() -> tm().loadByKeys(contact.getTransferData().getServerApproveEntities()))
|
||||
.values(),
|
||||
PollMessage.class),
|
||||
ImmutableList.of(gainingApproveMessage, losingApproveMessage));
|
||||
@@ -145,18 +143,18 @@ class ContactTransferRequestFlowTest
|
||||
runFlow();
|
||||
}
|
||||
|
||||
@Test
|
||||
@TestOfyAndSql
|
||||
void testDryRun() throws Exception {
|
||||
setEppInput("contact_transfer_request.xml");
|
||||
dryRunFlowAssertResponse(loadFile("contact_transfer_request_response.xml"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@TestOfyAndSql
|
||||
void testSuccess() throws Exception {
|
||||
doSuccessfulTest("contact_transfer_request.xml", "contact_transfer_request_response.xml");
|
||||
}
|
||||
|
||||
@Test
|
||||
@TestOfyAndSql
|
||||
void testFailure_noAuthInfo() {
|
||||
EppException thrown =
|
||||
assertThrows(
|
||||
@@ -165,7 +163,7 @@ class ContactTransferRequestFlowTest
|
||||
assertAboutEppExceptions().that(thrown).marshalsToXml();
|
||||
}
|
||||
|
||||
@Test
|
||||
@TestOfyAndSql
|
||||
void testFailure_badPassword() {
|
||||
// Change the contact's password so it does not match the password in the file.
|
||||
contact =
|
||||
@@ -181,37 +179,37 @@ class ContactTransferRequestFlowTest
|
||||
assertAboutEppExceptions().that(thrown).marshalsToXml();
|
||||
}
|
||||
|
||||
@Test
|
||||
@TestOfyAndSql
|
||||
void testSuccess_clientApproved() throws Exception {
|
||||
changeTransferStatus(TransferStatus.CLIENT_APPROVED);
|
||||
doSuccessfulTest("contact_transfer_request.xml", "contact_transfer_request_response.xml");
|
||||
}
|
||||
|
||||
@Test
|
||||
@TestOfyAndSql
|
||||
void testSuccess_clientRejected() throws Exception {
|
||||
changeTransferStatus(TransferStatus.CLIENT_REJECTED);
|
||||
doSuccessfulTest("contact_transfer_request.xml", "contact_transfer_request_response.xml");
|
||||
}
|
||||
|
||||
@Test
|
||||
@TestOfyAndSql
|
||||
void testSuccess_clientCancelled() throws Exception {
|
||||
changeTransferStatus(TransferStatus.CLIENT_CANCELLED);
|
||||
doSuccessfulTest("contact_transfer_request.xml", "contact_transfer_request_response.xml");
|
||||
}
|
||||
|
||||
@Test
|
||||
@TestOfyAndSql
|
||||
void testSuccess_serverApproved() throws Exception {
|
||||
changeTransferStatus(TransferStatus.SERVER_APPROVED);
|
||||
doSuccessfulTest("contact_transfer_request.xml", "contact_transfer_request_response.xml");
|
||||
}
|
||||
|
||||
@Test
|
||||
@TestOfyAndSql
|
||||
void testSuccess_serverCancelled() throws Exception {
|
||||
changeTransferStatus(TransferStatus.SERVER_CANCELLED);
|
||||
doSuccessfulTest("contact_transfer_request.xml", "contact_transfer_request_response.xml");
|
||||
}
|
||||
|
||||
@Test
|
||||
@TestOfyAndSql
|
||||
void testFailure_pending() {
|
||||
contact =
|
||||
persistResource(
|
||||
@@ -232,7 +230,7 @@ class ContactTransferRequestFlowTest
|
||||
assertAboutEppExceptions().that(thrown).marshalsToXml();
|
||||
}
|
||||
|
||||
@Test
|
||||
@TestOfyAndSql
|
||||
void testFailure_sponsoringClient() {
|
||||
setClientIdForFlow("TheRegistrar");
|
||||
EppException thrown =
|
||||
@@ -242,7 +240,7 @@ class ContactTransferRequestFlowTest
|
||||
assertAboutEppExceptions().that(thrown).marshalsToXml();
|
||||
}
|
||||
|
||||
@Test
|
||||
@TestOfyAndSql
|
||||
void testFailure_deletedContact() throws Exception {
|
||||
contact =
|
||||
persistResource(contact.asBuilder().setDeletionTime(clock.nowUtc().minusDays(1)).build());
|
||||
@@ -254,7 +252,7 @@ class ContactTransferRequestFlowTest
|
||||
assertAboutEppExceptions().that(thrown).marshalsToXml();
|
||||
}
|
||||
|
||||
@Test
|
||||
@TestOfyAndSql
|
||||
void testFailure_nonexistentContact() throws Exception {
|
||||
deleteResource(contact);
|
||||
ResourceDoesNotExistException thrown =
|
||||
@@ -265,7 +263,7 @@ class ContactTransferRequestFlowTest
|
||||
assertAboutEppExceptions().that(thrown).marshalsToXml();
|
||||
}
|
||||
|
||||
@Test
|
||||
@TestOfyAndSql
|
||||
void testFailure_clientTransferProhibited() {
|
||||
contact =
|
||||
persistResource(
|
||||
@@ -278,7 +276,7 @@ class ContactTransferRequestFlowTest
|
||||
assertAboutEppExceptions().that(thrown).marshalsToXml();
|
||||
}
|
||||
|
||||
@Test
|
||||
@TestOfyAndSql
|
||||
void testFailure_serverTransferProhibited() {
|
||||
contact =
|
||||
persistResource(
|
||||
@@ -291,7 +289,7 @@ class ContactTransferRequestFlowTest
|
||||
assertAboutEppExceptions().that(thrown).marshalsToXml();
|
||||
}
|
||||
|
||||
@Test
|
||||
@TestOfyAndSql
|
||||
void testFailure_pendingDelete() {
|
||||
contact =
|
||||
persistResource(contact.asBuilder().addStatusValue(StatusValue.PENDING_DELETE).build());
|
||||
@@ -303,7 +301,7 @@ class ContactTransferRequestFlowTest
|
||||
assertAboutEppExceptions().that(thrown).marshalsToXml();
|
||||
}
|
||||
|
||||
@Test
|
||||
@TestOfyAndSql
|
||||
void testIcannActivityReportField_getsLogged() throws Exception {
|
||||
runFlow();
|
||||
assertIcannReportingActivityFieldLogged("srs-cont-transfer-request");
|
||||
|
||||
@@ -184,7 +184,7 @@ class DomainCreateFlowTest extends ResourceFlowTestCase<DomainCreateFlow, Domain
|
||||
|
||||
@Order(value = Order.DEFAULT - 2)
|
||||
@RegisterExtension
|
||||
final ReplayExtension replayExtension = new ReplayExtension(clock);
|
||||
final ReplayExtension replayExtension = ReplayExtension.createWithCompare(clock);
|
||||
|
||||
DomainCreateFlowTest() {
|
||||
setEppInput("domain_create.xml", ImmutableMap.of("DOMAIN", "example.tld"));
|
||||
@@ -525,8 +525,7 @@ class DomainCreateFlowTest extends ResourceFlowTestCase<DomainCreateFlow, Domain
|
||||
clock.advanceOneMilli();
|
||||
runFlow();
|
||||
assertSuccessfulCreate("tld", ImmutableSet.of(), token);
|
||||
HistoryEntry historyEntry =
|
||||
ofy().load().type(HistoryEntry.class).ancestor(reloadResourceByForeignKey()).first().now();
|
||||
HistoryEntry historyEntry = getHistoryEntries(reloadResourceByForeignKey()).get(0);
|
||||
assertThat(ofy().load().entity(token).now().getRedemptionHistoryEntry())
|
||||
.hasValue(HistoryEntry.createVKey(Key.create(historyEntry)));
|
||||
}
|
||||
|
||||
@@ -112,7 +112,7 @@ class DomainDeleteFlowTest extends ResourceFlowTestCase<DomainDeleteFlow, Domain
|
||||
|
||||
@Order(value = Order.DEFAULT - 2)
|
||||
@RegisterExtension
|
||||
final ReplayExtension replayExtension = new ReplayExtension(clock);
|
||||
final ReplayExtension replayExtension = ReplayExtension.createWithoutCompare(clock);
|
||||
|
||||
private DomainBase domain;
|
||||
private HistoryEntry earlierHistoryEntry;
|
||||
|
||||
@@ -108,7 +108,7 @@ class DomainRenewFlowTest extends ResourceFlowTestCase<DomainRenewFlow, DomainBa
|
||||
|
||||
@Order(value = Order.DEFAULT - 2)
|
||||
@RegisterExtension
|
||||
final ReplayExtension replayExtension = new ReplayExtension(clock);
|
||||
final ReplayExtension replayExtension = ReplayExtension.createWithoutCompare(clock);
|
||||
|
||||
@BeforeEach
|
||||
void initDomainTest() {
|
||||
|
||||
@@ -117,7 +117,7 @@ class DomainUpdateFlowTest extends ResourceFlowTestCase<DomainUpdateFlow, Domain
|
||||
|
||||
@Order(value = Order.DEFAULT - 2)
|
||||
@RegisterExtension
|
||||
final ReplayExtension replayExtension = new ReplayExtension(clock);
|
||||
final ReplayExtension replayExtension = ReplayExtension.createWithoutCompare(clock);
|
||||
|
||||
@BeforeEach
|
||||
void initDomainTest() {
|
||||
@@ -919,7 +919,7 @@ class DomainUpdateFlowTest extends ResourceFlowTestCase<DomainUpdateFlow, Domain
|
||||
.addStatusValue(SERVER_UPDATE_PROHIBITED)
|
||||
.build());
|
||||
Exception e = assertThrows(ResourceStatusProhibitsOperationException.class, this::runFlow);
|
||||
assertThat(e).hasMessageThat().containsMatch("serverUpdateProhibited");
|
||||
assertThat(e).hasMessageThat().contains("serverUpdateProhibited");
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -937,7 +937,7 @@ class DomainUpdateFlowTest extends ResourceFlowTestCase<DomainUpdateFlow, Domain
|
||||
persistReferencedEntities();
|
||||
persistDomain();
|
||||
Exception e = assertThrows(StatusNotClientSettableException.class, this::runFlow);
|
||||
assertThat(e).hasMessageThat().containsMatch("serverUpdateProhibited");
|
||||
assertThat(e).hasMessageThat().contains("serverUpdateProhibited");
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@@ -174,9 +174,7 @@ class PollAckFlowTest extends FlowTestCase<PollAckFlow> {
|
||||
void testFailure_noSuchMessage() throws Exception {
|
||||
assertTransactionalFlow(true);
|
||||
Exception e = assertThrows(MessageDoesNotExistException.class, this::runFlow);
|
||||
assertThat(e)
|
||||
.hasMessageThat()
|
||||
.containsMatch(String.format("(1-3-EXAMPLE-4-%d-2011)", MESSAGE_ID));
|
||||
assertThat(e).hasMessageThat().contains(String.format("(1-3-EXAMPLE-4-%d-2011)", MESSAGE_ID));
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -255,8 +253,6 @@ class PollAckFlowTest extends FlowTestCase<PollAckFlow> {
|
||||
.build());
|
||||
assertTransactionalFlow(true);
|
||||
Exception e = assertThrows(MessageDoesNotExistException.class, this::runFlow);
|
||||
assertThat(e)
|
||||
.hasMessageThat()
|
||||
.containsMatch(String.format("(1-3-EXAMPLE-4-%d-2011)", MESSAGE_ID));
|
||||
assertThat(e).hasMessageThat().contains(String.format("(1-3-EXAMPLE-4-%d-2011)", MESSAGE_ID));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,37 +15,66 @@
|
||||
package google.registry.flows.session;
|
||||
|
||||
import static google.registry.testing.DatabaseHelper.persistResource;
|
||||
import static google.registry.util.DateTimeUtils.START_OF_TIME;
|
||||
import static google.registry.util.X509Utils.encodeX509CertificateFromPemString;
|
||||
import static org.joda.time.DateTimeZone.UTC;
|
||||
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
import com.google.common.collect.ImmutableSortedMap;
|
||||
import com.google.common.net.InetAddresses;
|
||||
import google.registry.flows.TlsCredentials;
|
||||
import google.registry.flows.TlsCredentials.BadRegistrarCertificateException;
|
||||
import google.registry.flows.TlsCredentials.BadRegistrarIpAddressException;
|
||||
import google.registry.flows.TlsCredentials.MissingRegistrarCertificateException;
|
||||
import google.registry.flows.certs.CertificateChecker;
|
||||
import google.registry.model.registrar.Registrar;
|
||||
import google.registry.testing.CertificateSamples;
|
||||
import google.registry.util.CidrAddressBlock;
|
||||
import google.registry.util.SelfSignedCaCertificate;
|
||||
import java.io.StringWriter;
|
||||
import java.security.cert.CertificateException;
|
||||
import java.security.cert.X509Certificate;
|
||||
import java.util.Base64;
|
||||
import java.util.Optional;
|
||||
import org.joda.time.DateTime;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.testcontainers.shaded.org.bouncycastle.openssl.jcajce.JcaMiscPEMGenerator;
|
||||
import org.testcontainers.shaded.org.bouncycastle.util.io.pem.PemObjectGenerator;
|
||||
import org.testcontainers.shaded.org.bouncycastle.util.io.pem.PemWriter;
|
||||
|
||||
/** Unit tests for {@link LoginFlow} when accessed via a TLS transport. */
|
||||
public class LoginFlowViaTlsTest extends LoginFlowTestCase {
|
||||
|
||||
private static final Optional<String> GOOD_CERT =
|
||||
Optional.of(CertificateSamples.SAMPLE_CERT_HASH);
|
||||
private static final Optional<String> BAD_CERT =
|
||||
private static final Optional<String> GOOD_CERT = Optional.of(CertificateSamples.SAMPLE_CERT3);
|
||||
private static final Optional<String> GOOD_CERT_HASH =
|
||||
Optional.of(CertificateSamples.SAMPLE_CERT3_HASH);
|
||||
private static final Optional<String> BAD_CERT = Optional.of(CertificateSamples.SAMPLE_CERT2);
|
||||
private static final Optional<String> BAD_CERT_HASH =
|
||||
Optional.of(CertificateSamples.SAMPLE_CERT2_HASH);
|
||||
private static final Optional<String> GOOD_IP = Optional.of("192.168.1.1");
|
||||
private static final Optional<String> BAD_IP = Optional.of("1.1.1.1");
|
||||
private static final Optional<String> GOOD_IPV6 = Optional.of("2001:db8::1");
|
||||
private static final Optional<String> BAD_IPV6 = Optional.of("2001:db8::2");
|
||||
private final CertificateChecker certificateChecker =
|
||||
new CertificateChecker(
|
||||
ImmutableSortedMap.of(START_OF_TIME, 825, DateTime.parse("2020-09-01T00:00:00Z"), 398),
|
||||
30,
|
||||
2048,
|
||||
ImmutableSet.of("secp256r1", "secp384r1"),
|
||||
clock);
|
||||
private Optional<String> encodedCertString;
|
||||
|
||||
@BeforeEach
|
||||
void beforeEach() throws CertificateException {
|
||||
encodedCertString = Optional.of(encodeX509CertificateFromPemString(GOOD_CERT.get()));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Registrar.Builder getRegistrarBuilder() {
|
||||
return super.getRegistrarBuilder()
|
||||
.setClientCertificate(CertificateSamples.SAMPLE_CERT, DateTime.now(UTC))
|
||||
.setClientCertificate(GOOD_CERT.get(), DateTime.now(UTC))
|
||||
.setIpAddressAllowList(
|
||||
ImmutableList.of(CidrAddressBlock.create(InetAddresses.forString(GOOD_IP.get()), 32)));
|
||||
}
|
||||
@@ -53,7 +82,36 @@ public class LoginFlowViaTlsTest extends LoginFlowTestCase {
|
||||
@Test
|
||||
void testSuccess_withGoodCredentials() throws Exception {
|
||||
persistResource(getRegistrarBuilder().build());
|
||||
credentials = new TlsCredentials(true, GOOD_CERT, GOOD_IP);
|
||||
credentials =
|
||||
new TlsCredentials(true, GOOD_CERT_HASH, encodedCertString, GOOD_IP, certificateChecker);
|
||||
doSuccessfulTest("login_valid.xml");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSuccess_withNewlyConstructedCertificate() throws Exception {
|
||||
X509Certificate certificate =
|
||||
SelfSignedCaCertificate.create(
|
||||
"test", clock.nowUtc().minusDays(100), clock.nowUtc().plusDays(150))
|
||||
.cert();
|
||||
|
||||
StringWriter sw = new StringWriter();
|
||||
try (PemWriter pw = new PemWriter(sw)) {
|
||||
PemObjectGenerator generator = new JcaMiscPEMGenerator(certificate);
|
||||
pw.writeObject(generator);
|
||||
}
|
||||
|
||||
persistResource(
|
||||
super.getRegistrarBuilder()
|
||||
.setClientCertificate(sw.toString(), DateTime.now(UTC))
|
||||
.setIpAddressAllowList(
|
||||
ImmutableList.of(
|
||||
CidrAddressBlock.create(InetAddresses.forString(GOOD_IP.get()), 32)))
|
||||
.build());
|
||||
|
||||
String encodedCertificate = Base64.getEncoder().encodeToString(certificate.getEncoded());
|
||||
credentials =
|
||||
new TlsCredentials(
|
||||
true, Optional.empty(), Optional.of(encodedCertificate), GOOD_IP, certificateChecker);
|
||||
doSuccessfulTest("login_valid.xml");
|
||||
}
|
||||
|
||||
@@ -64,7 +122,8 @@ public class LoginFlowViaTlsTest extends LoginFlowTestCase {
|
||||
.setIpAddressAllowList(
|
||||
ImmutableList.of(CidrAddressBlock.create("2001:db8:0:0:0:0:1:1/32")))
|
||||
.build());
|
||||
credentials = new TlsCredentials(true, GOOD_CERT, GOOD_IPV6);
|
||||
credentials =
|
||||
new TlsCredentials(true, GOOD_CERT_HASH, encodedCertString, GOOD_IPV6, certificateChecker);
|
||||
doSuccessfulTest("login_valid.xml");
|
||||
}
|
||||
|
||||
@@ -75,7 +134,8 @@ public class LoginFlowViaTlsTest extends LoginFlowTestCase {
|
||||
.setIpAddressAllowList(
|
||||
ImmutableList.of(CidrAddressBlock.create("2001:db8:0:0:0:0:1:1/32")))
|
||||
.build());
|
||||
credentials = new TlsCredentials(true, GOOD_CERT, GOOD_IPV6);
|
||||
credentials =
|
||||
new TlsCredentials(true, GOOD_CERT_HASH, encodedCertString, GOOD_IPV6, certificateChecker);
|
||||
doSuccessfulTest("login_valid.xml");
|
||||
}
|
||||
|
||||
@@ -85,21 +145,35 @@ public class LoginFlowViaTlsTest extends LoginFlowTestCase {
|
||||
getRegistrarBuilder()
|
||||
.setIpAddressAllowList(ImmutableList.of(CidrAddressBlock.create("192.168.1.255/24")))
|
||||
.build());
|
||||
credentials = new TlsCredentials(true, GOOD_CERT, GOOD_IP);
|
||||
credentials =
|
||||
new TlsCredentials(true, GOOD_CERT_HASH, encodedCertString, GOOD_IP, certificateChecker);
|
||||
doSuccessfulTest("login_valid.xml");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testFailure_incorrectClientCertificateHash() {
|
||||
void testFailure_incorrectClientCertificateHash() throws Exception {
|
||||
persistResource(getRegistrarBuilder().build());
|
||||
credentials = new TlsCredentials(true, BAD_CERT, GOOD_IP);
|
||||
String proxyEncoded = encodeX509CertificateFromPemString(BAD_CERT.get());
|
||||
credentials =
|
||||
new TlsCredentials(
|
||||
true, BAD_CERT_HASH, Optional.of(proxyEncoded), GOOD_IP, certificateChecker);
|
||||
doFailingTest("login_valid.xml", BadRegistrarCertificateException.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testFailure_missingClientCertificateHash() {
|
||||
// TODO(Sarahbot): This should fail once hash fallback is removed
|
||||
void testSuccess_missingClientCertificate() throws Exception {
|
||||
persistResource(getRegistrarBuilder().build());
|
||||
credentials = new TlsCredentials(true, Optional.empty(), GOOD_IP);
|
||||
credentials =
|
||||
new TlsCredentials(true, GOOD_CERT_HASH, Optional.empty(), GOOD_IP, certificateChecker);
|
||||
doSuccessfulTest("login_valid.xml");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testFailure_missingClientCertificateAndHash() {
|
||||
persistResource(getRegistrarBuilder().build());
|
||||
credentials =
|
||||
new TlsCredentials(true, Optional.empty(), Optional.empty(), GOOD_IP, certificateChecker);
|
||||
doFailingTest("login_valid.xml", MissingRegistrarCertificateException.class);
|
||||
}
|
||||
|
||||
@@ -112,7 +186,8 @@ public class LoginFlowViaTlsTest extends LoginFlowTestCase {
|
||||
CidrAddressBlock.create(InetAddresses.forString("192.168.1.1"), 32),
|
||||
CidrAddressBlock.create(InetAddresses.forString("2001:db8::1"), 128)))
|
||||
.build());
|
||||
credentials = new TlsCredentials(true, GOOD_CERT, Optional.empty());
|
||||
credentials =
|
||||
new TlsCredentials(true, GOOD_CERT_HASH, GOOD_CERT, Optional.empty(), certificateChecker);
|
||||
doFailingTest("login_valid.xml", BadRegistrarIpAddressException.class);
|
||||
}
|
||||
|
||||
@@ -125,7 +200,7 @@ public class LoginFlowViaTlsTest extends LoginFlowTestCase {
|
||||
CidrAddressBlock.create(InetAddresses.forString("192.168.1.1"), 32),
|
||||
CidrAddressBlock.create(InetAddresses.forString("2001:db8::1"), 128)))
|
||||
.build());
|
||||
credentials = new TlsCredentials(true, GOOD_CERT, BAD_IP);
|
||||
credentials = new TlsCredentials(true, GOOD_CERT_HASH, GOOD_CERT, BAD_IP, certificateChecker);
|
||||
doFailingTest("login_valid.xml", BadRegistrarIpAddressException.class);
|
||||
}
|
||||
|
||||
@@ -138,7 +213,7 @@ public class LoginFlowViaTlsTest extends LoginFlowTestCase {
|
||||
CidrAddressBlock.create(InetAddresses.forString("192.168.1.1"), 32),
|
||||
CidrAddressBlock.create(InetAddresses.forString("2001:db8::1"), 128)))
|
||||
.build());
|
||||
credentials = new TlsCredentials(true, GOOD_CERT, BAD_IPV6);
|
||||
credentials = new TlsCredentials(true, GOOD_CERT_HASH, GOOD_CERT, BAD_IPV6, certificateChecker);
|
||||
doFailingTest("login_valid.xml", BadRegistrarIpAddressException.class);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,19 +14,30 @@
|
||||
|
||||
package google.registry.model;
|
||||
|
||||
import static com.google.common.base.Preconditions.checkNotNull;
|
||||
import static com.google.common.truth.Truth.assertAbout;
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
import com.google.common.base.Joiner;
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
import com.google.common.collect.Sets;
|
||||
import com.google.common.truth.Correspondence;
|
||||
import com.google.common.truth.Correspondence.BinaryPredicate;
|
||||
import com.google.common.truth.FailureMetadata;
|
||||
import com.google.common.truth.SimpleSubjectBuilder;
|
||||
import com.google.common.truth.Subject;
|
||||
import java.lang.annotation.Annotation;
|
||||
import java.lang.reflect.Field;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.Iterator;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.function.Predicate;
|
||||
import java.util.stream.Collector;
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
/** Truth subject for asserting things about ImmutableObjects that are not built in. */
|
||||
@@ -53,6 +64,304 @@ public final class ImmutableObjectSubject extends Subject {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks that {@code expected} has the same contents as {@code actual} except for fields that are
|
||||
* marked with {@link ImmutableObject.DoNotCompare}.
|
||||
*
|
||||
* <p>This is used to verify that entities stored in both cloud SQL and Datastore are identical.
|
||||
*/
|
||||
public void isEqualAcrossDatabases(@Nullable ImmutableObject expected) {
|
||||
ComparisonResult result =
|
||||
checkObjectAcrossDatabases(
|
||||
actual, expected, actual != null ? actual.getClass().getName() : "null");
|
||||
if (result.isFailure()) {
|
||||
throw new AssertionError(result.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
// The following "check" methods implement a recursive check of immutable object equality across
|
||||
// databases. All of them function in both assertive and predicate modes: if "path" is
|
||||
// provided (not null) then they throw AssertionError's with detailed error messages. If
|
||||
// it is null, they return true for equal objects and false for inequal ones.
|
||||
//
|
||||
// The reason for this dual-mode behavior is that all of these methods can either be used in the
|
||||
// context of a test assertion (in which case we want a detailed error message describing exactly
|
||||
// the location in a complex object where a difference was discovered) or in the context of a
|
||||
// membership check in a set (in which case we don't care about the specific location of the first
|
||||
// difference, we just want to be able to determine if the object "is equal to" another object as
|
||||
// efficiently as possible -- see checkSetAcrossDatabase()).
|
||||
|
||||
@VisibleForTesting
|
||||
static ComparisonResult checkObjectAcrossDatabases(
|
||||
@Nullable Object actual, @Nullable Object expected, @Nullable String path) {
|
||||
if (Objects.equals(actual, expected)) {
|
||||
return ComparisonResult.createSuccess();
|
||||
}
|
||||
|
||||
// They're different, do a more detailed comparison.
|
||||
|
||||
// Check for null first (we can assume both variables are not null at this point).
|
||||
if (actual == null) {
|
||||
return ComparisonResult.createFailure(path, "expected ", expected, "got null.");
|
||||
} else if (expected == null) {
|
||||
return ComparisonResult.createFailure(path, "expected null, got ", actual);
|
||||
|
||||
// For immutable objects, we have to recurse since the contained
|
||||
// object could have DoNotCompare fields, too.
|
||||
} else if (expected instanceof ImmutableObject) {
|
||||
// We only verify that actual is an ImmutableObject so we get a good error message instead
|
||||
// of a context-less ClassCastException.
|
||||
if (!(actual instanceof ImmutableObject)) {
|
||||
return ComparisonResult.createFailure(path, actual, " is not an immutable object.");
|
||||
}
|
||||
|
||||
return checkImmutableAcrossDatabases(
|
||||
(ImmutableObject) actual, (ImmutableObject) expected, path);
|
||||
} else if (expected instanceof Map) {
|
||||
if (!(actual instanceof Map)) {
|
||||
return ComparisonResult.createFailure(path, actual, " is not a Map.");
|
||||
}
|
||||
|
||||
// This would likely be more efficient if we could assume that keys can be compared across
|
||||
// databases using .equals(), however we cannot guarantee key equality so the simplest and
|
||||
// most correct way to accomplish this is by reusing the set comparison.
|
||||
return checkSetAcrossDatabases(
|
||||
((Map<?, ?>) actual).entrySet(), ((Map<?, ?>) expected).entrySet(), path, "Map");
|
||||
} else if (expected instanceof Set) {
|
||||
if (!(actual instanceof Set)) {
|
||||
return ComparisonResult.createFailure(path, actual, " is not a Set.");
|
||||
}
|
||||
|
||||
return checkSetAcrossDatabases((Set<?>) actual, (Set<?>) expected, path, "Set");
|
||||
} else if (expected instanceof Collection) {
|
||||
if (!(actual instanceof Collection)) {
|
||||
return ComparisonResult.createFailure(path, actual, " is not a Collection.");
|
||||
}
|
||||
|
||||
return checkListAcrossDatabases((Collection<?>) actual, (Collection<?>) expected, path);
|
||||
// Give Map.Entry special treatment to facilitate the use of Set comparison for verification
|
||||
// of Map.
|
||||
} else if (expected instanceof Map.Entry) {
|
||||
if (!(actual instanceof Map.Entry)) {
|
||||
return ComparisonResult.createFailure(path, actual, " is not a Map.Entry.");
|
||||
}
|
||||
|
||||
// Check both the key and value. We can always ignore the path here, this should only be
|
||||
// called from within a set comparison.
|
||||
ComparisonResult result;
|
||||
if ((result =
|
||||
checkObjectAcrossDatabases(
|
||||
((Map.Entry<?, ?>) actual).getKey(), ((Map.Entry<?, ?>) expected).getKey(), null))
|
||||
.isFailure()) {
|
||||
return result;
|
||||
}
|
||||
if ((result =
|
||||
checkObjectAcrossDatabases(
|
||||
((Map.Entry<?, ?>) actual).getValue(),
|
||||
((Map.Entry<?, ?>) expected).getValue(),
|
||||
null))
|
||||
.isFailure()) {
|
||||
return result;
|
||||
}
|
||||
} else {
|
||||
// Since we know that the objects are not equal and since any other types can not be expected
|
||||
// to contain DoNotCompare elements, this condition is always a failure.
|
||||
return ComparisonResult.createFailure(path, actual, " is not equal to ", expected);
|
||||
}
|
||||
|
||||
return ComparisonResult.createSuccess();
|
||||
}
|
||||
|
||||
private static ComparisonResult checkSetAcrossDatabases(
|
||||
Set<?> actual, Set<?> expected, String path, String type) {
|
||||
// Unfortunately, we can't just check to see whether one set "contains" all of the elements of
|
||||
// the other, as the cross database checks don't require strict equality. Instead we have to do
|
||||
// an N^2 comparison to search for an equivalent element.
|
||||
|
||||
// Objects in expected that aren't in actual. We use "identity sets" here and below because we
|
||||
// want to keep track of the _objects themselves_ rather than rely upon any overridable notion
|
||||
// of equality.
|
||||
Set<Object> missing = path != null ? Sets.newIdentityHashSet() : null;
|
||||
|
||||
// Objects from actual that have matching elements in expected.
|
||||
Set<Object> found = Sets.newIdentityHashSet();
|
||||
|
||||
// Build missing and found.
|
||||
for (Object expectedElem : expected) {
|
||||
boolean gotMatch = false;
|
||||
for (Object actualElem : actual) {
|
||||
if (!checkObjectAcrossDatabases(actualElem, expectedElem, null).isFailure()) {
|
||||
gotMatch = true;
|
||||
|
||||
// Add the element to the set of expected elements that were "found" in actual. If the
|
||||
// element matches multiple elements in "expected," we have a basic problem with this
|
||||
// kind of set that we'll want to know about.
|
||||
if (!found.add(actualElem)) {
|
||||
return ComparisonResult.createFailure(
|
||||
path, "element ", actualElem, " matches multiple elements in ", expected);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!gotMatch) {
|
||||
if (path == null) {
|
||||
return ComparisonResult.createFailure();
|
||||
}
|
||||
missing.add(expectedElem);
|
||||
}
|
||||
}
|
||||
|
||||
if (path != null) {
|
||||
// Provide a detailed message consisting of any missing or unexpected items.
|
||||
|
||||
// Build a set of all objects in actual that don't have counterparts in expected.
|
||||
Set<Object> unexpected =
|
||||
actual.stream()
|
||||
.filter(actualElem -> !found.contains(actualElem))
|
||||
.collect(
|
||||
Collector.of(
|
||||
Sets::newIdentityHashSet,
|
||||
Set::add,
|
||||
(result, values) -> {
|
||||
result.addAll(values);
|
||||
return result;
|
||||
}));
|
||||
|
||||
if (!missing.isEmpty() || !unexpected.isEmpty()) {
|
||||
String message = type + " does not contain the expected contents.";
|
||||
if (!missing.isEmpty()) {
|
||||
message += " It is missing: " + formatItems(missing.iterator());
|
||||
}
|
||||
|
||||
if (!unexpected.isEmpty()) {
|
||||
message += " It contains additional elements: " + formatItems(unexpected.iterator());
|
||||
}
|
||||
|
||||
return ComparisonResult.createFailure(path, message);
|
||||
}
|
||||
|
||||
// We just need to check if there were any objects in "actual" that were not in "expected"
|
||||
// (where "found" is a proxy for "expected").
|
||||
} else if (actual.stream().anyMatch(Predicate.not(found::contains))) {
|
||||
return ComparisonResult.createFailure();
|
||||
}
|
||||
|
||||
return ComparisonResult.createSuccess();
|
||||
}
|
||||
|
||||
private static ComparisonResult checkListAcrossDatabases(
|
||||
Collection<?> actual, Collection<?> expected, @Nullable String path) {
|
||||
Iterator<?> actualIter = actual.iterator();
|
||||
Iterator<?> expectedIter = expected.iterator();
|
||||
int index = 0;
|
||||
while (actualIter.hasNext() && expectedIter.hasNext()) {
|
||||
Object actualItem = actualIter.next();
|
||||
Object expectedItem = expectedIter.next();
|
||||
ComparisonResult result =
|
||||
checkObjectAcrossDatabases(
|
||||
actualItem, expectedItem, path != null ? path + "[" + index + "]" : null);
|
||||
if (result.isFailure()) {
|
||||
return result;
|
||||
}
|
||||
++index;
|
||||
}
|
||||
|
||||
if (actualIter.hasNext()) {
|
||||
return ComparisonResult.createFailure(
|
||||
path, "has additional items: ", formatItems(actualIter));
|
||||
}
|
||||
|
||||
if (expectedIter.hasNext()) {
|
||||
return ComparisonResult.createFailure(path, "missing items: ", formatItems(expectedIter));
|
||||
}
|
||||
|
||||
return ComparisonResult.createSuccess();
|
||||
}
|
||||
|
||||
/** Recursive helper for isEqualAcrossDatabases. */
|
||||
private static ComparisonResult checkImmutableAcrossDatabases(
|
||||
ImmutableObject actual, ImmutableObject expected, String path) {
|
||||
Map<Field, Object> actualFields = filterFields(actual, ImmutableObject.DoNotCompare.class);
|
||||
Map<Field, Object> expectedFields = filterFields(expected, ImmutableObject.DoNotCompare.class);
|
||||
|
||||
for (Map.Entry<Field, Object> entry : expectedFields.entrySet()) {
|
||||
if (!actualFields.containsKey(entry.getKey())) {
|
||||
return ComparisonResult.createFailure(path, "is missing field ", entry.getKey().getName());
|
||||
}
|
||||
|
||||
// Verify that the field values are the same.
|
||||
Object expectedFieldValue = entry.getValue();
|
||||
Object actualFieldValue = actualFields.get(entry.getKey());
|
||||
ComparisonResult result =
|
||||
checkObjectAcrossDatabases(
|
||||
actualFieldValue,
|
||||
expectedFieldValue,
|
||||
path != null ? path + "." + entry.getKey().getName() : null);
|
||||
if (result.isFailure()) {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
// Check for fields in actual that are not in expected.
|
||||
for (Map.Entry<Field, Object> entry : actualFields.entrySet()) {
|
||||
if (!expectedFields.containsKey(entry.getKey())) {
|
||||
return ComparisonResult.createFailure(
|
||||
path, "has additional field ", entry.getKey().getName());
|
||||
}
|
||||
}
|
||||
|
||||
return ComparisonResult.createSuccess();
|
||||
}
|
||||
|
||||
private static String formatItems(Iterator<?> iter) {
|
||||
return Joiner.on(", ").join(iter);
|
||||
}
|
||||
|
||||
/** Encapsulates success/failure result in recursive comparison with optional error string. */
|
||||
static class ComparisonResult {
|
||||
boolean succeeded;
|
||||
String message;
|
||||
|
||||
private ComparisonResult(boolean succeeded, @Nullable String message) {
|
||||
this.succeeded = succeeded;
|
||||
this.message = message;
|
||||
}
|
||||
|
||||
static ComparisonResult createFailure() {
|
||||
return new ComparisonResult(false, null);
|
||||
}
|
||||
|
||||
static ComparisonResult createFailure(@Nullable String path, Object... message) {
|
||||
return new ComparisonResult(false, "At " + path + ": " + Joiner.on("").join(message));
|
||||
}
|
||||
|
||||
static ComparisonResult createSuccess() {
|
||||
return new ComparisonResult(true, null);
|
||||
}
|
||||
|
||||
String getMessage() {
|
||||
checkNotNull(message);
|
||||
return message;
|
||||
}
|
||||
|
||||
boolean isFailure() {
|
||||
return !succeeded;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks that the hash value reported by {@code actual} is correct.
|
||||
*
|
||||
* <p>This is used in the replay tests to ensure that hibernate hasn't modified any fields that
|
||||
* are not marked as @Insignificant while loading.
|
||||
*/
|
||||
public void hasCorrectHashValue() {
|
||||
assertThat(Arrays.hashCode(actual.getSignificantFields().values().toArray()))
|
||||
.isEqualTo(actual.hashCode());
|
||||
}
|
||||
|
||||
public static Correspondence<ImmutableObject, ImmutableObject> immutableObjectCorrespondence(
|
||||
String... ignoredFields) {
|
||||
return Correspondence.from(
|
||||
@@ -87,7 +396,8 @@ public final class ImmutableObjectSubject extends Subject {
|
||||
}
|
||||
}
|
||||
|
||||
public static Map<Field, Object> filterFields(ImmutableObject original, String... ignoredFields) {
|
||||
private static Map<Field, Object> filterFields(
|
||||
ImmutableObject original, String... ignoredFields) {
|
||||
ImmutableSet<String> ignoredFieldSet = ImmutableSet.copyOf(ignoredFields);
|
||||
Map<Field, Object> originalFields = ModelUtils.getFieldValues(original);
|
||||
// don't use ImmutableMap or a stream->collect model since we can have nulls
|
||||
@@ -99,4 +409,26 @@ public final class ImmutableObjectSubject extends Subject {
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/** Filter out fields with the given annotation. */
|
||||
private static Map<Field, Object> filterFields(
|
||||
ImmutableObject original, Class<? extends Annotation> annotation) {
|
||||
Map<Field, Object> originalFields = ModelUtils.getFieldValues(original);
|
||||
// don't use ImmutableMap or a stream->collect model since we can have nulls
|
||||
Map<Field, Object> result = new LinkedHashMap<>();
|
||||
for (Map.Entry<Field, Object> entry : originalFields.entrySet()) {
|
||||
if (!entry.getKey().isAnnotationPresent(annotation)) {
|
||||
|
||||
// Perform any necessary substitutions.
|
||||
if (entry.getKey().isAnnotationPresent(ImmutableObject.EmptySetToNull.class)
|
||||
&& entry.getValue() != null
|
||||
&& ((Set<?>) entry.getValue()).isEmpty()) {
|
||||
result.put(entry.getKey(), null);
|
||||
} else {
|
||||
result.put(entry.getKey(), entry.getValue());
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,481 @@
|
||||
// Copyright 2021 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.model;
|
||||
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
import static google.registry.model.ImmutableObjectSubject.ComparisonResult;
|
||||
import static google.registry.model.ImmutableObjectSubject.assertAboutImmutableObjects;
|
||||
import static google.registry.model.ImmutableObjectSubject.checkObjectAcrossDatabases;
|
||||
import static org.junit.Assert.assertThrows;
|
||||
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.google.common.collect.ImmutableMap;
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
import java.util.regex.Pattern;
|
||||
import javax.annotation.Nullable;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
public class ImmutableObjectSubjectTest {
|
||||
|
||||
// Unique id to assign to the "ignored" field so that it always gets a unique value.
|
||||
private static int uniqueId = 0;
|
||||
|
||||
@Test
|
||||
void testCrossDatabase_nulls() {
|
||||
assertAboutImmutableObjects().that(null).isEqualAcrossDatabases(null);
|
||||
assertAboutImmutableObjects()
|
||||
.that(makeTestAtom(null))
|
||||
.isEqualAcrossDatabases(makeTestAtom(null));
|
||||
|
||||
assertThat(checkObjectAcrossDatabases(null, makeTestAtom("foo"), null).isFailure()).isTrue();
|
||||
assertThat(checkObjectAcrossDatabases(null, makeTestAtom("foo"), null).isFailure()).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testCrossDatabase_equalObjects() {
|
||||
TestImmutableObject actual = makeTestObj();
|
||||
assertAboutImmutableObjects().that(actual).isEqualAcrossDatabases(actual);
|
||||
assertAboutImmutableObjects().that(actual).isEqualAcrossDatabases(makeTestObj());
|
||||
assertThat(checkObjectAcrossDatabases(makeTestObj(), makeTestObj(), null).isFailure())
|
||||
.isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testCrossDatabase_simpleFieldFailure() {
|
||||
AssertionError e =
|
||||
assertThrows(
|
||||
AssertionError.class,
|
||||
() ->
|
||||
assertAboutImmutableObjects()
|
||||
.that(makeTestObj())
|
||||
.isEqualAcrossDatabases(makeTestObj().withStringField("bar")));
|
||||
assertThat(e)
|
||||
.hasMessageThat()
|
||||
.contains(
|
||||
"At google.registry.model.ImmutableObjectSubjectTest$TestImmutableObject.stringField:");
|
||||
assertThat(
|
||||
checkObjectAcrossDatabases(makeTestObj(), makeTestObj().withStringField(null), null)
|
||||
.isFailure())
|
||||
.isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testCrossDatabase_nestedImmutableFailure() {
|
||||
// Repeat the null checks to verify that the attribute path is preserved.
|
||||
AssertionError e =
|
||||
assertThrows(
|
||||
AssertionError.class,
|
||||
() ->
|
||||
assertAboutImmutableObjects()
|
||||
.that(makeTestObj())
|
||||
.isEqualAcrossDatabases(makeTestObj().withNested(null)));
|
||||
assertThat(e)
|
||||
.hasMessageThat()
|
||||
.contains(
|
||||
"At google.registry.model.ImmutableObjectSubjectTest$TestImmutableObject.nested:"
|
||||
+ " expected null, got TestImmutableObject");
|
||||
e =
|
||||
assertThrows(
|
||||
AssertionError.class,
|
||||
() ->
|
||||
assertAboutImmutableObjects()
|
||||
.that(makeTestObj().withNested(null))
|
||||
.isEqualAcrossDatabases(makeTestObj()));
|
||||
assertThat(e)
|
||||
.hasMessageThat()
|
||||
.contains(
|
||||
"At google.registry.model.ImmutableObjectSubjectTest$TestImmutableObject.nested:"
|
||||
+ " expected TestImmutableObject");
|
||||
assertThat(e).hasMessageThat().contains("got null.");
|
||||
|
||||
// Test with a field difference.
|
||||
e =
|
||||
assertThrows(
|
||||
AssertionError.class,
|
||||
() ->
|
||||
assertAboutImmutableObjects()
|
||||
.that(makeTestObj())
|
||||
.isEqualAcrossDatabases(
|
||||
makeTestObj().withNested(makeTestObj().withNested(null))));
|
||||
assertThat(e)
|
||||
.hasMessageThat()
|
||||
.contains(
|
||||
"At google.registry.model.ImmutableObjectSubjectTest$"
|
||||
+ "TestImmutableObject.nested.stringField:");
|
||||
assertThat(
|
||||
checkObjectAcrossDatabases(makeTestObj(), makeTestObj().withNested(null), null)
|
||||
.isFailure())
|
||||
.isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testCrossDatabase_listFailure() {
|
||||
AssertionError e =
|
||||
assertThrows(
|
||||
AssertionError.class,
|
||||
() ->
|
||||
assertAboutImmutableObjects()
|
||||
.that(makeTestObj())
|
||||
.isEqualAcrossDatabases(makeTestObj().withList(null)));
|
||||
assertThat(e)
|
||||
.hasMessageThat()
|
||||
.contains(
|
||||
"At google.registry.model.ImmutableObjectSubjectTest$" + "TestImmutableObject.list:");
|
||||
e =
|
||||
assertThrows(
|
||||
AssertionError.class,
|
||||
() ->
|
||||
assertAboutImmutableObjects()
|
||||
.that(makeTestObj())
|
||||
.isEqualAcrossDatabases(
|
||||
makeTestObj().withList(ImmutableList.of(makeTestAtom("wack")))));
|
||||
assertThat(e)
|
||||
.hasMessageThat()
|
||||
.contains(
|
||||
"At google.registry.model.ImmutableObjectSubjectTest$"
|
||||
+ "TestImmutableObject.list[0].stringField:");
|
||||
e =
|
||||
assertThrows(
|
||||
AssertionError.class,
|
||||
() ->
|
||||
assertAboutImmutableObjects()
|
||||
.that(makeTestObj())
|
||||
.isEqualAcrossDatabases(
|
||||
makeTestObj()
|
||||
.withList(
|
||||
ImmutableList.of(
|
||||
makeTestAtom("baz"),
|
||||
makeTestAtom("bot"),
|
||||
makeTestAtom("boq")))));
|
||||
assertThat(e)
|
||||
.hasMessageThat()
|
||||
.contains(
|
||||
"At google.registry.model.ImmutableObjectSubjectTest$"
|
||||
+ "TestImmutableObject.list: missing items");
|
||||
// Make sure multiple additional items get formatted nicely.
|
||||
assertThat(e).hasMessageThat().contains("}, TestImmutableObject");
|
||||
e =
|
||||
assertThrows(
|
||||
AssertionError.class,
|
||||
() ->
|
||||
assertAboutImmutableObjects()
|
||||
.that(makeTestObj())
|
||||
.isEqualAcrossDatabases(makeTestObj().withList(ImmutableList.of())));
|
||||
assertThat(e)
|
||||
.hasMessageThat()
|
||||
.contains(
|
||||
"At google.registry.model.ImmutableObjectSubjectTest$"
|
||||
+ "TestImmutableObject.list: has additional items");
|
||||
assertThat(
|
||||
checkObjectAcrossDatabases(
|
||||
makeTestObj(),
|
||||
makeTestObj()
|
||||
.withList(ImmutableList.of(makeTestAtom("baz"), makeTestAtom("gauze"))),
|
||||
null)
|
||||
.isFailure())
|
||||
.isTrue();
|
||||
assertThat(
|
||||
checkObjectAcrossDatabases(
|
||||
makeTestObj(), makeTestObj().withList(ImmutableList.of()), null)
|
||||
.isFailure())
|
||||
.isTrue();
|
||||
assertThat(
|
||||
checkObjectAcrossDatabases(
|
||||
makeTestObj(),
|
||||
makeTestObj().withList(ImmutableList.of(makeTestAtom("gauze"))),
|
||||
null)
|
||||
.isFailure())
|
||||
.isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testCrossDatabase_setFailure() {
|
||||
AssertionError e =
|
||||
assertThrows(
|
||||
AssertionError.class,
|
||||
() ->
|
||||
assertAboutImmutableObjects()
|
||||
.that(makeTestObj())
|
||||
.isEqualAcrossDatabases(makeTestObj().withSet(null)));
|
||||
assertThat(e)
|
||||
.hasMessageThat()
|
||||
.contains(
|
||||
"At google.registry.model.ImmutableObjectSubjectTest$"
|
||||
+ "TestImmutableObject.set: expected null, got ");
|
||||
|
||||
e =
|
||||
assertThrows(
|
||||
AssertionError.class,
|
||||
() ->
|
||||
assertAboutImmutableObjects()
|
||||
.that(makeTestObj())
|
||||
.isEqualAcrossDatabases(
|
||||
makeTestObj().withSet(ImmutableSet.of(makeTestAtom("jim")))));
|
||||
assertThat(e)
|
||||
.hasMessageThat()
|
||||
.containsMatch(
|
||||
Pattern.compile(
|
||||
"Set does not contain the expected contents. "
|
||||
+ "It is missing: .*jim.* It contains additional elements: .*bob",
|
||||
Pattern.DOTALL));
|
||||
|
||||
// Trickery here to verify that multiple items that both match existing items in the set trigger
|
||||
// an error: we can add two of the same items because equality for purposes of the set includes
|
||||
// the DoNotCompare field.
|
||||
e =
|
||||
assertThrows(
|
||||
AssertionError.class,
|
||||
() ->
|
||||
assertAboutImmutableObjects()
|
||||
.that(makeTestObj())
|
||||
.isEqualAcrossDatabases(
|
||||
makeTestObj()
|
||||
.withSet(ImmutableSet.of(makeTestAtom("bob"), makeTestAtom("bob")))));
|
||||
assertThat(e)
|
||||
.hasMessageThat()
|
||||
.containsMatch(
|
||||
Pattern.compile(
|
||||
"At google.registry.model.ImmutableObjectSubjectTest\\$TestImmutableObject.set: "
|
||||
+ "element .*bob.* matches multiple elements in .*bob.*bob",
|
||||
Pattern.DOTALL));
|
||||
e =
|
||||
assertThrows(
|
||||
AssertionError.class,
|
||||
() ->
|
||||
assertAboutImmutableObjects()
|
||||
.that(
|
||||
makeTestObj()
|
||||
.withSet(ImmutableSet.of(makeTestAtom("bob"), makeTestAtom("bob"))))
|
||||
.isEqualAcrossDatabases(makeTestObj()));
|
||||
assertThat(e)
|
||||
.hasMessageThat()
|
||||
.containsMatch(
|
||||
Pattern.compile(
|
||||
"At google.registry.model.ImmutableObjectSubjectTest\\$TestImmutableObject.set: "
|
||||
+ "Set does not contain the expected contents. It contains additional "
|
||||
+ "elements: .*bob",
|
||||
Pattern.DOTALL));
|
||||
|
||||
assertThat(
|
||||
checkObjectAcrossDatabases(
|
||||
makeTestObj(),
|
||||
makeTestObj()
|
||||
.withSet(ImmutableSet.of(makeTestAtom("bob"), makeTestAtom("robert"))),
|
||||
null)
|
||||
.isFailure())
|
||||
.isTrue();
|
||||
assertThat(
|
||||
checkObjectAcrossDatabases(
|
||||
makeTestObj(), makeTestObj().withSet(ImmutableSet.of()), null)
|
||||
.isFailure())
|
||||
.isTrue();
|
||||
assertThat(
|
||||
checkObjectAcrossDatabases(
|
||||
makeTestObj(),
|
||||
makeTestObj()
|
||||
.withSet(ImmutableSet.of(makeTestAtom("bob"), makeTestAtom("bob"))),
|
||||
null)
|
||||
.isFailure())
|
||||
.isTrue();
|
||||
// We don't test the case where actual's set contains multiple items matching the single item in
|
||||
// the expected set: that path is the same as the "additional contents" path.
|
||||
}
|
||||
|
||||
@Test
|
||||
void testCrossDatabase_mapFailure() {
|
||||
AssertionError e =
|
||||
assertThrows(
|
||||
AssertionError.class,
|
||||
() ->
|
||||
assertAboutImmutableObjects()
|
||||
.that(makeTestObj())
|
||||
.isEqualAcrossDatabases(makeTestObj().withMap(null)));
|
||||
assertThat(e)
|
||||
.hasMessageThat()
|
||||
.contains(
|
||||
"At google.registry.model.ImmutableObjectSubjectTest$"
|
||||
+ "TestImmutableObject.map: expected null, got ");
|
||||
|
||||
e =
|
||||
assertThrows(
|
||||
AssertionError.class,
|
||||
() ->
|
||||
assertAboutImmutableObjects()
|
||||
.that(makeTestObj())
|
||||
.isEqualAcrossDatabases(
|
||||
makeTestObj()
|
||||
.withMap(ImmutableMap.of(makeTestAtom("difk"), makeTestAtom("difv")))));
|
||||
assertThat(e)
|
||||
.hasMessageThat()
|
||||
.containsMatch(
|
||||
Pattern.compile(
|
||||
"Map does not contain the expected contents. "
|
||||
+ "It is missing: .*difk.*difv.* It contains additional elements: .*key.*val",
|
||||
Pattern.DOTALL));
|
||||
assertThat(
|
||||
checkObjectAcrossDatabases(
|
||||
makeTestObj(),
|
||||
makeTestObj()
|
||||
.withMap(
|
||||
ImmutableMap.of(
|
||||
makeTestAtom("key"), makeTestAtom("val"),
|
||||
makeTestAtom("otherk"), makeTestAtom("otherv"))),
|
||||
null)
|
||||
.isFailure())
|
||||
.isTrue();
|
||||
assertThat(
|
||||
checkObjectAcrossDatabases(
|
||||
makeTestObj(), makeTestObj().withMap(ImmutableMap.of()), null)
|
||||
.isFailure())
|
||||
.isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testCrossDatabase_typeChecks() {
|
||||
ComparisonResult result = checkObjectAcrossDatabases("blech", makeTestObj(), "xxx");
|
||||
assertThat(result.getMessage()).isEqualTo("At xxx: blech is not an immutable object.");
|
||||
assertThat(result.isFailure()).isTrue();
|
||||
assertThat(checkObjectAcrossDatabases("blech", makeTestObj(), null).isFailure()).isTrue();
|
||||
|
||||
result = checkObjectAcrossDatabases("blech", ImmutableMap.of(), "xxx");
|
||||
assertThat(result.getMessage()).isEqualTo("At xxx: blech is not a Map.");
|
||||
assertThat(result.isFailure()).isTrue();
|
||||
assertThat(checkObjectAcrossDatabases("blech", ImmutableMap.of(), null).isFailure()).isTrue();
|
||||
|
||||
result = checkObjectAcrossDatabases("blech", ImmutableList.of(), "xxx");
|
||||
assertThat(result.getMessage()).isEqualTo("At xxx: blech is not a Collection.");
|
||||
assertThat(result.isFailure()).isTrue();
|
||||
assertThat(checkObjectAcrossDatabases("blech", ImmutableList.of(), null).isFailure()).isTrue();
|
||||
|
||||
for (ImmutableMap.Entry<String, String> entry : ImmutableMap.of("foo", "bar").entrySet()) {
|
||||
result = checkObjectAcrossDatabases("blech", entry, "xxx");
|
||||
assertThat(result.getMessage()).isEqualTo("At xxx: blech is not a Map.Entry.");
|
||||
assertThat(result.isFailure()).isTrue();
|
||||
assertThat(checkObjectAcrossDatabases("blech", entry, "xxx").isFailure()).isTrue();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void testCrossDatabase_checkAdditionalFields() {
|
||||
AssertionError e =
|
||||
assertThrows(
|
||||
AssertionError.class,
|
||||
() ->
|
||||
assertAboutImmutableObjects()
|
||||
.that(DerivedImmutableObject.create())
|
||||
.isEqualAcrossDatabases(makeTestAtom(null)));
|
||||
assertThat(e)
|
||||
.hasMessageThat()
|
||||
.contains(
|
||||
"At google.registry.model.ImmutableObjectSubjectTest$DerivedImmutableObject: "
|
||||
+ "has additional field extraField");
|
||||
|
||||
assertThat(
|
||||
checkObjectAcrossDatabases(DerivedImmutableObject.create(), makeTestAtom(null), null)
|
||||
.isFailure())
|
||||
.isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testHasCorrectHashValue() {
|
||||
TestImmutableObject object = makeTestObj();
|
||||
assertAboutImmutableObjects().that(object).hasCorrectHashValue();
|
||||
|
||||
object.stringField = "changed value!";
|
||||
assertThrows(
|
||||
AssertionError.class,
|
||||
() -> assertAboutImmutableObjects().that(object).hasCorrectHashValue());
|
||||
}
|
||||
|
||||
/** Make a test object with all fields set up. */
|
||||
TestImmutableObject makeTestObj() {
|
||||
return TestImmutableObject.create(
|
||||
"foo",
|
||||
makeTestAtom("bar"),
|
||||
ImmutableList.of(makeTestAtom("baz")),
|
||||
ImmutableSet.of(makeTestAtom("bob")),
|
||||
ImmutableMap.of(makeTestAtom("key"), makeTestAtom("val")));
|
||||
}
|
||||
|
||||
/** Make a test object without the collection fields. */
|
||||
TestImmutableObject makeTestAtom(String stringField) {
|
||||
return TestImmutableObject.create(stringField, null, null, null, null);
|
||||
}
|
||||
|
||||
static class TestImmutableObject extends ImmutableObject {
|
||||
String stringField;
|
||||
TestImmutableObject nested;
|
||||
ImmutableList<TestImmutableObject> list;
|
||||
ImmutableSet<TestImmutableObject> set;
|
||||
ImmutableMap<TestImmutableObject, TestImmutableObject> map;
|
||||
|
||||
@ImmutableObject.DoNotCompare int ignored;
|
||||
|
||||
static TestImmutableObject create(
|
||||
String stringField,
|
||||
TestImmutableObject nested,
|
||||
ImmutableList<TestImmutableObject> list,
|
||||
ImmutableSet<TestImmutableObject> set,
|
||||
ImmutableMap<TestImmutableObject, TestImmutableObject> map) {
|
||||
TestImmutableObject instance = new TestImmutableObject();
|
||||
instance.stringField = stringField;
|
||||
instance.nested = nested;
|
||||
instance.list = list;
|
||||
instance.set = set;
|
||||
instance.map = map;
|
||||
instance.ignored = ++uniqueId;
|
||||
return instance;
|
||||
}
|
||||
|
||||
TestImmutableObject withStringField(@Nullable String stringField) {
|
||||
TestImmutableObject result = ImmutableObject.clone(this);
|
||||
result.stringField = stringField;
|
||||
return result;
|
||||
}
|
||||
|
||||
TestImmutableObject withNested(@Nullable TestImmutableObject nested) {
|
||||
TestImmutableObject result = ImmutableObject.clone(this);
|
||||
result.nested = nested;
|
||||
return result;
|
||||
}
|
||||
|
||||
TestImmutableObject withList(@Nullable ImmutableList<TestImmutableObject> list) {
|
||||
TestImmutableObject result = ImmutableObject.clone(this);
|
||||
result.list = list;
|
||||
return result;
|
||||
}
|
||||
|
||||
TestImmutableObject withSet(@Nullable ImmutableSet<TestImmutableObject> set) {
|
||||
TestImmutableObject result = ImmutableObject.clone(this);
|
||||
result.set = set;
|
||||
return result;
|
||||
}
|
||||
|
||||
TestImmutableObject withMap(
|
||||
@Nullable ImmutableMap<TestImmutableObject, TestImmutableObject> map) {
|
||||
TestImmutableObject result = ImmutableObject.clone(this);
|
||||
result.map = map;
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
static class DerivedImmutableObject extends TestImmutableObject {
|
||||
String extraField;
|
||||
|
||||
static DerivedImmutableObject create() {
|
||||
return new DerivedImmutableObject();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -71,7 +71,7 @@ public class ContactCommandTest {
|
||||
EppLoader eppLoader = new EppLoader(this, "contact_update.xml");
|
||||
Update command =
|
||||
(Update)
|
||||
((ResourceCommandWrapper) (eppLoader.getEpp().getCommandWrapper().getCommand()))
|
||||
((ResourceCommandWrapper) eppLoader.getEpp().getCommandWrapper().getCommand())
|
||||
.getResourceCommand();
|
||||
Change change = command.getInnerChange();
|
||||
assertThat(change.getInternationalizedPostalInfo().getAddress())
|
||||
|
||||
@@ -221,8 +221,7 @@ public class DomainHistoryTest extends EntityTestCase {
|
||||
.setGracePeriods(
|
||||
ImmutableSet.of(
|
||||
GracePeriod.create(
|
||||
GracePeriodStatus.ADD, "domainRepoId", END_OF_TIME, "clientId", null)
|
||||
.cloneWithPrepopulatedId()))
|
||||
GracePeriodStatus.ADD, "domainRepoId", END_OF_TIME, "clientId", null)))
|
||||
.build();
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,127 @@
|
||||
// Copyright 2020 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.model.reporting;
|
||||
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
import static google.registry.model.ImmutableObjectSubject.immutableObjectCorrespondence;
|
||||
import static google.registry.persistence.transaction.TransactionManagerUtil.transactIfJpaTm;
|
||||
import static google.registry.testing.DatabaseHelper.createTld;
|
||||
import static google.registry.testing.DatabaseHelper.newDomainBase;
|
||||
import static google.registry.testing.DatabaseHelper.persistActiveDomain;
|
||||
import static google.registry.testing.DatabaseHelper.persistResource;
|
||||
import static google.registry.util.DateTimeUtils.END_OF_TIME;
|
||||
import static google.registry.util.DateTimeUtils.START_OF_TIME;
|
||||
import static java.nio.charset.StandardCharsets.UTF_8;
|
||||
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
import google.registry.model.EntityTestCase;
|
||||
import google.registry.model.domain.DomainBase;
|
||||
import google.registry.model.domain.DomainHistory;
|
||||
import google.registry.model.domain.Period;
|
||||
import google.registry.model.eppcommon.Trid;
|
||||
import google.registry.model.reporting.DomainTransactionRecord.TransactionReportField;
|
||||
import google.registry.testing.DualDatabaseTest;
|
||||
import google.registry.testing.TestOfyAndSql;
|
||||
import org.joda.time.DateTime;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
|
||||
@DualDatabaseTest
|
||||
public class HistoryEntryDaoTest extends EntityTestCase {
|
||||
|
||||
private DomainBase domain;
|
||||
private HistoryEntry historyEntry;
|
||||
|
||||
@BeforeEach
|
||||
void beforeEach() {
|
||||
fakeClock.setTo(DateTime.parse("2020-10-01T00:00:00Z"));
|
||||
createTld("foobar");
|
||||
domain = persistActiveDomain("foo.foobar");
|
||||
DomainTransactionRecord transactionRecord =
|
||||
new DomainTransactionRecord.Builder()
|
||||
.setTld("foobar")
|
||||
.setReportingTime(fakeClock.nowUtc())
|
||||
.setReportField(TransactionReportField.NET_ADDS_1_YR)
|
||||
.setReportAmount(1)
|
||||
.build();
|
||||
// Set up a new persisted HistoryEntry entity.
|
||||
historyEntry =
|
||||
new DomainHistory.Builder()
|
||||
.setParent(domain)
|
||||
.setType(HistoryEntry.Type.DOMAIN_CREATE)
|
||||
.setPeriod(Period.create(1, Period.Unit.YEARS))
|
||||
.setXmlBytes("<xml></xml>".getBytes(UTF_8))
|
||||
.setModificationTime(fakeClock.nowUtc())
|
||||
.setClientId("TheRegistrar")
|
||||
.setOtherClientId("otherClient")
|
||||
.setTrid(Trid.create("ABC-123", "server-trid"))
|
||||
.setBySuperuser(false)
|
||||
.setReason("reason")
|
||||
.setRequestedByRegistrar(false)
|
||||
.setDomainTransactionRecords(ImmutableSet.of(transactionRecord))
|
||||
.build();
|
||||
persistResource(historyEntry);
|
||||
}
|
||||
|
||||
@TestOfyAndSql
|
||||
void testSimpleLoadAll() {
|
||||
assertThat(HistoryEntryDao.loadAllHistoryObjects(START_OF_TIME, END_OF_TIME))
|
||||
.comparingElementsUsing(immutableObjectCorrespondence("nsHosts"))
|
||||
.containsExactly(historyEntry);
|
||||
}
|
||||
|
||||
@TestOfyAndSql
|
||||
void testSkips_tooEarly() {
|
||||
assertThat(HistoryEntryDao.loadAllHistoryObjects(fakeClock.nowUtc().plusMillis(1), END_OF_TIME))
|
||||
.isEmpty();
|
||||
}
|
||||
|
||||
@TestOfyAndSql
|
||||
void testSkips_tooLate() {
|
||||
assertThat(
|
||||
HistoryEntryDao.loadAllHistoryObjects(START_OF_TIME, fakeClock.nowUtc().minusMillis(1)))
|
||||
.isEmpty();
|
||||
}
|
||||
|
||||
@TestOfyAndSql
|
||||
void testLoadByResource() {
|
||||
transactIfJpaTm(
|
||||
() ->
|
||||
assertThat(HistoryEntryDao.loadHistoryObjectsForResource(domain.createVKey()))
|
||||
.comparingElementsUsing(immutableObjectCorrespondence("nsHosts"))
|
||||
.containsExactly(historyEntry));
|
||||
}
|
||||
|
||||
@TestOfyAndSql
|
||||
void testLoadByResource_skips_tooEarly() {
|
||||
assertThat(
|
||||
HistoryEntryDao.loadHistoryObjectsForResource(
|
||||
domain.createVKey(), fakeClock.nowUtc().plusMillis(1), END_OF_TIME))
|
||||
.isEmpty();
|
||||
}
|
||||
|
||||
@TestOfyAndSql
|
||||
void testLoadByResource_skips_tooLate() {
|
||||
assertThat(
|
||||
HistoryEntryDao.loadHistoryObjectsForResource(
|
||||
domain.createVKey(), START_OF_TIME, fakeClock.nowUtc().minusMillis(1)))
|
||||
.isEmpty();
|
||||
}
|
||||
|
||||
@TestOfyAndSql
|
||||
void testLoadByResource_noEntriesForResource() {
|
||||
DomainBase newDomain = persistResource(newDomainBase("new.foobar"));
|
||||
assertThat(HistoryEntryDao.loadHistoryObjectsForResource(newDomain.createVKey())).isEmpty();
|
||||
}
|
||||
}
|
||||
@@ -14,22 +14,29 @@
|
||||
|
||||
package google.registry.model.reporting;
|
||||
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
import static google.registry.model.ofy.ObjectifyService.ofy;
|
||||
import static google.registry.model.ImmutableObjectSubject.assertAboutImmutableObjects;
|
||||
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
|
||||
import static google.registry.persistence.transaction.TransactionManagerUtil.transactIfJpaTm;
|
||||
import static google.registry.testing.DatabaseHelper.createTld;
|
||||
import static google.registry.testing.DatabaseHelper.newDomainBase;
|
||||
import static google.registry.testing.DatabaseHelper.persistActiveDomain;
|
||||
import static google.registry.testing.DatabaseHelper.persistResource;
|
||||
import static java.nio.charset.StandardCharsets.UTF_8;
|
||||
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
import com.google.common.collect.Iterables;
|
||||
import google.registry.model.EntityTestCase;
|
||||
import google.registry.model.domain.DomainBase;
|
||||
import google.registry.model.domain.DomainHistory;
|
||||
import google.registry.model.domain.Period;
|
||||
import google.registry.model.eppcommon.Trid;
|
||||
import google.registry.model.reporting.DomainTransactionRecord.TransactionReportField;
|
||||
import google.registry.testing.DualDatabaseTest;
|
||||
import google.registry.testing.TestOfyAndSql;
|
||||
import google.registry.testing.TestOfyOnly;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
/** Unit tests for {@link HistoryEntry}. */
|
||||
@DualDatabaseTest
|
||||
class HistoryEntryTest extends EntityTestCase {
|
||||
|
||||
private HistoryEntry historyEntry;
|
||||
@@ -37,6 +44,7 @@ class HistoryEntryTest extends EntityTestCase {
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
createTld("foobar");
|
||||
DomainBase domain = persistActiveDomain("foo.foobar");
|
||||
DomainTransactionRecord transactionRecord =
|
||||
new DomainTransactionRecord.Builder()
|
||||
.setTld("foobar")
|
||||
@@ -46,13 +54,13 @@ class HistoryEntryTest extends EntityTestCase {
|
||||
.build();
|
||||
// Set up a new persisted HistoryEntry entity.
|
||||
historyEntry =
|
||||
new HistoryEntry.Builder()
|
||||
.setParent(newDomainBase("foo.foobar"))
|
||||
new DomainHistory.Builder()
|
||||
.setParent(domain)
|
||||
.setType(HistoryEntry.Type.DOMAIN_CREATE)
|
||||
.setPeriod(Period.create(1, Period.Unit.YEARS))
|
||||
.setXmlBytes("<xml></xml>".getBytes(UTF_8))
|
||||
.setModificationTime(fakeClock.nowUtc())
|
||||
.setClientId("foo")
|
||||
.setClientId("TheRegistrar")
|
||||
.setOtherClientId("otherClient")
|
||||
.setTrid(Trid.create("ABC-123", "server-trid"))
|
||||
.setBySuperuser(false)
|
||||
@@ -63,13 +71,23 @@ class HistoryEntryTest extends EntityTestCase {
|
||||
persistResource(historyEntry);
|
||||
}
|
||||
|
||||
@Test
|
||||
@TestOfyAndSql
|
||||
void testPersistence() {
|
||||
assertThat(ofy().load().entity(historyEntry).now()).isEqualTo(historyEntry);
|
||||
transactIfJpaTm(
|
||||
() -> {
|
||||
HistoryEntry fromDatabase = tm().loadByEntity(historyEntry);
|
||||
assertAboutImmutableObjects()
|
||||
.that(fromDatabase)
|
||||
.isEqualExceptFields(historyEntry, "nsHosts", "domainTransactionRecords");
|
||||
assertAboutImmutableObjects()
|
||||
.that(Iterables.getOnlyElement(fromDatabase.getDomainTransactionRecords()))
|
||||
.isEqualExceptFields(
|
||||
Iterables.getOnlyElement(historyEntry.getDomainTransactionRecords()), "id");
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
@TestOfyOnly
|
||||
void testIndexing() throws Exception {
|
||||
verifyIndexing(historyEntry, "modificationTime", "clientId");
|
||||
verifyIndexing(historyEntry.asHistoryEntry(), "modificationTime", "clientId");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -54,28 +54,28 @@ public class TransferDataTest {
|
||||
transferBillingEventKey =
|
||||
VKey.create(
|
||||
BillingEvent.OneTime.class,
|
||||
12345,
|
||||
Key.create(historyEntryKey, BillingEvent.OneTime.class, 12345));
|
||||
12345L,
|
||||
Key.create(historyEntryKey, BillingEvent.OneTime.class, 12345L));
|
||||
otherServerApproveBillingEventKey =
|
||||
VKey.create(
|
||||
BillingEvent.Cancellation.class,
|
||||
2468,
|
||||
Key.create(historyEntryKey, BillingEvent.Cancellation.class, 2468));
|
||||
2468L,
|
||||
Key.create(historyEntryKey, BillingEvent.Cancellation.class, 2468L));
|
||||
recurringBillingEventKey =
|
||||
VKey.create(
|
||||
BillingEvent.Recurring.class,
|
||||
13579,
|
||||
Key.create(historyEntryKey, BillingEvent.Recurring.class, 13579));
|
||||
13579L,
|
||||
Key.create(historyEntryKey, BillingEvent.Recurring.class, 13579L));
|
||||
autorenewPollMessageKey =
|
||||
VKey.create(
|
||||
PollMessage.Autorenew.class,
|
||||
67890,
|
||||
Key.create(historyEntryKey, PollMessage.Autorenew.class, 67890));
|
||||
67890L,
|
||||
Key.create(historyEntryKey, PollMessage.Autorenew.class, 67890L));
|
||||
otherServerApprovePollMessageKey =
|
||||
VKey.create(
|
||||
PollMessage.OneTime.class,
|
||||
314159,
|
||||
Key.create(historyEntryKey, PollMessage.OneTime.class, 314159));
|
||||
314159L,
|
||||
Key.create(historyEntryKey, PollMessage.OneTime.class, 314159L));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
+2
-2
@@ -48,8 +48,8 @@ import org.junit.jupiter.api.extension.RegisterExtension;
|
||||
* Unit tests for SQL only APIs defined in {@link JpaTransactionManagerImpl}. Note that the tests
|
||||
* for common APIs in {@link TransactionManager} are added in {@link TransactionManagerTest}.
|
||||
*
|
||||
* <p>TODO(shicong): Remove duplicate tests that covered by TransactionManagerTest by refactoring
|
||||
* the test schema.
|
||||
* <p>TODO(b/177587763): Remove duplicate tests that covered by TransactionManagerTest by
|
||||
* refactoring the test schema.
|
||||
*/
|
||||
class JpaTransactionManagerImplTest {
|
||||
|
||||
|
||||
@@ -95,7 +95,7 @@ class RdapDomainActionTest extends RdapActionBaseTestCase<RdapDomainAction> {
|
||||
registrarLol)
|
||||
.asBuilder()
|
||||
.setCreationTimeForTest(clock.nowUtc().minusYears(3))
|
||||
.setCreationClientId("foo")
|
||||
.setCreationClientId("TheRegistrar")
|
||||
.build());
|
||||
|
||||
// deleted domain in lol
|
||||
@@ -128,7 +128,7 @@ class RdapDomainActionTest extends RdapActionBaseTestCase<RdapDomainAction> {
|
||||
registrarLol)
|
||||
.asBuilder()
|
||||
.setCreationTimeForTest(clock.nowUtc().minusYears(3))
|
||||
.setCreationClientId("foo")
|
||||
.setCreationClientId("TheRegistrar")
|
||||
.setDeletionTime(clock.nowUtc().minusDays(1))
|
||||
.build());
|
||||
// cat.みんな
|
||||
@@ -168,7 +168,7 @@ class RdapDomainActionTest extends RdapActionBaseTestCase<RdapDomainAction> {
|
||||
registrarIdn)
|
||||
.asBuilder()
|
||||
.setCreationTimeForTest(clock.nowUtc().minusYears(3))
|
||||
.setCreationClientId("foo")
|
||||
.setCreationClientId("TheRegistrar")
|
||||
.build());
|
||||
|
||||
// 1.tld
|
||||
@@ -208,7 +208,7 @@ class RdapDomainActionTest extends RdapActionBaseTestCase<RdapDomainAction> {
|
||||
registrar1Tld)
|
||||
.asBuilder()
|
||||
.setCreationTimeForTest(clock.nowUtc().minusYears(3))
|
||||
.setCreationClientId("foo")
|
||||
.setCreationClientId("TheRegistrar")
|
||||
.build());
|
||||
|
||||
// history entries
|
||||
|
||||
@@ -176,7 +176,7 @@ class RdapDomainSearchActionTest extends RdapSearchActionTestCase<RdapDomainSear
|
||||
.asBuilder()
|
||||
.setSubordinateHosts(ImmutableSet.of("ns1.cat.lol", "ns2.cat.lol"))
|
||||
.setCreationTimeForTest(clock.nowUtc().minusYears(3))
|
||||
.setCreationClientId("foo")
|
||||
.setCreationClientId("TheRegistrar")
|
||||
.build());
|
||||
persistResource(
|
||||
hostNs1CatLol.asBuilder().setSuperordinateDomain(domainCatLol.createVKey()).build());
|
||||
@@ -216,7 +216,7 @@ class RdapDomainSearchActionTest extends RdapSearchActionTestCase<RdapDomainSear
|
||||
registrar)
|
||||
.asBuilder()
|
||||
.setCreationTimeForTest(clock.nowUtc().minusYears(3))
|
||||
.setCreationClientId("foo")
|
||||
.setCreationClientId("TheRegistrar")
|
||||
.build());
|
||||
// cat.example
|
||||
createTld("example");
|
||||
@@ -255,7 +255,7 @@ class RdapDomainSearchActionTest extends RdapSearchActionTestCase<RdapDomainSear
|
||||
registrar)
|
||||
.asBuilder()
|
||||
.setCreationTimeForTest(clock.nowUtc().minusYears(3))
|
||||
.setCreationClientId("foo")
|
||||
.setCreationClientId("TheRegistrar")
|
||||
.build());
|
||||
// cat.みんな
|
||||
createTld("xn--q9jyb4c");
|
||||
@@ -294,7 +294,7 @@ class RdapDomainSearchActionTest extends RdapSearchActionTestCase<RdapDomainSear
|
||||
registrar)
|
||||
.asBuilder()
|
||||
.setCreationTimeForTest(clock.nowUtc().minusYears(3))
|
||||
.setCreationClientId("foo")
|
||||
.setCreationClientId("TheRegistrar")
|
||||
.build());
|
||||
// cat.1.test
|
||||
createTld("1.test");
|
||||
@@ -334,7 +334,7 @@ class RdapDomainSearchActionTest extends RdapSearchActionTestCase<RdapDomainSear
|
||||
.asBuilder()
|
||||
.setSubordinateHosts(ImmutableSet.of("ns1.cat.1.test"))
|
||||
.setCreationTimeForTest(clock.nowUtc().minusYears(3))
|
||||
.setCreationClientId("foo")
|
||||
.setCreationClientId("TheRegistrar")
|
||||
.build());
|
||||
|
||||
persistResource(makeRegistrar("otherregistrar", "other", Registrar.State.ACTIVE));
|
||||
@@ -430,7 +430,7 @@ class RdapDomainSearchActionTest extends RdapSearchActionTestCase<RdapDomainSear
|
||||
.asBuilder()
|
||||
.setNameservers(hostKeys)
|
||||
.setCreationTimeForTest(clock.nowUtc().minusYears(3))
|
||||
.setCreationClientId("foo");
|
||||
.setCreationClientId("TheRegistrar");
|
||||
if (domainName.equals(mainDomainName)) {
|
||||
builder.setSubordinateHosts(subordinateHostnamesBuilder.build());
|
||||
}
|
||||
|
||||
@@ -191,7 +191,6 @@ class RdapJsonFormatterTest {
|
||||
hostResourceIpv6,
|
||||
registrar)
|
||||
.asBuilder()
|
||||
.setCreationClientId("foo")
|
||||
.setCreationTimeForTest(clock.nowUtc().minusMonths(4))
|
||||
.setLastEppUpdateTime(clock.nowUtc().minusMonths(3))
|
||||
.build());
|
||||
@@ -206,7 +205,6 @@ class RdapJsonFormatterTest {
|
||||
null,
|
||||
registrar)
|
||||
.asBuilder()
|
||||
.setCreationClientId("foo")
|
||||
.setCreationTimeForTest(clock.nowUtc())
|
||||
.setLastEppUpdateTime(null)
|
||||
.build());
|
||||
|
||||
@@ -313,7 +313,7 @@ class RdapTestHelper {
|
||||
}
|
||||
builder.append(
|
||||
String.format(
|
||||
"Different: %s -> %s\ninstead of %s\n\n",
|
||||
"Actual: %s -> %s\nExpected: %s\n\n",
|
||||
name, jsonifyAndIndent(actual), jsonifyAndIndent(expected)));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -109,6 +109,7 @@ import google.registry.model.transfer.TransferStatus;
|
||||
import google.registry.persistence.VKey;
|
||||
import google.registry.tmch.LordnTaskUtils;
|
||||
import java.util.Arrays;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
@@ -116,7 +117,6 @@ import javax.annotation.Nullable;
|
||||
import org.joda.money.CurrencyUnit;
|
||||
import org.joda.money.Money;
|
||||
import org.joda.time.DateTime;
|
||||
import org.joda.time.DateTimeComparator;
|
||||
import org.joda.time.DateTimeZone;
|
||||
|
||||
/** Static utils for setting up test resources. */
|
||||
@@ -532,11 +532,14 @@ public class DatabaseHelper {
|
||||
DateTime requestTime,
|
||||
DateTime expirationTime,
|
||||
DateTime now) {
|
||||
HistoryEntry historyEntryContactTransfer = persistResource(
|
||||
new HistoryEntry.Builder()
|
||||
.setType(HistoryEntry.Type.CONTACT_TRANSFER_REQUEST)
|
||||
.setParent(contact)
|
||||
.build());
|
||||
HistoryEntry historyEntryContactTransfer =
|
||||
persistResource(
|
||||
new HistoryEntry.Builder()
|
||||
.setType(HistoryEntry.Type.CONTACT_TRANSFER_REQUEST)
|
||||
.setParent(persistResource(contact))
|
||||
.setModificationTime(now)
|
||||
.build()
|
||||
.toChildHistoryEntity());
|
||||
return persistResource(
|
||||
contact
|
||||
.asBuilder()
|
||||
@@ -1115,7 +1118,8 @@ public class DatabaseHelper {
|
||||
historyEntry ->
|
||||
historyEntry.getParent().getName().equals(resource.getRepoId()))
|
||||
.collect(toImmutableList());
|
||||
return ImmutableList.sortedCopyOf(DateTimeComparator.getInstance(), filtered);
|
||||
return ImmutableList.sortedCopyOf(
|
||||
Comparator.comparing(HistoryEntry::getModificationTime), filtered);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -392,17 +392,18 @@ public final class FullFieldsTestEntityHelper {
|
||||
Period period,
|
||||
String reason,
|
||||
DateTime modificationTime) {
|
||||
HistoryEntry.Builder builder = new HistoryEntry.Builder()
|
||||
.setParent(resource)
|
||||
.setType(type)
|
||||
.setPeriod(period)
|
||||
.setXmlBytes("<xml></xml>".getBytes(UTF_8))
|
||||
.setModificationTime(modificationTime)
|
||||
.setClientId("foo")
|
||||
.setTrid(Trid.create("ABC-123", "server-trid"))
|
||||
.setBySuperuser(false)
|
||||
.setReason(reason)
|
||||
.setRequestedByRegistrar(false);
|
||||
HistoryEntry.Builder builder =
|
||||
new HistoryEntry.Builder()
|
||||
.setParent(resource)
|
||||
.setType(type)
|
||||
.setPeriod(period)
|
||||
.setXmlBytes("<xml></xml>".getBytes(UTF_8))
|
||||
.setModificationTime(modificationTime)
|
||||
.setClientId(resource.getPersistedCurrentSponsorClientId())
|
||||
.setTrid(Trid.create("ABC-123", "server-trid"))
|
||||
.setBySuperuser(false)
|
||||
.setReason(reason)
|
||||
.setRequestedByRegistrar(false);
|
||||
return builder.build();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,7 +14,19 @@
|
||||
|
||||
package google.registry.testing;
|
||||
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
import static google.registry.model.ImmutableObjectSubject.assertAboutImmutableObjects;
|
||||
import static google.registry.persistence.transaction.TransactionManagerFactory.jpaTm;
|
||||
|
||||
import com.google.common.collect.ImmutableMap;
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
import com.googlecode.objectify.Key;
|
||||
import google.registry.model.ImmutableObject;
|
||||
import google.registry.model.ofy.ReplayQueue;
|
||||
import google.registry.model.ofy.TransactionInfo;
|
||||
import google.registry.persistence.VKey;
|
||||
import google.registry.schema.replay.DatastoreEntity;
|
||||
import java.util.Optional;
|
||||
import org.junit.jupiter.api.extension.AfterEachCallback;
|
||||
import org.junit.jupiter.api.extension.BeforeEachCallback;
|
||||
import org.junit.jupiter.api.extension.ExtensionContext;
|
||||
@@ -26,13 +38,26 @@ import org.junit.jupiter.api.extension.ExtensionContext;
|
||||
* that extension are also replayed. If AppEngineExtension is not used,
|
||||
* JpaTransactionManagerExtension must be, and this extension should be ordered _after_
|
||||
* JpaTransactionManagerExtension so that writes to SQL work.
|
||||
*
|
||||
* <p>If the "compare" flag is set in the constructor, this will also compare all touched objects in
|
||||
* both databases after performing the replay.
|
||||
*/
|
||||
public class ReplayExtension implements BeforeEachCallback, AfterEachCallback {
|
||||
|
||||
FakeClock clock;
|
||||
boolean compare;
|
||||
|
||||
public ReplayExtension(FakeClock clock) {
|
||||
private ReplayExtension(FakeClock clock, boolean compare) {
|
||||
this.clock = clock;
|
||||
this.compare = compare;
|
||||
}
|
||||
|
||||
public static ReplayExtension createWithCompare(FakeClock clock) {
|
||||
return new ReplayExtension(clock, true);
|
||||
}
|
||||
|
||||
public static ReplayExtension createWithoutCompare(FakeClock clock) {
|
||||
return new ReplayExtension(clock, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -51,8 +76,52 @@ public class ReplayExtension implements BeforeEachCallback, AfterEachCallback {
|
||||
replayToSql();
|
||||
}
|
||||
|
||||
private static ImmutableSet<String> NON_REPLICATED_TYPES =
|
||||
ImmutableSet.of(
|
||||
"PremiumList",
|
||||
"PremiumListRevision",
|
||||
"PremiumListEntry",
|
||||
"ReservedList",
|
||||
"RdeRevision",
|
||||
"KmsSecretRevision",
|
||||
"ServerSecret",
|
||||
"SignedMarkRevocationList",
|
||||
"ClaimsListShard",
|
||||
"TmchCrl",
|
||||
"EppResourceIndex",
|
||||
"ForeignKeyIndex",
|
||||
"ForeignKeyHostIndex",
|
||||
"ForeignKeyContactIndex",
|
||||
"ForeignKeyDomainIndex");
|
||||
|
||||
public void replayToSql() {
|
||||
DatabaseHelper.setAlwaysSaveWithBackup(false);
|
||||
ReplayQueue.replay();
|
||||
ImmutableMap<Key<?>, Object> changes = ReplayQueue.replay();
|
||||
|
||||
// Compare JPA to OFY, if requested.
|
||||
if (compare) {
|
||||
for (ImmutableMap.Entry<Key<?>, Object> entry : changes.entrySet()) {
|
||||
// Don't verify non-replicated types.
|
||||
if (NON_REPLICATED_TYPES.contains(entry.getKey().getKind())) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Since the object may have changed in datastore by the time we're doing the replay, we
|
||||
// have to compare the current value in SQL (which we just mutated) against the value that
|
||||
// we originally would have persisted (that being the object in the entry).
|
||||
VKey<?> vkey = VKey.from(entry.getKey());
|
||||
Optional<?> jpaValue = jpaTm().transact(() -> jpaTm().loadByKeyIfPresent(vkey));
|
||||
if (entry.getValue().equals(TransactionInfo.Delete.SENTINEL)) {
|
||||
assertThat(jpaValue.isPresent()).isFalse();
|
||||
} else {
|
||||
ImmutableObject immutJpaObject = (ImmutableObject) jpaValue.get();
|
||||
assertAboutImmutableObjects().that(immutJpaObject).hasCorrectHashValue();
|
||||
assertAboutImmutableObjects()
|
||||
.that(immutJpaObject)
|
||||
.isEqualAcrossDatabases(
|
||||
(ImmutableObject) ((DatastoreEntity) entry.getValue()).toSqlEntity().get());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -53,7 +53,7 @@ class TmchCertificateAuthorityTest {
|
||||
CertificateExpiredException e =
|
||||
assertThrows(
|
||||
CertificateExpiredException.class, tmchCertificateAuthority::getAndValidateRoot);
|
||||
assertThat(e).hasMessageThat().containsMatch("NotAfter: Sun Jul 23 23:59:59 UTC 2023");
|
||||
assertThat(e).hasMessageThat().contains("NotAfter: Sun Jul 23 23:59:59 UTC 2023");
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -64,7 +64,7 @@ class TmchCertificateAuthorityTest {
|
||||
CertificateNotYetValidException e =
|
||||
assertThrows(
|
||||
CertificateNotYetValidException.class, tmchCertificateAuthority::getAndValidateRoot);
|
||||
assertThat(e).hasMessageThat().containsMatch("NotBefore: Wed Jul 24 00:00:00 UTC 2013");
|
||||
assertThat(e).hasMessageThat().contains("NotBefore: Wed Jul 24 00:00:00 UTC 2013");
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@@ -61,7 +61,7 @@ public class CompareDbBackupsTest {
|
||||
URL backupRootFolder = Resources.getResource("google/registry/tools/datastore-export");
|
||||
CompareDbBackups.main(new String[] {backupRootFolder.getPath(), backupRootFolder.getPath()});
|
||||
String output = new String(stdout.toByteArray(), UTF_8);
|
||||
assertThat(output).containsMatch("Both sets have the same 41 entities");
|
||||
assertThat(output).contains("Both sets have the same 41 entities");
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@@ -38,6 +38,7 @@ import com.google.common.collect.ImmutableSortedMap;
|
||||
import com.google.common.collect.Range;
|
||||
import com.google.common.net.MediaType;
|
||||
import google.registry.flows.certs.CertificateChecker;
|
||||
import google.registry.flows.certs.CertificateChecker.InsecureCertificateException;
|
||||
import google.registry.model.registrar.Registrar;
|
||||
import java.io.IOException;
|
||||
import java.util.Optional;
|
||||
@@ -389,9 +390,9 @@ class CreateRegistrarCommandTest extends CommandTestCase<CreateRegistrarCommand>
|
||||
@Test
|
||||
void testFail_clientCertFileFlagWithViolation() throws Exception {
|
||||
fakeClock.setTo(DateTime.parse("2020-10-01T00:00:00Z"));
|
||||
IllegalArgumentException thrown =
|
||||
InsecureCertificateException thrown =
|
||||
assertThrows(
|
||||
IllegalArgumentException.class,
|
||||
InsecureCertificateException.class,
|
||||
() ->
|
||||
runCommandForced(
|
||||
"--name=blobio",
|
||||
@@ -419,9 +420,9 @@ class CreateRegistrarCommandTest extends CommandTestCase<CreateRegistrarCommand>
|
||||
@Test
|
||||
void testFail_clientCertFileFlagWithMultipleViolations() throws Exception {
|
||||
fakeClock.setTo(DateTime.parse("2055-10-01T00:00:00Z"));
|
||||
IllegalArgumentException thrown =
|
||||
InsecureCertificateException thrown =
|
||||
assertThrows(
|
||||
IllegalArgumentException.class,
|
||||
InsecureCertificateException.class,
|
||||
() ->
|
||||
runCommandForced(
|
||||
"--name=blobio",
|
||||
@@ -476,9 +477,9 @@ class CreateRegistrarCommandTest extends CommandTestCase<CreateRegistrarCommand>
|
||||
@Test
|
||||
void testFail_failoverClientCertFileFlagWithViolations() throws Exception {
|
||||
fakeClock.setTo(DateTime.parse("2020-11-01T00:00:00Z"));
|
||||
IllegalArgumentException thrown =
|
||||
InsecureCertificateException thrown =
|
||||
assertThrows(
|
||||
IllegalArgumentException.class,
|
||||
InsecureCertificateException.class,
|
||||
() ->
|
||||
runCommandForced(
|
||||
"--name=blobio",
|
||||
@@ -506,9 +507,9 @@ class CreateRegistrarCommandTest extends CommandTestCase<CreateRegistrarCommand>
|
||||
@Test
|
||||
void testFail_failoverClientCertFileFlagWithMultipleViolations() throws Exception {
|
||||
fakeClock.setTo(DateTime.parse("2055-11-01T00:00:00Z"));
|
||||
IllegalArgumentException thrown =
|
||||
InsecureCertificateException thrown =
|
||||
assertThrows(
|
||||
IllegalArgumentException.class,
|
||||
InsecureCertificateException.class,
|
||||
() ->
|
||||
runCommandForced(
|
||||
"--name=blobio",
|
||||
|
||||
@@ -22,12 +22,14 @@ import static google.registry.testing.FullFieldsTestEntityHelper.makeHistoryEntr
|
||||
import google.registry.model.domain.DomainBase;
|
||||
import google.registry.model.domain.Period;
|
||||
import google.registry.model.reporting.HistoryEntry;
|
||||
import google.registry.testing.DualDatabaseTest;
|
||||
import google.registry.testing.FakeClock;
|
||||
import google.registry.testing.TestOfyAndSql;
|
||||
import org.joda.time.DateTime;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
/** Unit tests for {@link GetClaimsListCommand}. */
|
||||
@DualDatabaseTest
|
||||
class GetHistoryEntriesCommandTest extends CommandTestCase<GetHistoryEntriesCommand> {
|
||||
|
||||
private final FakeClock clock = new FakeClock(DateTime.parse("2000-01-01T00:00:00Z"));
|
||||
@@ -40,7 +42,7 @@ class GetHistoryEntriesCommandTest extends CommandTestCase<GetHistoryEntriesComm
|
||||
domain = persistActiveDomain("example.tld");
|
||||
}
|
||||
|
||||
@Test
|
||||
@TestOfyAndSql
|
||||
void testSuccess_works() throws Exception {
|
||||
persistResource(
|
||||
makeHistoryEntry(
|
||||
@@ -51,7 +53,7 @@ class GetHistoryEntriesCommandTest extends CommandTestCase<GetHistoryEntriesComm
|
||||
clock.nowUtc()));
|
||||
runCommand("--id=example.tld", "--type=DOMAIN");
|
||||
assertStdoutIs(
|
||||
"Client: foo\n"
|
||||
"Client: TheRegistrar\n"
|
||||
+ "Time: 2000-01-01T00:00:00.000Z\n"
|
||||
+ "Client TRID: ABC-123\n"
|
||||
+ "Server TRID: server-trid\n"
|
||||
@@ -60,7 +62,57 @@ class GetHistoryEntriesCommandTest extends CommandTestCase<GetHistoryEntriesComm
|
||||
+ "\n");
|
||||
}
|
||||
|
||||
@Test
|
||||
@TestOfyAndSql
|
||||
void testSuccess_nothingBefore() throws Exception {
|
||||
persistResource(
|
||||
makeHistoryEntry(
|
||||
domain,
|
||||
HistoryEntry.Type.DOMAIN_CREATE,
|
||||
Period.create(1, Period.Unit.YEARS),
|
||||
"created",
|
||||
clock.nowUtc()));
|
||||
runCommand("--before", clock.nowUtc().minusMinutes(1).toString());
|
||||
assertStdoutIs("");
|
||||
}
|
||||
|
||||
@TestOfyAndSql
|
||||
void testSuccess_nothingAfter() throws Exception {
|
||||
persistResource(
|
||||
makeHistoryEntry(
|
||||
domain,
|
||||
HistoryEntry.Type.DOMAIN_CREATE,
|
||||
Period.create(1, Period.Unit.YEARS),
|
||||
"created",
|
||||
clock.nowUtc()));
|
||||
runCommand("--after", clock.nowUtc().plusMinutes(1).toString());
|
||||
assertStdoutIs("");
|
||||
}
|
||||
|
||||
@TestOfyAndSql
|
||||
void testSuccess_withinRange() throws Exception {
|
||||
persistResource(
|
||||
makeHistoryEntry(
|
||||
domain,
|
||||
HistoryEntry.Type.DOMAIN_CREATE,
|
||||
Period.create(1, Period.Unit.YEARS),
|
||||
"created",
|
||||
clock.nowUtc()));
|
||||
runCommand(
|
||||
"--after",
|
||||
clock.nowUtc().minusMinutes(1).toString(),
|
||||
"--before",
|
||||
clock.nowUtc().plusMinutes(1).toString());
|
||||
assertStdoutIs(
|
||||
"Client: TheRegistrar\n"
|
||||
+ "Time: 2000-01-01T00:00:00.000Z\n"
|
||||
+ "Client TRID: ABC-123\n"
|
||||
+ "Server TRID: server-trid\n"
|
||||
+ "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>\n"
|
||||
+ "<xml/>\n"
|
||||
+ "\n");
|
||||
}
|
||||
|
||||
@TestOfyAndSql
|
||||
void testSuccess_noTrid() throws Exception {
|
||||
persistResource(
|
||||
makeHistoryEntry(
|
||||
@@ -74,7 +126,7 @@ class GetHistoryEntriesCommandTest extends CommandTestCase<GetHistoryEntriesComm
|
||||
.build());
|
||||
runCommand("--id=example.tld", "--type=DOMAIN");
|
||||
assertStdoutIs(
|
||||
"Client: foo\n"
|
||||
"Client: TheRegistrar\n"
|
||||
+ "Time: 2000-01-01T00:00:00.000Z\n"
|
||||
+ "Client TRID: null\n"
|
||||
+ "Server TRID: null\n"
|
||||
|
||||
@@ -16,6 +16,7 @@ package google.registry.tools;
|
||||
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
import static google.registry.model.domain.rgp.GracePeriodStatus.AUTO_RENEW;
|
||||
import static google.registry.model.eppcommon.StatusValue.PENDING_DELETE;
|
||||
import static google.registry.model.eppcommon.StatusValue.SERVER_UPDATE_PROHIBITED;
|
||||
import static google.registry.model.reporting.HistoryEntry.Type.DOMAIN_CREATE;
|
||||
import static google.registry.testing.DatabaseHelper.createTld;
|
||||
@@ -334,6 +335,39 @@ class UpdateDomainCommandTest extends EppToolCommandTestCase<UpdateDomainCommand
|
||||
assertThat(stdErr).contains("example.tld");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSuccess_canUpdatePendingDeleteDomain_whenSuperuserPassesOverrideFlag() throws Exception {
|
||||
ContactResource adminContact1 = persistResource(newContactResource("crr-admin1"));
|
||||
ContactResource adminContact2 = persistResource(newContactResource("crr-admin2"));
|
||||
ContactResource techContact1 = persistResource(newContactResource("crr-tech1"));
|
||||
ContactResource techContact2 = persistResource(newContactResource("crr-tech2"));
|
||||
VKey<ContactResource> adminResourceKey1 = adminContact1.createVKey();
|
||||
VKey<ContactResource> adminResourceKey2 = adminContact2.createVKey();
|
||||
VKey<ContactResource> techResourceKey1 = techContact1.createVKey();
|
||||
VKey<ContactResource> techResourceKey2 = techContact2.createVKey();
|
||||
|
||||
persistResource(
|
||||
newDomainBase("example.tld")
|
||||
.asBuilder()
|
||||
.setContacts(
|
||||
ImmutableSet.of(
|
||||
DesignatedContact.create(DesignatedContact.Type.ADMIN, adminResourceKey1),
|
||||
DesignatedContact.create(DesignatedContact.Type.ADMIN, adminResourceKey2),
|
||||
DesignatedContact.create(DesignatedContact.Type.TECH, techResourceKey1),
|
||||
DesignatedContact.create(DesignatedContact.Type.TECH, techResourceKey2)))
|
||||
.setStatusValues(ImmutableSet.of(PENDING_DELETE))
|
||||
.build());
|
||||
|
||||
runCommandForced(
|
||||
"--client=NewRegistrar",
|
||||
"--admins=crr-admin2,crr-admin3",
|
||||
"--techs=crr-tech2,crr-tech3",
|
||||
"--superuser",
|
||||
"--force_in_pending_delete",
|
||||
"example.tld");
|
||||
eppVerifier.expectSuperuser().verifySent("domain_update_set_contacts.xml");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testFailure_cantUpdateRegistryLockedDomainEvenAsSuperuser() {
|
||||
HostResource host = persistActiveHost("ns1.zdns.google");
|
||||
@@ -356,7 +390,30 @@ class UpdateDomainCommandTest extends EppToolCommandTestCase<UpdateDomainCommand
|
||||
"example.tld"));
|
||||
assertThat(e)
|
||||
.hasMessageThat()
|
||||
.containsMatch("The domain 'example.tld' has status SERVER_UPDATE_PROHIBITED");
|
||||
.contains("The domain 'example.tld' has status SERVER_UPDATE_PROHIBITED.");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testFailure_cantUpdatePendingDeleteDomainEvenAsSuperuser_withoutPassingOverrideFlag() {
|
||||
HostResource host = persistActiveHost("ns1.zdns.google");
|
||||
ImmutableSet<VKey<HostResource>> nameservers = ImmutableSet.of(host.createVKey());
|
||||
persistResource(
|
||||
newDomainBase("example.tld")
|
||||
.asBuilder()
|
||||
.setStatusValues(ImmutableSet.of(PENDING_DELETE))
|
||||
.setNameservers(nameservers)
|
||||
.build());
|
||||
|
||||
Exception e =
|
||||
assertThrows(
|
||||
IllegalArgumentException.class,
|
||||
() ->
|
||||
runCommandForced(
|
||||
"--client=NewRegistrar",
|
||||
"--statuses=clientRenewProhibited,serverHold",
|
||||
"--superuser",
|
||||
"example.tld"));
|
||||
assertThat(e).hasMessageThat().contains("The domain 'example.tld' has status PENDING_DELETE.");
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@@ -33,6 +33,7 @@ import com.google.common.collect.ImmutableMap;
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
import com.google.common.collect.ImmutableSortedMap;
|
||||
import google.registry.flows.certs.CertificateChecker;
|
||||
import google.registry.flows.certs.CertificateChecker.InsecureCertificateException;
|
||||
import google.registry.model.registrar.Registrar;
|
||||
import google.registry.model.registrar.Registrar.State;
|
||||
import google.registry.model.registrar.Registrar.Type;
|
||||
@@ -265,9 +266,9 @@ class UpdateRegistrarCommandTest extends CommandTestCase<UpdateRegistrarCommand>
|
||||
Registrar registrar = loadRegistrar("NewRegistrar");
|
||||
assertThat(registrar.getClientCertificate()).isEmpty();
|
||||
assertThat(registrar.getClientCertificateHash()).isEmpty();
|
||||
IllegalArgumentException thrown =
|
||||
InsecureCertificateException thrown =
|
||||
assertThrows(
|
||||
IllegalArgumentException.class,
|
||||
InsecureCertificateException.class,
|
||||
() -> runCommand("--cert_file=" + getCertFilename(), "--force", "NewRegistrar"));
|
||||
assertThat(thrown.getMessage())
|
||||
.isEqualTo(
|
||||
@@ -282,9 +283,9 @@ class UpdateRegistrarCommandTest extends CommandTestCase<UpdateRegistrarCommand>
|
||||
Registrar registrar = loadRegistrar("NewRegistrar");
|
||||
assertThat(registrar.getClientCertificate()).isEmpty();
|
||||
assertThat(registrar.getClientCertificateHash()).isEmpty();
|
||||
IllegalArgumentException thrown =
|
||||
InsecureCertificateException thrown =
|
||||
assertThrows(
|
||||
IllegalArgumentException.class,
|
||||
InsecureCertificateException.class,
|
||||
() -> runCommand("--cert_file=" + getCertFilename(), "--force", "NewRegistrar"));
|
||||
assertThat(thrown.getMessage())
|
||||
.isEqualTo(
|
||||
@@ -298,9 +299,9 @@ class UpdateRegistrarCommandTest extends CommandTestCase<UpdateRegistrarCommand>
|
||||
fakeClock.setTo(DateTime.parse("2020-11-01T00:00:00Z"));
|
||||
Registrar registrar = loadRegistrar("NewRegistrar");
|
||||
assertThat(registrar.getFailoverClientCertificate()).isEmpty();
|
||||
IllegalArgumentException thrown =
|
||||
InsecureCertificateException thrown =
|
||||
assertThrows(
|
||||
IllegalArgumentException.class,
|
||||
InsecureCertificateException.class,
|
||||
() ->
|
||||
runCommand("--failover_cert_file=" + getCertFilename(), "--force", "NewRegistrar"));
|
||||
assertThat(thrown.getMessage())
|
||||
@@ -315,9 +316,9 @@ class UpdateRegistrarCommandTest extends CommandTestCase<UpdateRegistrarCommand>
|
||||
fakeClock.setTo(DateTime.parse("2055-10-01T00:00:00Z"));
|
||||
Registrar registrar = loadRegistrar("NewRegistrar");
|
||||
assertThat(registrar.getFailoverClientCertificate()).isEmpty();
|
||||
IllegalArgumentException thrown =
|
||||
InsecureCertificateException thrown =
|
||||
assertThrows(
|
||||
IllegalArgumentException.class,
|
||||
InsecureCertificateException.class,
|
||||
() ->
|
||||
runCommand("--failover_cert_file=" + getCertFilename(), "--force", "NewRegistrar"));
|
||||
assertThat(thrown.getMessage())
|
||||
|
||||
@@ -20,14 +20,17 @@ import static google.registry.testing.DatabaseHelper.createTld;
|
||||
import static google.registry.testing.DatabaseHelper.loadRegistrar;
|
||||
import static google.registry.testing.DatabaseHelper.persistResource;
|
||||
import static google.registry.testing.EppExceptionSubject.assertAboutEppExceptions;
|
||||
import static google.registry.util.DateTimeUtils.START_OF_TIME;
|
||||
import static org.joda.time.DateTimeZone.UTC;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
|
||||
import com.beust.jcommander.ParameterException;
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
import com.google.common.collect.ImmutableSortedMap;
|
||||
import google.registry.flows.EppException;
|
||||
import google.registry.flows.TransportCredentials.BadRegistrarPasswordException;
|
||||
import google.registry.flows.certs.CertificateChecker;
|
||||
import google.registry.model.registrar.Registrar;
|
||||
import google.registry.model.registrar.Registrar.State;
|
||||
import google.registry.testing.CertificateSamples;
|
||||
@@ -42,7 +45,7 @@ import org.junit.jupiter.api.Test;
|
||||
class ValidateLoginCredentialsCommandTest extends CommandTestCase<ValidateLoginCredentialsCommand> {
|
||||
|
||||
private static final String PASSWORD = "foo-BAR2";
|
||||
private static final String CERT_HASH = CertificateSamples.SAMPLE_CERT_HASH;
|
||||
private static final String CERT_HASH = CertificateSamples.SAMPLE_CERT3_HASH;
|
||||
private static final String CLIENT_IP = "1.2.3.4";
|
||||
|
||||
@BeforeEach
|
||||
@@ -52,11 +55,18 @@ class ValidateLoginCredentialsCommandTest extends CommandTestCase<ValidateLoginC
|
||||
loadRegistrar("NewRegistrar")
|
||||
.asBuilder()
|
||||
.setPassword(PASSWORD)
|
||||
.setClientCertificate(CertificateSamples.SAMPLE_CERT, DateTime.now(UTC))
|
||||
.setClientCertificate(CertificateSamples.SAMPLE_CERT3, DateTime.now(UTC))
|
||||
.setIpAddressAllowList(ImmutableList.of(new CidrAddressBlock(CLIENT_IP)))
|
||||
.setState(ACTIVE)
|
||||
.setAllowedTlds(ImmutableSet.of("tld"))
|
||||
.build());
|
||||
command.certificateChecker =
|
||||
new CertificateChecker(
|
||||
ImmutableSortedMap.of(START_OF_TIME, 825, DateTime.parse("2020-09-01T00:00:00Z"), 398),
|
||||
30,
|
||||
2048,
|
||||
ImmutableSet.of("secp256r1", "secp384r1"),
|
||||
fakeClock);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -64,7 +74,7 @@ class ValidateLoginCredentialsCommandTest extends CommandTestCase<ValidateLoginC
|
||||
runCommand(
|
||||
"--client=NewRegistrar",
|
||||
"--password=" + PASSWORD,
|
||||
"--cert_hash=" + CERT_HASH,
|
||||
"--cert_file=" + getCertFilename(CertificateSamples.SAMPLE_CERT3),
|
||||
"--ip_address=" + CLIENT_IP);
|
||||
}
|
||||
|
||||
@@ -83,7 +93,7 @@ class ValidateLoginCredentialsCommandTest extends CommandTestCase<ValidateLoginC
|
||||
runCommand(
|
||||
"--client=NewRegistrar",
|
||||
"--password=" + PASSWORD,
|
||||
"--cert_hash=" + CERT_HASH,
|
||||
"--cert_file=" + getCertFilename(CertificateSamples.SAMPLE_CERT3),
|
||||
"--ip_address=" + CLIENT_IP));
|
||||
assertThat(thrown)
|
||||
.hasMessageThat()
|
||||
@@ -91,7 +101,7 @@ class ValidateLoginCredentialsCommandTest extends CommandTestCase<ValidateLoginC
|
||||
}
|
||||
|
||||
@Test
|
||||
void testFailure_loginWithBadPassword() {
|
||||
void testFailure_loginWithBadPassword() throws Exception {
|
||||
EppException thrown =
|
||||
assertThrows(
|
||||
BadRegistrarPasswordException.class,
|
||||
@@ -99,7 +109,7 @@ class ValidateLoginCredentialsCommandTest extends CommandTestCase<ValidateLoginC
|
||||
runCommand(
|
||||
"--client=NewRegistrar",
|
||||
"--password=" + new StringBuilder(PASSWORD).reverse(),
|
||||
"--cert_hash=" + CERT_HASH,
|
||||
"--cert_file=" + getCertFilename(CertificateSamples.SAMPLE_CERT3),
|
||||
"--ip_address=" + CLIENT_IP));
|
||||
assertAboutEppExceptions().that(thrown).marshalsToXml();
|
||||
}
|
||||
|
||||
+9
-7
@@ -37,15 +37,17 @@ import google.registry.model.registrar.Registrar;
|
||||
import google.registry.model.reporting.HistoryEntry;
|
||||
import google.registry.schema.domain.RegistryLock;
|
||||
import google.registry.testing.DeterministicStringGenerator;
|
||||
import google.registry.testing.DualDatabaseTest;
|
||||
import google.registry.testing.TestOfyAndSql;
|
||||
import google.registry.tools.CommandTestCase;
|
||||
import google.registry.util.StringGenerator.Alphabets;
|
||||
import java.util.Optional;
|
||||
import org.joda.time.DateTime;
|
||||
import org.joda.time.Duration;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
/** Unit tests for {@link BackfillRegistryLocksCommand}. */
|
||||
@DualDatabaseTest
|
||||
class BackfillRegistryLocksCommandTest extends CommandTestCase<BackfillRegistryLocksCommand> {
|
||||
|
||||
@BeforeEach
|
||||
@@ -57,7 +59,7 @@ class BackfillRegistryLocksCommandTest extends CommandTestCase<BackfillRegistryL
|
||||
command.stringGenerator = new DeterministicStringGenerator(Alphabets.BASE_58);
|
||||
}
|
||||
|
||||
@Test
|
||||
@TestOfyAndSql
|
||||
void testSimpleBackfill() throws Exception {
|
||||
DomainBase domain = persistLockedDomain("example.tld");
|
||||
Truth8.assertThat(getMostRecentRegistryLockByRepoId(domain.getRepoId())).isEmpty();
|
||||
@@ -69,7 +71,7 @@ class BackfillRegistryLocksCommandTest extends CommandTestCase<BackfillRegistryL
|
||||
Truth8.assertThat(lockOptional.get().getLockCompletionTimestamp()).isPresent();
|
||||
}
|
||||
|
||||
@Test
|
||||
@TestOfyAndSql
|
||||
void testBackfill_onlyLockedDomains() throws Exception {
|
||||
DomainBase neverLockedDomain = persistActiveDomain("neverlocked.tld");
|
||||
DomainBase previouslyLockedDomain = persistLockedDomain("unlocked.tld");
|
||||
@@ -89,7 +91,7 @@ class BackfillRegistryLocksCommandTest extends CommandTestCase<BackfillRegistryL
|
||||
assertThat(Iterables.getOnlyElement(locks).getDomainName()).isEqualTo("locked.tld");
|
||||
}
|
||||
|
||||
@Test
|
||||
@TestOfyAndSql
|
||||
void testBackfill_skipsDeletedDomains() throws Exception {
|
||||
DomainBase domain = persistDeletedDomain("example.tld", fakeClock.nowUtc());
|
||||
persistResource(domain.asBuilder().setStatusValues(REGISTRY_LOCK_STATUSES).build());
|
||||
@@ -98,7 +100,7 @@ class BackfillRegistryLocksCommandTest extends CommandTestCase<BackfillRegistryL
|
||||
Truth8.assertThat(getMostRecentRegistryLockByRepoId(domain.getRepoId())).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
@TestOfyAndSql
|
||||
void testBackfill_skipsDomains_ifLockAlreadyExists() throws Exception {
|
||||
DomainBase domain = persistLockedDomain("example.tld");
|
||||
|
||||
@@ -123,7 +125,7 @@ class BackfillRegistryLocksCommandTest extends CommandTestCase<BackfillRegistryL
|
||||
.isEqualTo(previousLock.getLockCompletionTimestamp());
|
||||
}
|
||||
|
||||
@Test
|
||||
@TestOfyAndSql
|
||||
void testBackfill_usesUrsTime_ifExists() throws Exception {
|
||||
DateTime ursTime = fakeClock.nowUtc();
|
||||
DomainBase ursDomain = persistLockedDomain("urs.tld");
|
||||
@@ -151,7 +153,7 @@ class BackfillRegistryLocksCommandTest extends CommandTestCase<BackfillRegistryL
|
||||
assertThat(nonUrsLock.getLockCompletionTimestamp()).hasValue(fakeClock.nowUtc());
|
||||
}
|
||||
|
||||
@Test
|
||||
@TestOfyAndSql
|
||||
void testFailure_mustProvideDomainRoids() {
|
||||
assertThat(assertThrows(IllegalArgumentException.class, this::runCommandForced))
|
||||
.hasMessageThat()
|
||||
|
||||
@@ -33,7 +33,7 @@
|
||||
"events": [
|
||||
{
|
||||
"eventAction": "registration",
|
||||
"eventActor": "foo",
|
||||
"eventActor": "TheRegistrar",
|
||||
"eventDate": "1997-01-01T00:00:00.000Z"
|
||||
},
|
||||
{
|
||||
|
||||
@@ -33,7 +33,7 @@
|
||||
"events": [
|
||||
{
|
||||
"eventAction": "registration",
|
||||
"eventActor": "foo",
|
||||
"eventActor": "TheRegistrar",
|
||||
"eventDate": "1997-01-01T00:00:00.000Z"
|
||||
},
|
||||
{
|
||||
|
||||
@@ -34,7 +34,7 @@
|
||||
"events": [
|
||||
{
|
||||
"eventAction": "registration",
|
||||
"eventActor": "foo",
|
||||
"eventActor": "TheRegistrar",
|
||||
"eventDate": "1997-01-01T00:00:00.000Z"
|
||||
},
|
||||
{
|
||||
@@ -47,7 +47,7 @@
|
||||
},
|
||||
{
|
||||
"eventAction": "deletion",
|
||||
"eventActor": "foo",
|
||||
"eventActor": "evilregistrar",
|
||||
"eventDate": "1999-07-01T00:00:00.000Z"
|
||||
},
|
||||
{
|
||||
|
||||
+1
-1
@@ -33,7 +33,7 @@
|
||||
"events": [
|
||||
{
|
||||
"eventAction": "registration",
|
||||
"eventActor": "foo",
|
||||
"eventActor": "TheRegistrar",
|
||||
"eventDate": "1997-01-01T00:00:00.000Z"
|
||||
},
|
||||
{
|
||||
|
||||
@@ -34,7 +34,7 @@
|
||||
"events": [
|
||||
{
|
||||
"eventAction": "registration",
|
||||
"eventActor": "foo",
|
||||
"eventActor": "TheRegistrar",
|
||||
"eventDate": "1997-01-01T00:00:00.000Z"
|
||||
},
|
||||
{
|
||||
|
||||
+1
-1
@@ -34,7 +34,7 @@
|
||||
"events": [
|
||||
{
|
||||
"eventAction": "registration",
|
||||
"eventActor": "foo",
|
||||
"eventActor": "TheRegistrar",
|
||||
"eventDate": "1997-01-01T00:00:00.000Z"
|
||||
},
|
||||
{
|
||||
|
||||
@@ -31,7 +31,7 @@
|
||||
"events": [
|
||||
{
|
||||
"eventAction": "registration",
|
||||
"eventActor": "foo",
|
||||
"eventActor": "unicoderegistrar",
|
||||
"eventDate": "1999-09-01T00:00:00.000Z"
|
||||
},
|
||||
{
|
||||
@@ -44,7 +44,7 @@
|
||||
},
|
||||
{
|
||||
"eventAction": "transfer",
|
||||
"eventActor": "foo",
|
||||
"eventActor": "unicoderegistrar",
|
||||
"eventDate": "1999-12-01T00:00:00.000Z"
|
||||
},
|
||||
{
|
||||
|
||||
@@ -31,7 +31,7 @@
|
||||
"events": [
|
||||
{
|
||||
"eventAction": "registration",
|
||||
"eventActor": "foo",
|
||||
"eventActor": "unicoderegistrar",
|
||||
"eventDate": "1999-09-01T00:00:00.000Z"
|
||||
},
|
||||
{
|
||||
@@ -44,7 +44,7 @@
|
||||
},
|
||||
{
|
||||
"eventAction": "transfer",
|
||||
"eventActor": "foo",
|
||||
"eventActor": "unicoderegistrar",
|
||||
"eventDate": "1999-12-01T00:00:00.000Z"
|
||||
},
|
||||
{
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
"events": [
|
||||
{
|
||||
"eventAction": "registration",
|
||||
"eventActor": "foo",
|
||||
"eventActor": "unicoderegistrar",
|
||||
"eventDate": "2000-01-01T00:00:00.000Z"
|
||||
},
|
||||
{
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
"events": [
|
||||
{
|
||||
"eventAction": "registration",
|
||||
"eventActor": "foo",
|
||||
"eventActor": "TheRegistrar",
|
||||
"eventDate": "1999-01-01T00:00:00.000Z"
|
||||
},
|
||||
{
|
||||
|
||||
@@ -261,11 +261,11 @@ td.section {
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="property_name">generated on</td>
|
||||
<td class="property_value">2021-01-14 16:15:22.842637</td>
|
||||
<td class="property_value">2021-01-21 00:11:27.19594</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="property_name">last flyway file</td>
|
||||
<td id="lastFlywayFile" class="property_value">V84__add_vkey_columns_in_billing_cancellation.sql</td>
|
||||
<td id="lastFlywayFile" class="property_value">V85__add_required_columns_in_transfer_data.sql</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
@@ -274,19 +274,19 @@ td.section {
|
||||
<svg viewbox="0.00 0.00 4221.44 2624.18" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" id="erDiagram" style="overflow: hidden; width: 100%; height: 800px"> <g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 2620.18)">
|
||||
<title>SchemaCrawler_Diagram</title>
|
||||
<polygon fill="white" stroke="transparent" points="-4,4 -4,-2620.18 4217.44,-2620.18 4217.44,4 -4,4" />
|
||||
<text text-anchor="start" x="3944.94" y="-29.8" font-family="Helvetica,sans-Serif" font-size="14.00">
|
||||
<text text-anchor="start" x="3952.94" y="-29.8" font-family="Helvetica,sans-Serif" font-size="14.00">
|
||||
generated by
|
||||
</text>
|
||||
<text text-anchor="start" x="4027.94" y="-29.8" font-family="Helvetica,sans-Serif" font-size="14.00">
|
||||
<text text-anchor="start" x="4035.94" y="-29.8" font-family="Helvetica,sans-Serif" font-size="14.00">
|
||||
SchemaCrawler 16.10.1
|
||||
</text>
|
||||
<text text-anchor="start" x="3943.94" y="-10.8" font-family="Helvetica,sans-Serif" font-size="14.00">
|
||||
<text text-anchor="start" x="3951.94" y="-10.8" font-family="Helvetica,sans-Serif" font-size="14.00">
|
||||
generated on
|
||||
</text>
|
||||
<text text-anchor="start" x="4027.94" y="-10.8" font-family="Helvetica,sans-Serif" font-size="14.00">
|
||||
2021-01-14 16:15:22.842637
|
||||
<text text-anchor="start" x="4035.94" y="-10.8" font-family="Helvetica,sans-Serif" font-size="14.00">
|
||||
2021-01-21 00:11:27.19594
|
||||
</text>
|
||||
<polygon fill="none" stroke="#888888" points="3940.44,-4 3940.44,-44 4205.44,-44 4205.44,-4 3940.44,-4" /> <!-- allocationtoken_a08ccbef -->
|
||||
<polygon fill="none" stroke="#888888" points="3948.44,-4 3948.44,-44 4205.44,-44 4205.44,-4 3948.44,-4" /> <!-- allocationtoken_a08ccbef -->
|
||||
<g id="node1" class="node">
|
||||
<title>allocationtoken_a08ccbef</title>
|
||||
<polygon fill="#ebcef2" stroke="transparent" points="2538.5,-1071.68 2538.5,-1090.68 2691.5,-1090.68 2691.5,-1071.68 2538.5,-1071.68" />
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -82,3 +82,4 @@ V81__drop_spec11_fkeys.sql
|
||||
V82__add_columns_to_restore_symmetric_billing_vkey.sql
|
||||
V83__add_indexes_on_domainhost.sql
|
||||
V84__add_vkey_columns_in_billing_cancellation.sql
|
||||
V85__add_required_columns_in_transfer_data.sql
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
-- Copyright 2021 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.
|
||||
|
||||
alter table "Contact"
|
||||
add column if not exists "transfer_history_entry_id" int8;
|
||||
alter table "Contact"
|
||||
add column if not exists "transfer_repo_id" text;
|
||||
alter table "Contact"
|
||||
rename column "transfer_gaining_poll_message_id"
|
||||
to "transfer_poll_message_id_1";
|
||||
alter table "Contact"
|
||||
rename column "transfer_losing_poll_message_id"
|
||||
to "transfer_poll_message_id_2";
|
||||
|
||||
alter table "ContactHistory"
|
||||
add column if not exists "transfer_history_entry_id" int8;
|
||||
alter table "ContactHistory"
|
||||
add column if not exists "transfer_repo_id" text;
|
||||
alter table "ContactHistory"
|
||||
rename column "transfer_gaining_poll_message_id"
|
||||
to "transfer_poll_message_id_1";
|
||||
alter table "ContactHistory"
|
||||
rename column "transfer_losing_poll_message_id"
|
||||
to "transfer_poll_message_id_2";
|
||||
|
||||
alter table "Domain"
|
||||
add column if not exists "transfer_history_entry_id" int8;
|
||||
alter table "Domain"
|
||||
add column if not exists "transfer_repo_id" text;
|
||||
alter table "Domain"
|
||||
rename column "transfer_gaining_poll_message_id"
|
||||
to "transfer_poll_message_id_1";
|
||||
alter table "Domain"
|
||||
rename column "transfer_losing_poll_message_id"
|
||||
to "transfer_poll_message_id_2";
|
||||
|
||||
alter table "DomainHistory"
|
||||
add column if not exists "transfer_history_entry_id" int8;
|
||||
alter table "DomainHistory"
|
||||
add column if not exists "transfer_repo_id" text;
|
||||
alter table "DomainHistory"
|
||||
rename column "transfer_gaining_poll_message_id"
|
||||
to "transfer_poll_message_id_1";
|
||||
alter table "DomainHistory"
|
||||
rename column "transfer_losing_poll_message_id"
|
||||
to "transfer_poll_message_id_2";
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user