1
0
mirror of https://github.com/google/nomulus synced 2026-05-21 15:21:48 +00:00

Compare commits

...

20 Commits

Author SHA1 Message Date
Ben McIlwain
6f0bc1ded9 Add Augmented Latin IDN table to IDN enums (#2914)
This was added in https://github.com/google/nomulus/pull/2884 , but now as of
this PR it can actually be configured and used on a TLD.
2025-12-27 00:57:24 +00:00
gbrodman
db9fc3271d Change EPP errors 2306->2005 for some structural issues (#2911)
2306 signifies something that is syntactically valid but semantically
invalid (like if someone tried to register a .com domain). These errors
are for domain syntax that could never be valid, thus we should throw a
syntax exception instead of a policy exception.
2025-12-26 16:08:04 +00:00
Ben McIlwain
84491fde70 Don't allow underscores in TLD ROID suffixes (#2913)
Per ICANN it's a disallowed character.
2025-12-26 16:01:28 +00:00
Juan Celhay
0519e2ffcf Change gradle memory/workers to avoid OOM in CB (#2910) 2025-12-23 15:49:25 +00:00
gbrodman
85f75494ab Remove implementation of contact flows (#2896)
Now that we have transitioned to the minimum dataset, we no longer
support any actions on contacts (and by the time this is merged /
deployed, all contacts will be deleted). We should just throw an
appropriate exception on all contact-related flows. We don't delete the
flows themselves, so that we can have an appropriate error message.

We also keep all the flows and XML templates around individually for now because we may be
required to continue to differentiate the requests in ICANN activity
reporting (e.g. srs-cont-create vs srs-cont-delete)
2025-12-23 15:38:24 +00:00
Ben McIlwain
cbba91558a Allow double hyphens in 3rd&4th position in all domain operations (#2909)
This is a follow-up to PR #2908, which relaxed this restriction on bare TLDs
only, but now we also allow it systemwide on domains and hostnames as well.  The
rules against hyphens in these positions are still enforced on all parts of the
domain name except the last one. Correct handling of multi-part TLDs in this
regard is out of scope in this PR; a multi-part TLD that looked something like
".zz--foobar.foobar" would still fail validation. (But of course you cannot a
priori know just from looking at a 3-part string whether it might be a hostname
on a normal TLD, or a domain name on a 2-part TLD.)

This also has some annoying interactions with a trailing dot (indicating the
root), which need to be preserved, but otherwise don't affect how TLD validation
is handled.

BUG= http://b/471013082
2025-12-23 00:57:57 +00:00
Ben McIlwain
c24f09febc Don't call canonicalizeHostname() on nomulus command TLD args (#2908)
The canonicalizeHostname() helper method is only suitable for use with domain
names or host names. It does not work on bare TLDs, because a bare TLD can
have hyphens in the third and fourth position without necessarily being an IDN.
Note that the configure TLD command already correctly allows TLDs with such
names to be created.

Note that we are still enforcing that the TLDs to be added exist, so they have
to pass all TLD naming requirements that are enforced on creating TLDs, and we
are still lowercasing the TLD names passed as arguments here (though we're no
longer punycoding them, although arguably that's not super useful on
command-line params anyway).

BUG= http://b/471013082
2025-12-22 21:34:55 +00:00
Weimin Yu
fd51035f23 Stop depending on GCS public access for Kokoro (#2907)
We used to publish test artifacts to a Maven repo on GCS, for use by
schema tests. For this to work with Kokoro, the GCS bucket must be
accessible to all users.

To comply with the no-public-user requirement, we store the necessary
jars at at well-known bucket and map them into Kokoro. This strategy
cannot be used on the Maven repo because only a small number of files
with fixed names may be mapped. With the Maven repo, there are too many
files to map.
2025-12-17 20:55:03 +00:00
gbrodman
90eb078e3f Add a BulkDomainTransferCommand (#2898)
This is a decently simple wrapper around the previously-created
BulkDomainTransferAction that batches a provided list of domains up and
sends them along to be transferred.
2025-12-12 21:15:47 +00:00
gbrodman
2a94bdc257 Add a command to delete feature flags (#2904)
This allows us to delete old ones to avoid confusion, and so that we can
more easily clean up the codebase.
2025-12-11 21:52:59 +00:00
gbrodman
50fa49e0c0 Always act as if contacts are prohibited (#2897)
This PR finds instances where we previously checked if the feature flag
for contacts-prohibited was set and removes those checks, making the
contacts-prohibited behavior the only behavior. Because the tests didn't
have that feature flag set, this means we need to change a ton of tests
to remove contact references.
2025-12-11 19:48:26 +00:00
gbrodman
a581259edb Remove trailing slash in schema-deploy script (#2903) 2025-12-11 18:32:09 +00:00
Pavlo Tkach
fcdac3e86e Update nomulus-frontend.yaml memory requests (#2900) 2025-12-10 22:36:28 +00:00
Nilay Shah
b652f81193 Refactor MosApiTLSKeySecretName configuration to the correct name (#2899) 2025-12-10 11:28:46 +00:00
Nilay Shah
d98d65eee5 Add mosapi client to intract with ICANN's monitoring system (#2892)
* Add mosapi client to intract with ICANN's monitoring system

This change introduces a comprehensive client to interact with ICANN's Monitoring System API (MoSAPI). This provides direct, automated access to critical registry health and compliance data, moving Nomulus towards a more proactive monitoring posture.

A core, stateless MosApiClient that manages communication and authentication with the MoSAPI service using TLS client certificates.

* Resolve review feedback & upgrade to OkHttp3 client

This commit addresses and resolves all outstanding review comments, primarily encompassing a shift to OkHttp3, security configuration cleanup, and general code improvements.

* **Review:** Addressed and resolved all pending review comments.
* **Feature:** Switched the underlying HTTP client implementation to [OkHttp3](https://square.github.io/okhttp/).
* **Configuration:** Consolidated TLS Certificates-related configuration into the dedicated configuration area.
* **Cleanup:** Removed unused components (`HttpUtils` and `HttpModule`) and performed general code cleanup.
* **Quality:** Improved exception handling logic for better robustness.

* Refactor and fix Mosapi exception handling

Addresses code review feedback and resulting test failures.

- Flattens package structure by moving MosApiException and its test.
- Corrects exception handling to ensure MosApiAuthorizationException
  propagates correctly, before the general exception handler.
- Adds a default case to the MosApiException factory for robustness.
- Uses lowercase for placeholder TLDs in default-config.yaml.

* Refactor and improve Mosapi client implementation

Simplifying URL validation with Guava
Preconditions and refining exception handling to use `Throwables`.

* Refactor precondition checks using project specific utility
2025-12-09 16:29:05 +00:00
gbrodman
28e72bd0d0 Add a BulkDomainTransferAction (#2893)
This will be necessary if we wish to do larger BTAPPA transfers (or
other types of transfers, I suppose). The nomulus command-line tool is
not fast enough to quickly transfer thousands of domains within a
reasonable timeframe.
2025-12-08 20:28:25 +00:00
gbrodman
0777be3d6c Allow superuser ext to override client/server transfer prohibited (#2890)
The superuser can remove/add those statuses anyway, so there's not
really any point. This also saves us trouble if we need to do a BTAPPA
transfer.
2025-12-05 20:22:15 +00:00
Weimin Yu
f9cd167ae4 Copy artifacts for schema tests after deployment (#2895)
After each deployment in sandbox or production, move the artifacts from
the corresponding release to a well-known location so that they can be
mapped to Kokoro in presubmit tests. The Kokoro-mapping does not need
public access to the GCS bucket.

The artifacts include the  postgresql schema jar, the nomulus release
jar, and the uber jar of the nomulus schema integration test classes.

Every jar name consists of a fixed prefix and the environment. Each jar
of a new deployment overrides the previous copy.
2025-12-04 20:55:19 +00:00
sharma1210
eed1886121 Implement rdap_query command (#2886)
* Implement rdap_query command

* modifying and correcting issues

* modifying and correcting issues

* modifying and correcting issues

* resolving comments

* resolving comments

* resolving comments

* resolving comments

* resolving comments

* modifying and correcting issues

* resolving comments

* resolving comments

* resolving comments

* modifying and correcting issues

* modifying and correcting issues

* modifying and correcting issues

* resolving comments

* modifying and correcting issues

* resolving comments

* Fixing Deduplication in test

* Fixing Deduplication in test

* resolving comments
2025-12-01 20:45:57 +00:00
gbrodman
7149fd3307 Remove more references to GAE (#2894)
These are old/pointless now that we've migrated to GKE. Note that this
doesn't update anything in the docs/ folder, as that's a much larger
project that should be done on its own.
2025-12-01 16:43:49 +00:00
343 changed files with 3020 additions and 8360 deletions

View File

@@ -84,10 +84,10 @@ tasks.build.dependsOn(tasks.checkLicense)
// Paths to main and test sources.
ext.projectRootDir = "${rootDir}"
// Tasks to deploy/stage all App Engine services
// Tasks to deploy/stage all services
task deploy {
group = 'deployment'
description = 'Deploys all services to App Engine.'
description = 'Deploys all services.'
}
task stage {

View File

@@ -33,8 +33,8 @@ public abstract class DateTimeUtils {
/**
* A date in the far future that we can treat as infinity.
*
* <p>This value is (2^63-1)/1000 rounded down. AppEngine stores dates as 64 bit microseconds, but
* Java uses milliseconds, so this is the largest representable date that will survive a
* <p>This value is (2^63-1)/1000 rounded down. Postgres can store dates as 64 bit microseconds,
* but Java uses milliseconds, so this is the largest representable date that will survive a
* round-trip through the database.
*/
public static final DateTime END_OF_TIME = new DateTime(Long.MAX_VALUE / 1000, DateTimeZone.UTC);

View File

@@ -56,7 +56,7 @@ PROPERTIES_HEADER = """\
# nom_build), run ./nom_build --help.
#
# DO NOT EDIT THIS FILE BY HAND
org.gradle.jvmargs=-Xmx2048m
org.gradle.jvmargs=-Xmx4096m
org.gradle.caching=true
org.gradle.parallel=true
"""
@@ -104,7 +104,7 @@ PROPERTIES = [
Property('testFilter',
'Comma separated list of test patterns, if specified run only '
'these.'),
Property('environment', 'GAE Environment for deployment and staging.'),
Property('environment', 'Environment for deployment and staging.'),
# Cloud SQL properties
Property('dbServer',
@@ -117,28 +117,19 @@ PROPERTIES = [
Property('dbUser', 'Database user name for use in connection'),
Property('dbPassword', 'Database password for use in connection'),
Property('publish_repo',
'Maven repository that hosts the Cloud SQL schema jar and the '
'registry server test jars. Such jars are needed for '
'server/schema integration tests. Please refer to <a '
'href="./integration/README.md">integration project</a> for more '
'information.'),
Property('baseSchemaTag',
'The nomulus version tag of the schema for use in the schema'
'deployment integration test (:db:schemaIncrementalDeployTest)'),
Property('schema_version',
'The nomulus version tag of the schema for use in a database'
'integration test.'),
Property('nomulus_version',
'The version of nomulus to test against in a database '
'integration test.'),
Property('dot_path',
'The path to "dot", part of the graphviz package that converts '
'a BEAM pipeline to image. Setting this property to empty string '
'will disable image generation.',
'/usr/bin/dot'),
Property('pipeline',
'The name of the Beam pipeline being staged.')
'The name of the Beam pipeline being staged.'),
Property('nomulus_env',
'For use by scripts. Normally not set manually.'),
Property('schema_env',
'For use by scripts. Normally not set manually.'),
Property('schemaTestArtifactsDir',
'For use by scripts. Normally not set manually.')
]
GRADLE_FLAGS = [

View File

@@ -9,7 +9,7 @@ expected to change.
## Deployment
Webapp is deployed with the nomulus default service war to Google App Engine.
The webapp is deployed with the nomulus default service war to GKE.
During nomulus default service war build task, gradle script triggers the
following:

View File

@@ -110,7 +110,7 @@ configurations {
// for details.
nomulus_test
// Exclude non-canonical servlet-api jars. Our AppEngine deployment uses
// Exclude non-canonical servlet-api jars. Our deployment uses
// javax.servlet:servlet-api:2.5
// For reasons we do not understand, marking the following dependencies as
// compileOnly instead of compile does not exclude them from runtimeClasspath.
@@ -646,23 +646,6 @@ artifacts {
nomulus_test testUberJar
}
publishing {
repositories {
maven {
url project.publish_repo
}
}
publications {
nomulusTestsPublication(MavenPublication) {
groupId 'google.registry'
artifactId 'nomulus_test'
version project.nomulus_version
artifact nomulusFossJar
artifact testUberJar
}
}
}
task buildToolImage(dependsOn: nomulus, type: Exec) {
commandLine 'docker', 'build', '-t', 'nomulus-tool', '.'
}

View File

@@ -28,13 +28,20 @@ import static google.registry.request.RequestParameters.extractRequiredDatetimeP
import static google.registry.request.RequestParameters.extractRequiredParameter;
import static google.registry.request.RequestParameters.extractSetOfDatetimeParameters;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.common.util.concurrent.RateLimiter;
import com.google.gson.Gson;
import com.google.gson.JsonElement;
import com.google.gson.reflect.TypeToken;
import dagger.Module;
import dagger.Provides;
import google.registry.request.HttpException.BadRequestException;
import google.registry.request.OptionalJsonPayload;
import google.registry.request.Parameter;
import jakarta.inject.Named;
import jakarta.servlet.http.HttpServletRequest;
import java.util.List;
import java.util.Optional;
import org.joda.time.DateTime;
@@ -44,6 +51,8 @@ public class BatchModule {
public static final String PARAM_FAST = "fast";
static final int DEFAULT_MAX_QPS = 10;
@Provides
@Parameter("url")
static String provideUrl(HttpServletRequest req) {
@@ -140,8 +149,6 @@ public class BatchModule {
return extractBooleanParameter(req, PARAM_FAST);
}
private static final int DEFAULT_MAX_QPS = 10;
@Provides
@Parameter("maxQps")
static int provideMaxQps(HttpServletRequest req) {
@@ -149,8 +156,42 @@ public class BatchModule {
}
@Provides
@Named("removeAllDomainContacts")
static RateLimiter provideRemoveAllDomainContactsRateLimiter(@Parameter("maxQps") int maxQps) {
@Named("standardRateLimiter")
static RateLimiter provideStandardRateLimiter(@Parameter("maxQps") int maxQps) {
return RateLimiter.create(maxQps);
}
@Provides
@Parameter("gainingRegistrarId")
static String provideGainingRegistrarId(HttpServletRequest req) {
return extractRequiredParameter(req, "gainingRegistrarId");
}
@Provides
@Parameter("losingRegistrarId")
static String provideLosingRegistrarId(HttpServletRequest req) {
return extractRequiredParameter(req, "losingRegistrarId");
}
@Provides
@Parameter("bulkTransferDomainNames")
static ImmutableList<String> provideBulkTransferDomainNames(
Gson gson, @OptionalJsonPayload Optional<JsonElement> optionalJsonElement) {
return optionalJsonElement
.map(je -> ImmutableList.copyOf(gson.fromJson(je, new TypeToken<List<String>>() {})))
.orElseThrow(
() -> new BadRequestException("Missing POST body of bulk transfer domain names"));
}
@Provides
@Parameter("requestedByRegistrar")
static boolean provideRequestedByRegistrar(HttpServletRequest req) {
return extractBooleanParameter(req, "requestedByRegistrar");
}
@Provides
@Parameter("reason")
static String provideReason(HttpServletRequest req) {
return extractRequiredParameter(req, "reason");
}
}

View File

@@ -0,0 +1,242 @@
// Copyright 2025 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.net.MediaType.PLAIN_TEXT_UTF_8;
import static google.registry.flows.FlowUtils.marshalWithLenientRetry;
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
import static jakarta.servlet.http.HttpServletResponse.SC_INTERNAL_SERVER_ERROR;
import static jakarta.servlet.http.HttpServletResponse.SC_NO_CONTENT;
import static jakarta.servlet.http.HttpServletResponse.SC_OK;
import static java.nio.charset.StandardCharsets.US_ASCII;
import com.google.common.collect.ImmutableList;
import com.google.common.flogger.FluentLogger;
import com.google.common.util.concurrent.RateLimiter;
import google.registry.flows.EppController;
import google.registry.flows.EppRequestSource;
import google.registry.flows.PasswordOnlyTransportCredentials;
import google.registry.flows.StatelessRequestSessionMetadata;
import google.registry.model.ForeignKeyUtils;
import google.registry.model.domain.Domain;
import google.registry.model.eppcommon.ProtocolDefinition;
import google.registry.model.eppcommon.StatusValue;
import google.registry.model.eppoutput.EppOutput;
import google.registry.request.Action;
import google.registry.request.Parameter;
import google.registry.request.Response;
import google.registry.request.auth.Auth;
import google.registry.request.lock.LockHandler;
import google.registry.util.DateTimeUtils;
import jakarta.inject.Inject;
import jakarta.inject.Named;
import java.util.Optional;
import java.util.concurrent.Callable;
import java.util.logging.Level;
import org.joda.time.Duration;
/**
* An action that transfers a set of domains from one registrar to another.
*
* <p>This should be used as part of the BTAPPA (Bulk Transfer After a Partial Portfolio
* Acquisition) process in order to transfer a (possibly large) list of domains from one registrar
* to another, though it may be used in other situations as well.
*
* <p>The body of the HTTP post request should be a JSON list of the domains to be transferred.
* Because the list of domains to process can be quite large, this action should be called by a tool
* that batches the list of domains into reasonable sizes if necessary. The recommended usage path
* is to call this through the {@link google.registry.tools.BulkDomainTransferCommand}, which
* handles batching and input handling.
*
* <p>This runs as a single-threaded idempotent action that runs a superuser domain transfer on each
* domain to process. We go through the standard EPP process to make sure that we have an accurate
* historical representation of events (rather than force-modifying the domains in place).
*
* <p>Consider passing in an "maxQps" parameter based on the number of domains being transferred,
* otherwise the default is {@link BatchModule#DEFAULT_MAX_QPS}.
*/
@Action(
service = Action.Service.BACKEND,
path = BulkDomainTransferAction.PATH,
method = Action.Method.POST,
auth = Auth.AUTH_ADMIN)
public class BulkDomainTransferAction implements Runnable {
public static final String PATH = "/_dr/task/bulkDomainTransfer";
private static final String SUPERUSER_TRANSFER_XML_FORMAT =
"""
<epp xmlns="urn:ietf:params:xml:ns:epp-1.0">
<command>
<transfer op="request">
<domain:transfer xmlns:domain="urn:ietf:params:xml:ns:domain-1.0">
<domain:name>%DOMAIN_NAME%</domain:name>
</domain:transfer>
</transfer>
<extension>
<superuser:domainTransferRequest xmlns:superuser="urn:google:params:xml:ns:superuser-1.0">
<superuser:renewalPeriod unit="y">0</superuser:renewalPeriod>
<superuser:automaticTransferLength>0</superuser:automaticTransferLength>
</superuser:domainTransferRequest>
<metadata:metadata xmlns:metadata="urn:google:params:xml:ns:metadata-1.0">
<metadata:reason>%REASON%</metadata:reason>
<metadata:requestedByRegistrar>%REQUESTED_BY_REGISTRAR%</metadata:requestedByRegistrar>
</metadata:metadata>
</extension>
<clTRID>BulkDomainTransferAction</clTRID>
</command>
</epp>
""";
private static final String LOCK_NAME = "Domain bulk transfer";
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
private final EppController eppController;
private final LockHandler lockHandler;
private final RateLimiter rateLimiter;
private final ImmutableList<String> bulkTransferDomainNames;
private final String gainingRegistrarId;
private final String losingRegistrarId;
private final boolean requestedByRegistrar;
private final String reason;
private final Response response;
private int successes = 0;
private int alreadyTransferred = 0;
private int pendingDelete = 0;
private int missingDomains = 0;
private int errors = 0;
@Inject
BulkDomainTransferAction(
EppController eppController,
LockHandler lockHandler,
@Named("standardRateLimiter") RateLimiter rateLimiter,
@Parameter("bulkTransferDomainNames") ImmutableList<String> bulkTransferDomainNames,
@Parameter("gainingRegistrarId") String gainingRegistrarId,
@Parameter("losingRegistrarId") String losingRegistrarId,
@Parameter("requestedByRegistrar") boolean requestedByRegistrar,
@Parameter("reason") String reason,
Response response) {
this.eppController = eppController;
this.lockHandler = lockHandler;
this.rateLimiter = rateLimiter;
this.bulkTransferDomainNames = bulkTransferDomainNames;
this.gainingRegistrarId = gainingRegistrarId;
this.losingRegistrarId = losingRegistrarId;
this.requestedByRegistrar = requestedByRegistrar;
this.reason = reason;
this.response = response;
}
@Override
public void run() {
response.setContentType(PLAIN_TEXT_UTF_8);
Callable<Void> runner =
() -> {
try {
runLocked();
response.setStatus(SC_OK);
} catch (Exception e) {
logger.atSevere().withCause(e).log("Errored out during execution.");
response.setStatus(SC_INTERNAL_SERVER_ERROR);
response.setPayload(String.format("Errored out with cause: %s", e));
}
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() {
logger.atInfo().log("Attempting to transfer %d domains.", bulkTransferDomainNames.size());
for (String domainName : bulkTransferDomainNames) {
rateLimiter.acquire();
tm().transact(() -> runTransferFlowInTransaction(domainName));
}
String msg =
String.format(
"Finished; %d domains were successfully transferred, %d were previously transferred, %s"
+ " were missing domains, %s are pending delete, and %d errored out.",
successes, alreadyTransferred, missingDomains, pendingDelete, errors);
logger.at(errors + missingDomains == 0 ? Level.INFO : Level.WARNING).log(msg);
response.setPayload(msg);
}
private void runTransferFlowInTransaction(String domainName) {
if (shouldSkipDomain(domainName)) {
return;
}
String xml =
SUPERUSER_TRANSFER_XML_FORMAT
.replace("%DOMAIN_NAME%", domainName)
.replace("%REASON%", reason)
.replace("%REQUESTED_BY_REGISTRAR%", String.valueOf(requestedByRegistrar));
EppOutput output =
eppController.handleEppCommand(
new StatelessRequestSessionMetadata(
gainingRegistrarId, ProtocolDefinition.getVisibleServiceExtensionUris()),
new PasswordOnlyTransportCredentials(),
EppRequestSource.TOOL,
false,
true,
xml.getBytes(US_ASCII));
if (output.isSuccess()) {
logger.atInfo().log("Successfully transferred domain '%s'.", domainName);
successes++;
} else {
logger.atWarning().log(
"Failed transferring domain '%s' with error '%s'.",
domainName, new String(marshalWithLenientRetry(output), US_ASCII));
errors++;
}
}
private boolean shouldSkipDomain(String domainName) {
Optional<Domain> maybeDomain =
ForeignKeyUtils.loadResource(Domain.class, domainName, tm().getTransactionTime());
if (maybeDomain.isEmpty()) {
logger.atWarning().log("Domain '%s' was already deleted", domainName);
missingDomains++;
return true;
}
Domain domain = maybeDomain.get();
String currentRegistrarId = domain.getCurrentSponsorRegistrarId();
if (currentRegistrarId.equals(gainingRegistrarId)) {
logger.atInfo().log("Domain '%s' was already transferred", domainName);
alreadyTransferred++;
return true;
}
if (!currentRegistrarId.equals(losingRegistrarId)) {
logger.atWarning().log(
"Domain '%s' had unexpected registrar '%s'", domainName, currentRegistrarId);
errors++;
return true;
}
if (domain.getStatusValues().contains(StatusValue.PENDING_DELETE)
|| !domain.getDeletionTime().equals(DateTimeUtils.END_OF_TIME)) {
logger.atWarning().log("Domain '%s' is in PENDING_DELETE", domainName);
pendingDelete++;
return true;
}
return false;
}
}

View File

@@ -55,8 +55,6 @@ import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.Optional;
import java.util.Random;
import java.util.function.BiConsumer;
import java.util.function.Consumer;
import java.util.function.Supplier;
import org.joda.time.Duration;
@@ -118,19 +116,13 @@ public class CloudTasksUtils implements Serializable {
* <p>For GET requests we add them on to the URL, and for POST requests we add them in the body of
* the request.
*
* <p>The parameters {@code putHeadersFunction} and {@code setBodyFunction} are used so that this
* method can be called with either an AppEngine HTTP request or a standard non-AppEngine HTTP
* request. The two objects do not have the same methods, but both have ways of setting headers /
* body.
*
* @return the resulting path (unchanged for POST requests, with params added for GET requests)
*/
private static String processRequestParameters(
String path,
Method method,
Multimap<String, String> params,
BiConsumer<String, String> putHeadersFunction,
Consumer<ByteString> setBodyFunction) {
HttpRequest.Builder requestBuilder) {
if (CollectionUtils.isNullOrEmpty(params)) {
return path;
}
@@ -148,8 +140,8 @@ public class CloudTasksUtils implements Serializable {
if (method.equals(Method.GET)) {
return String.format("%s?%s", path, encodedParams);
}
putHeadersFunction.accept(HttpHeaders.CONTENT_TYPE, MediaType.FORM_DATA.toString());
setBodyFunction.accept(ByteString.copyFrom(encodedParams, StandardCharsets.UTF_8));
requestBuilder.putHeaders(HttpHeaders.CONTENT_TYPE, MediaType.FORM_DATA.toString());
requestBuilder.setBody(ByteString.copyFrom(encodedParams, StandardCharsets.UTF_8));
return path;
}
@@ -160,18 +152,17 @@ public class CloudTasksUtils implements Serializable {
* default service account as the principal. That account must have permission to submit tasks to
* Cloud Tasks.
*
* <p>The caller of this method is responsible for passing in the appropriate service based on the
* runtime (GAE/GKE). Use the overload that takes an action class if possible.
* <p>The caller of this method is responsible for passing in the appropriate service. Use the
* overload that takes an action class if possible.
*
* @param path the relative URI (staring with a slash and ending without one).
* @param method the HTTP method to be used for the request.
* @param service the GAE/GKE service to route the request to.
* @param service the service to route the request to.
* @param params a multimap of URL query parameters. Duplicate keys are saved as is, and it is up
* to the server to process the duplicate keys.
* @return the enqueued task.
* @see <a
* href=ttps://cloud.google.com/appengine/docs/standard/java/taskqueue/push/creating-tasks#target>Specifyinig
* the worker service</a>
* @see <a href=https://docs.cloud.google.com/tasks/docs/creating-http-target-tasks#java>Creating
* HTTP target tasks</a>
*/
protected Task createTask(
String path, Method method, Action.Service service, Multimap<String, String> params) {
@@ -180,9 +171,7 @@ public class CloudTasksUtils implements Serializable {
"The path must start with a '/'.");
HttpRequest.Builder requestBuilder =
HttpRequest.newBuilder().setHttpMethod(HttpMethod.valueOf(method.name()));
path =
processRequestParameters(
path, method, params, requestBuilder::putHeaders, requestBuilder::setBody);
path = processRequestParameters(path, method, params, requestBuilder);
OidcToken.Builder oidcTokenBuilder =
OidcToken.newBuilder()
.setServiceAccountEmail(credential.serviceAccount())
@@ -204,16 +193,15 @@ public class CloudTasksUtils implements Serializable {
* Cloud Tasks.
*
* <p>Prefer this overload over the one where the path and service are explicitly defined, as this
* class will automatically determine the service to use based on the action and the runtime.
* class will automatically determine the service to use based on the action.
*
* @param actionClazz the action class to run, must be annotated with {@link Action}.
* @param method the HTTP method to be used for the request.
* @param params a multimap of URL query parameters. Duplicate keys are saved as is, and it is up
* to the server to process the duplicate keys.
* @return the enqueued task.
* @see <a
* href=ttps://cloud.google.com/appengine/docs/standard/java/taskqueue/push/creating-tasks#target>Specifyinig
* the worker service</a>
* @see <a href=https://docs.cloud.google.com/tasks/docs/creating-http-target-tasks#java>Creating
* HTTP target tasks</a>
*/
public Task createTask(
Class<? extends Runnable> actionClazz, Method method, Multimap<String, String> params) {
@@ -236,19 +224,18 @@ public class CloudTasksUtils implements Serializable {
/**
* Create a {@link Task} to be enqueued with a random delay up to {@code jitterSeconds}.
*
* <p>The caller of this method is responsible for passing in the appropriate service based on the
* runtime (GAE/GKE). Use the overload that takes an action class if possible.
* <p>The caller of this method is responsible for passing in the appropriate service. Use the
* overload that takes an action class if possible.
*
* @param path the relative URI (staring with a slash and ending without one).
* @param method the HTTP method to be used for the request.
* @param service the GAE/GKE service to route the request to.
* @param service the service to route the request to.
* @param params a multimap of URL query parameters. Duplicate keys are saved as is, and it is up
* to the server to process the duplicate keys.
* @param jitterSeconds the number of seconds that a task is randomly delayed up to.
* @return the enqueued task.
* @see <a
* href=ttps://cloud.google.com/appengine/docs/standard/java/taskqueue/push/creating-tasks#target>Specifyinig
* the worker service</a>
* @see <a href=https://docs.cloud.google.com/tasks/docs/creating-http-target-tasks#java>Creating
* HTTP target tasks</a>
*/
public Task createTaskWithJitter(
String path,
@@ -271,7 +258,7 @@ public class CloudTasksUtils implements Serializable {
* Create a {@link Task} to be enqueued with a random delay up to {@code jitterSeconds}.
*
* <p>Prefer this overload over the one where the path and service are explicitly defined, as this
* class will automatically determine the service to use based on the action and the runtime.
* class will automatically determine the service to use based on the action.
*
* @param actionClazz the action class to run, must be annotated with {@link Action}.
* @param method the HTTP method to be used for the request.
@@ -279,9 +266,8 @@ public class CloudTasksUtils implements Serializable {
* to the server to process the duplicate keys.
* @param jitterSeconds the number of seconds that a task is randomly delayed up to.
* @return the enqueued task.
* @see <a
* href=ttps://cloud.google.com/appengine/docs/standard/java/taskqueue/push/creating-tasks#target>Specifyinig
* the worker service</a>
* @see <a href=https://docs.cloud.google.com/tasks/docs/creating-http-target-tasks#java>Creating
* HTTP target tasks</a>
*/
public Task createTaskWithJitter(
Class<? extends Runnable> actionClazz,
@@ -302,14 +288,13 @@ public class CloudTasksUtils implements Serializable {
*
* @param path the relative URI (staring with a slash and ending without one).
* @param method the HTTP method to be used for the request.
* @param service the GAE/GKE service to route the request to.
* @param service the service to route the request to.
* @param params a multimap of URL query parameters. Duplicate keys are saved as is, and it is up
* to the server to process the duplicate keys.
* @param delay the amount of time that a task needs to be delayed for.
* @return the enqueued task.
* @see <a
* href=ttps://cloud.google.com/appengine/docs/standard/java/taskqueue/push/creating-tasks#target>Specifyinig
* the worker service</a>
* @see <a href=https://docs.cloud.google.com/tasks/docs/creating-http-target-tasks#java>Creating
* HTTP target tasks</a>
*/
private Task createTaskWithDelay(
String path,
@@ -330,7 +315,7 @@ public class CloudTasksUtils implements Serializable {
* Create a {@link Task} to be enqueued with delay of {@code duration}.
*
* <p>Prefer this overload over the one where the path and service are explicitly defined, as this
* class will automatically determine the service to use based on the action and the runtime.
* class will automatically determine the service to use based on the action.
*
* @param actionClazz the action class to run, must be annotated with {@link Action}.
* @param method the HTTP method to be used for the request.
@@ -338,9 +323,8 @@ public class CloudTasksUtils implements Serializable {
* to the server to process the duplicate keys.
* @param delay the amount of time that a task needs to be delayed for.
* @return the enqueued task.
* @see <a
* href=ttps://cloud.google.com/appengine/docs/standard/java/taskqueue/push/creating-tasks#target>Specifyinig
* the worker service</a>
* @see <a href=https://docs.cloud.google.com/tasks/docs/creating-http-target-tasks#java>Creating
* HTTP target tasks</a>
*/
public Task createTaskWithDelay(
Class<? extends Runnable> actionClazz,

View File

@@ -112,11 +112,11 @@ public class RelockDomainAction implements Runnable {
public void run() {
/* We wish to manually control our retry behavior, in order to limit the number of retries
* and/or notify registrars / support only after a certain number of retries, or only
* with a certain type of failure. AppEngine will automatically retry on any non-2xx status
* with a certain type of failure. Cloud Tasks will automatically retry on any non-2xx status
* code, so return SC_NO_CONTENT (204) by default to avoid this auto-retry.
*
* See https://cloud.google.com/appengine/docs/standard/java/taskqueue/push/retrying-tasks
* for more details on retry behavior. */
* See https://docs.cloud.google.com/tasks/docs/configuring-queues#retry for more details on
* retry behavior. */
response.setStatus(SC_NO_CONTENT);
response.setContentType(MediaType.PLAIN_TEXT_UTF_8);
tm().transact(this::relockDomain);

View File

@@ -14,10 +14,8 @@
package google.registry.batch;
import static com.google.common.base.Preconditions.checkState;
import static com.google.common.net.MediaType.PLAIN_TEXT_UTF_8;
import static google.registry.flows.FlowUtils.marshalWithLenientRetry;
import static google.registry.model.common.FeatureFlag.FeatureName.MINIMUM_DATASET_CONTACTS_PROHIBITED;
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
import static google.registry.util.DateTimeUtils.END_OF_TIME;
import static google.registry.util.ResourceUtils.readResourceUtf8;
@@ -36,7 +34,6 @@ import google.registry.flows.EppController;
import google.registry.flows.EppRequestSource;
import google.registry.flows.PasswordOnlyTransportCredentials;
import google.registry.flows.StatelessRequestSessionMetadata;
import google.registry.model.common.FeatureFlag;
import google.registry.model.contact.Contact;
import google.registry.model.domain.DesignatedContact;
import google.registry.model.domain.Domain;
@@ -93,7 +90,7 @@ public class RemoveAllDomainContactsAction implements Runnable {
EppController eppController,
@Config("registryAdminClientId") String registryAdminClientId,
LockHandler lockHandler,
@Named("removeAllDomainContacts") RateLimiter rateLimiter,
@Named("standardRateLimiter") RateLimiter rateLimiter,
Response response) {
this.eppController = eppController;
this.registryAdminClientId = registryAdminClientId;
@@ -106,11 +103,7 @@ public class RemoveAllDomainContactsAction implements Runnable {
@Override
public void run() {
checkState(
tm().transact(() -> FeatureFlag.isActiveNow(MINIMUM_DATASET_CONTACTS_PROHIBITED)),
"Minimum dataset migration must be completed prior to running this action");
response.setContentType(PLAIN_TEXT_UTF_8);
Callable<Void> runner =
() -> {
try {

View File

@@ -40,8 +40,6 @@ public class RegistryPipelineWorkerInitializer implements JvmInitializer {
@Override
public void beforeProcessing(PipelineOptions options) {
// TODO(b/416299900): remove next line after GAE is removed.
System.setProperty("google.registry.jetty", "true");
RegistryPipelineOptions registryOptions = options.as(RegistryPipelineOptions.class);
RegistryEnvironment environment = registryOptions.getRegistryEnvironment();
if (environment == null || environment.equals(RegistryEnvironment.UNITTEST)) {

View File

@@ -279,20 +279,6 @@ public class BigqueryConnection implements AutoCloseable {
private TableReference getTableReference() {
return table.getTableReference().clone();
}
/** Returns a string representation of the TableReference for the wrapped table. */
public String getStringReference() {
return tableReferenceToString(table.getTableReference());
}
/** Returns a string representation of the given TableReference. */
private static String tableReferenceToString(TableReference tableRef) {
return String.format(
"%s:%s.%s",
tableRef.getProjectId(),
tableRef.getDatasetId(),
tableRef.getTableId());
}
}
/**
@@ -398,29 +384,12 @@ public class BigqueryConnection implements AutoCloseable {
}
/**
* Starts an asynchronous query job to dump the results of the specified query into a local
* ImmutableTable object, row-keyed by the row number (indexed from 1), column-keyed by the
* TableFieldSchema for that column, and with the value object as the cell value. Note that null
* values will not actually be null, but they can be checked for using Data.isNull().
* Dumps the results of the specified query into a local ImmutableTable object, row-keyed by the
* row number (indexed from 1), column-keyed by the TableFieldSchema for that column, and with the
* value object as the cell value.
*
* <p>Returns a ListenableFuture that holds the ImmutableTable on success.
*/
public ListenableFuture<ImmutableTable<Integer, TableFieldSchema, Object>>
queryToLocalTable(String querySql) {
Job job = new Job()
.setConfiguration(new JobConfiguration()
.setQuery(new JobConfigurationQuery()
.setQuery(querySql)
.setDefaultDataset(getDataset())));
return transform(runJobToCompletion(job), this::getQueryResults, directExecutor());
}
/**
* Returns the result of calling queryToLocalTable, but synchronously to avoid spawning new
* background threads, which App Engine doesn't support.
*
* @see <a href="https://cloud.google.com/appengine/docs/standard/java/runtime#Threads">App Engine
* Runtime</a>
* <p>Note that null values will not actually be null, but they can be checked for using
* Data.isNull()
*/
public ImmutableTable<Integer, TableFieldSchema, Object> queryToLocalTableSync(String querySql) {
Job job = new Job()
@@ -634,10 +603,6 @@ public class BigqueryConnection implements AutoCloseable {
});
}
private ListenableFuture<Job> runJobToCompletion(final Job job) {
return service.submit(() -> runJob(job, null));
}
/** Helper that returns true if a dataset with this name exists. */
public boolean checkDatasetExists(String datasetName) throws IOException {
try {
@@ -676,14 +641,6 @@ public class BigqueryConnection implements AutoCloseable {
.setDatasetId(getDatasetId());
}
/** Returns table reference with the projectId and datasetId filled out for you. */
public TableReference getTable(String tableName) {
return new TableReference()
.setProjectId(getProjectId())
.setDatasetId(getDatasetId())
.setTableId(tableName);
}
/**
* Helper that creates a dataset with this name if it doesn't already exist, and returns true if
* creation took place.

View File

@@ -71,9 +71,7 @@ class BsaDiffCreator {
Optional<String> previousJobName = schedule.latestCompleted().map(CompletedJob::jobName);
/*
* Memory usage is a concern when creating a diff, when the newest download needs to be held in
* memory in its entirety. The top-grade AppEngine VM has 3GB of memory, leaving less than 1.5GB
* to application memory footprint after subtracting overheads due to copying garbage collection
* and non-heap data etc. Assuming 400K labels, each of which on average included in 5 orders,
* memory in its entirety. Assuming 400K labels, each of which on average included in 5 orders,
* the memory footprint is at least 300MB when loaded into a Hashset-backed Multimap (64-bit
* JVM, with 12-byte object header, 16-byte array header, and 16-byte alignment).
*

View File

@@ -44,8 +44,6 @@ public abstract class CredentialModule {
* <p>The credential returned by the Cloud Runtime depends on the runtime environment:
*
* <ul>
* <li>On App Engine, returns a scope-less {@code ComputeEngineCredentials} for
* PROJECT_ID@appspot.gserviceaccount.com
* <li>On Compute Engine, returns a scope-less {@code ComputeEngineCredentials} for
* PROJECT_NUMBER-compute@developer.gserviceaccount.com
* <li>On end user host, this returns the credential downloaded by gcloud. Please refer to <a
@@ -87,8 +85,8 @@ public abstract class CredentialModule {
* the application default credential user.
*
* <p>The Workspace domain must grant delegated admin access to the default service account user
* (project-id@appspot.gserviceaccount.com on AppEngine) with all scopes in {@code defaultScopes}
* and {@code delegationScopes}.
* (nomulus-service-account@{project-id}.iam.gserviceaccount.com on GCP) with all scopes in {@code
* defaultScopes} and {@code delegationScopes}.
*/
@AdcDelegatedCredential
@Provides
@@ -113,9 +111,9 @@ public abstract class CredentialModule {
* Provides a {@link GoogleCredentialsBundle} for sending emails through Google Workspace.
*
* <p>The Workspace domain must grant delegated admin access to the default service account user
* (project-id@appspot.gserviceaccount.com on AppEngine) with all scopes in {@code defaultScopes}
* and {@code delegationScopes}. In addition, the user {@code gSuiteOutgoingEmailAddress} must
* have the permission to send emails.
* (nomulus-service-account@{project-id}.iam.gserviceaccount.com on GCP) with all scopes in {@code
* defaultScopes} and {@code delegationScopes}. In addition, the user {@code
* gSuiteOutgoingEmailAddress} must have the permission to send emails.
*/
@GmailDelegatedCredential
@Provides

View File

@@ -55,8 +55,9 @@ import org.apache.commons.codec.binary.Base64;
*
* <p>This class accepts the application-default-credential as {@code ServiceAccountSigner},
* avoiding the need for exported private keys. In this case, the default credential user itself
* (project-id@appspot.gserviceaccount.com on AppEngine) must have domain-wide delegation to the
* Workspace APIs. The default credential user also must have the Token Creator role to itself.
* (nomulus-service-account@{project-id}.iam.gserviceaccount.com on GCP) must have domain-wide
* delegation to the Workspace APIs. The default credential user also must have the Token Creator
* role to itself.
*
* <p>If the user provides a credential {@code S} that carries its own private key, such as {@link
* com.google.auth.oauth2.ServiceAccountCredentials}, this class can use {@code S} to impersonate

View File

@@ -36,6 +36,7 @@ import dagger.Provides;
import google.registry.bsa.UploadBsaUnavailableDomainsAction;
import google.registry.dns.ReadDnsRefreshRequestsAction;
import google.registry.model.common.DnsRefreshRequest;
import google.registry.mosapi.MosApiClient;
import google.registry.persistence.transaction.JpaTransactionManager;
import google.registry.request.Action.Service;
import google.registry.util.RegistryEnvironment;
@@ -961,7 +962,7 @@ public final class RegistryConfig {
}
/**
* Number of times to retry a GAE operation when {@code TransientFailureException} is thrown.
* Number of times to retry an operation when {@code TransientFailureException} is thrown.
*
* <p>The number of milliseconds it'll sleep before giving up is {@code (2^n - 2) * 100}.
*
@@ -1415,6 +1416,52 @@ public final class RegistryConfig {
return config.bsa.uploadUnavailableDomainsUrl;
}
/**
* Returns the URL we send HTTP requests for MoSAPI.
*
* @see MosApiClient
*/
@Provides
@Config("mosapiServiceUrl")
public static String provideMosapiServiceUrl(RegistryConfigSettings config) {
return config.mosapi.serviceUrl;
}
/**
* Returns the entityType we send HTTP requests for MoSAPI.
*
* @see MosApiClient
*/
@Provides
@Config("mosapiEntityType")
public static String provideMosapiEntityType(RegistryConfigSettings config) {
return config.mosapi.entityType;
}
@Provides
@Config("mosapiTlsCertSecretName")
public static String provideMosapiTlsCertSecretName(RegistryConfigSettings config) {
return config.mosapi.tlsCertSecretName;
}
@Provides
@Config("mosapiTlsKeySecretName")
public static String provideMosapiTlsKeySecretName(RegistryConfigSettings config) {
return config.mosapi.tlsKeySecretName;
}
@Provides
@Config("mosapiTlds")
public static ImmutableSet<String> provideMosapiTlds(RegistryConfigSettings config) {
return ImmutableSet.copyOf(config.mosapi.tlds);
}
@Provides
@Config("mosapiServices")
public static ImmutableSet<String> provideMosapiServices(RegistryConfigSettings config) {
return ImmutableSet.copyOf(config.mosapi.services);
}
private static String formatComments(String text) {
return Splitter.on('\n').omitEmptyStrings().trimResults().splitToList(text).stream()
.map(s -> "# " + s)
@@ -1422,7 +1469,7 @@ public final class RegistryConfig {
}
}
/** Returns the App Engine project ID, which is based off the environment name. */
/** Returns the project ID, which is based off the environment name. */
public static String getProjectId() {
return CONFIG_SETTINGS.get().gcpProject.projectId;
}
@@ -1448,51 +1495,6 @@ public final class RegistryConfig {
return makeUrl(String.format("https://%s.%s", service.getServiceId(), getBaseDomain()));
}
/**
* Returns the address of the Nomulus app default HTTP server.
*
* <p>This is used by the {@code nomulus} tool to connect to the App Engine remote API.
*/
public static URL getDefaultServer() {
return makeUrl(CONFIG_SETTINGS.get().gcpProject.defaultServiceUrl);
}
/**
* Returns the address of the Nomulus app backend HTTP server.
*
* <p>This is used by the {@code nomulus} tool to connect to the App Engine remote API.
*/
public static URL getBackendServer() {
return makeUrl(CONFIG_SETTINGS.get().gcpProject.backendServiceUrl);
}
/**
* Returns the address of the Nomulus app bsa HTTP server.
*
* <p>This is used by the {@code nomulus} tool to connect to the App Engine remote API.
*/
public static URL getBsaServer() {
return makeUrl(CONFIG_SETTINGS.get().gcpProject.bsaServiceUrl);
}
/**
* Returns the address of the Nomulus app tools HTTP server.
*
* <p>This is used by the {@code nomulus} tool to connect to the App Engine remote API.
*/
public static URL getToolsServer() {
return makeUrl(CONFIG_SETTINGS.get().gcpProject.toolsServiceUrl);
}
/**
* Returns the address of the Nomulus app pubapi HTTP server.
*
* <p>This is used by the {@code nomulus} tool to connect to the App Engine remote API.
*/
public static URL getPubapiServer() {
return makeUrl(CONFIG_SETTINGS.get().gcpProject.pubapiServiceUrl);
}
/** Returns the amount of time a singleton should be cached, before expiring. */
public static java.time.Duration getSingletonCacheRefreshDuration() {
return java.time.Duration.ofSeconds(CONFIG_SETTINGS.get().caching.singletonCacheRefreshSeconds);

View File

@@ -43,6 +43,7 @@ public class RegistryConfigSettings {
public DnsUpdate dnsUpdate;
public BulkPricingPackageMonitoring bulkPricingPackageMonitoring;
public Bsa bsa;
public MosApi mosapi;
/** Configuration options that apply to the entire GCP project. */
public static class GcpProject {
@@ -50,11 +51,6 @@ public class RegistryConfigSettings {
public long projectIdNumber;
public String locationId;
public boolean isLocal;
public String defaultServiceUrl;
public String backendServiceUrl;
public String bsaServiceUrl;
public String toolsServiceUrl;
public String pubapiServiceUrl;
public String baseDomain;
}
@@ -267,4 +263,14 @@ public class RegistryConfigSettings {
public String unblockableDomainsUrl;
public String uploadUnavailableDomainsUrl;
}
/** Configuration for Mosapi. */
public static class MosApi {
public String serviceUrl;
public String tlsCertSecretName;
public String tlsKeySecretName;
public String entityType;
public List<String> tlds;
public List<String> services;
}
}

View File

@@ -12,17 +12,11 @@ gcpProject:
projectIdNumber: 123456789012
# Location of the GCP project, note that us-central1 and europe-west1 are special in that
# they are used without the trailing number in GCP commands and Google Cloud Console.
# See: https://cloud.google.com/appengine/docs/locations as an example
# See: https://docs.cloud.google.com/compute/docs/regions-zones as an example
locationId: registry-location-id
# whether to use local/test credentials when connecting to the servers
isLocal: true
# URLs of the services for the project.
defaultServiceUrl: https://default.example.com
backendServiceUrl: https://backend.example.com
bsaServiceUrl: https://bsa.example.com
toolsServiceUrl: https://tools.example.com
pubapiServiceUrl: https://pubapi.example.com
# The base domain name of the registry service. Services are reachable at [service].baseDomain.
baseDomain: registry.test
@@ -32,9 +26,9 @@ gSuite:
domainName: domain-registry.example
# Display name and email address used on outgoing emails through G Suite.
# The email address must be valid and have permission in the GAE app to send
# emails. For more info see:
# https://cloud.google.com/appengine/docs/standard/java/mail/#who_can_send_mail
# The email address must be valid and the domain must be set up to send emails.
# For more info see
# https://docs.cloud.google.com/compute/docs/tutorials/sending-mail
outgoingEmailDisplayName: Example Registry
outgoingEmailAddress: noreply@project-id.appspotmail.com
# TODO(b/279671974): reuse `outgoingEmailAddress` after migration
@@ -201,18 +195,16 @@ hibernate:
# but lock tables explicitly, either using framework-dependent API, or execute
# "select table for update" statements directly.
connectionIsolation: TRANSACTION_SERIALIZABLE
# Whether to log all SQL queries to App Engine logs. Overridable at runtime.
# Whether to log all SQL queries. Overridable at runtime.
logSqlQueries: false
# Connection pool configurations.
hikariConnectionTimeout: 20000
# Cloud SQL connections are a relatively scarce resource (maximum is 1000 as
# of March 2021). The minimumIdle should be a small value so that machines may
# release connections after a demand spike. The maximumPoolSize is set to 10
# because that is the maximum number of concurrent requests a Nomulus server
# instance can handle (as limited by AppEngine for basic/manual scaling). Note
# that BEAM pipelines are not subject to the maximumPoolSize value defined
# here. See PersistenceModule.java for more information.
# release connections after a demand spike. Note that BEAM pipelines are not
# subject to the maximumPoolSize value defined here. See PersistenceModule.java
# for more information.
hikariMinimumIdle: 1
hikariMaximumPoolSize: 40
hikariIdleTimeout: 300000
@@ -264,8 +256,8 @@ caching:
# Maximum total number of static premium list entry entities to cache in
# memory, across all premium lists for all TLDs. Tuning this up will use more
# memory (and might require using larger App Engine instances). Note that
# premium list entries that are absent are cached in addition to ones that are
# memory (and might require using larger instances). Note that premium list
# entries that are absent are cached in addition to ones that are
# present, so the total cache size is not bounded by the total number of
# premium price entries that exist.
staticPremiumListMaxCachedEntries: 200000
@@ -346,12 +338,8 @@ credentialOAuth:
localCredentialOauthScopes:
# View and manage data in all Google Cloud APIs.
- https://www.googleapis.com/auth/cloud-platform
# Call App Engine APIs locally.
- https://www.googleapis.com/auth/appengine.apis
# View your email address.
- https://www.googleapis.com/auth/userinfo.email
# View and manage your applications deployed on Google App Engine
- https://www.googleapis.com/auth/appengine.admin
# The lifetime of an access token generated by our custom credentials classes
# Must be shorter than one hour.
tokenRefreshDelaySeconds: 1800
@@ -433,7 +421,7 @@ misc:
spec11BccEmailAddresses:
- abuse@example.com
# Number of times to retry a GAE operation when a transient exception is thrown.
# Number of times to retry an operation when a transient exception is thrown.
# The number of milliseconds it'll sleep before giving up is (2^n - 2) * 100.
transientFailureRetries: 12
@@ -628,3 +616,30 @@ bsa:
unblockableDomainsUrl: "https://"
# API endpoint for uploading the list of unavailable domain names.
uploadUnavailableDomainsUrl: "https://"
mosapi:
# URL for the MosAPI
serviceUrl: https://mosapi.icann.org
# The type of entity being monitored.
# For registries, this is 'ry'
# For registrars, this is 'rr'
entityType: ry
# Add your List of TLDs to be monitored
tlds:
- your_tld1
- your_tld2
# Add tls cert secret name
# you configured in secret manager
tlsCertSecretName: YOUR_TLS_CERT_SECRET_NAME
# Add tls key secret name
# you configured in secret manager
tlsKeySecretName: YOUR_TLS_KEY_SECRET_NAME
# List of services to check for each TLD.
services:
- "dns"
- "rdap"
- "rdds"
- "epp"
- "dnssec"

View File

@@ -57,8 +57,7 @@ import java.util.stream.Stream;
*
* <ul>
* <li>{@code endpoint} (Required) URL path of servlet to launch. This may contain pathargs.
* <li>{@code queue} (Required) Name of the App Engine push queue to which this task should be
* sent.
* <li>{@code queue} (Required) Name of the queue to which this task should be sent.
* <li>{@code forEachRealTld} Launch the task in each real TLD namespace.
* <li>{@code forEachTestTld} Launch the task in each test TLD namespace.
* <li>{@code runInEmpty} Launch the task once, without the TLD argument.

View File

@@ -36,8 +36,10 @@ import org.xbill.DNS.Opcode;
/**
* A transport for DNS messages. Sends/receives DNS messages over TCP using old-style {@link Socket}
* s and the message framing defined in <a href="https://tools.ietf.org/html/rfc1035">RFC 1035</a>.
* We would like use the dnsjava library's {@link org.xbill.DNS.SimpleResolver} class for this, but
* it requires {@link java.nio.channels.SocketChannel} which is not supported on AppEngine.
*
* <p>TODO(b/463732345): now that we're no longer on AppEngine, see if we can use the dnsjava
* library's {@link org.xbill.DNS.SimpleResolver} class instead of this (that requires {@link
* java.nio.channels.SocketChannel} which is not supported on AppEngine).
*/
public class DnsMessageTransport {

View File

@@ -51,7 +51,7 @@ public class FlowReporter {
@Inject Class<? extends Flow> flowClass;
@Inject FlowReporter() {}
/** Records information about the current flow execution in the GAE request logs. */
/** Records information about the current flow execution in the request logs. */
public void recordToLogs() {
// Explicitly log flow metadata separately from the EPP XML itself so that it stays compact
// enough to be sure to fit in a single log entry (the XML part in rare cases could be long

View File

@@ -73,7 +73,7 @@ public class FlowRunner {
eppRequestSource,
isDryRun ? "DRY_RUN" : "LIVE",
isSuperuser ? "SUPERUSER" : "NORMAL");
// Record flow info to the GAE request logs for reporting purposes if it's not a dry run.
// Record flow info to the request logs for reporting purposes if it's not a dry run.
if (!isDryRun) {
flowReporter.recordToLogs();
}

View File

@@ -14,60 +14,19 @@
package google.registry.flows.contact;
import static google.registry.flows.FlowUtils.validateRegistrarIsLoggedIn;
import static google.registry.flows.ResourceFlowUtils.verifyTargetIdCount;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import google.registry.config.RegistryConfig.Config;
import google.registry.flows.EppException;
import google.registry.flows.ExtensionManager;
import google.registry.flows.FlowModule.RegistrarId;
import google.registry.flows.TransactionalFlow;
import google.registry.flows.annotations.ReportingSpec;
import google.registry.model.ForeignKeyUtils;
import google.registry.model.contact.Contact;
import google.registry.model.contact.ContactCommand.Check;
import google.registry.model.eppinput.ResourceCommand;
import google.registry.model.eppoutput.CheckData.ContactCheck;
import google.registry.model.eppoutput.CheckData.ContactCheckData;
import google.registry.model.eppoutput.EppResponse;
import google.registry.flows.exceptions.ContactsProhibitedException;
import google.registry.model.reporting.IcannReportingTypes.ActivityReportField;
import google.registry.util.Clock;
import jakarta.inject.Inject;
/**
* An EPP flow that checks whether a contact can be provisioned.
* An EPP flow that is meant to check whether a contact can be provisioned.
*
* <p>This flows can check the existence of multiple contacts simultaneously.
*
* @error {@link google.registry.flows.exceptions.TooManyResourceChecksException}
* @error {@link google.registry.flows.FlowUtils.NotLoggedInException}
* @error {@link ContactsProhibitedException}
*/
@Deprecated
@ReportingSpec(ActivityReportField.CONTACT_CHECK)
public final class ContactCheckFlow implements TransactionalFlow {
@Inject ResourceCommand resourceCommand;
@Inject @RegistrarId String registrarId;
@Inject ExtensionManager extensionManager;
@Inject Clock clock;
@Inject @Config("maxChecks") int maxChecks;
@Inject EppResponse.Builder responseBuilder;
public final class ContactCheckFlow extends ContactsProhibitedFlow {
@Inject ContactCheckFlow() {}
@Override
public EppResponse run() throws EppException {
validateRegistrarIsLoggedIn(registrarId);
extensionManager.validate(); // There are no legal extensions for this flow.
ImmutableList<String> targetIds = ((Check) resourceCommand).getTargetIds();
verifyTargetIdCount(targetIds, maxChecks);
ImmutableSet<String> existingIds =
ForeignKeyUtils.loadKeys(Contact.class, targetIds, clock.nowUtc()).keySet();
ImmutableList.Builder<ContactCheck> checks = new ImmutableList.Builder<>();
for (String id : targetIds) {
boolean unused = !existingIds.contains(id);
checks.add(ContactCheck.create(unused, id, unused ? null : "In use"));
}
return responseBuilder.setResData(ContactCheckData.create(checks.build())).build();
}
}

View File

@@ -14,94 +14,19 @@
package google.registry.flows.contact;
import static google.registry.flows.FlowUtils.validateRegistrarIsLoggedIn;
import static google.registry.flows.ResourceFlowUtils.verifyResourceDoesNotExist;
import static google.registry.flows.contact.ContactFlowUtils.validateAsciiPostalInfo;
import static google.registry.flows.contact.ContactFlowUtils.validateContactAgainstPolicy;
import static google.registry.model.EppResourceUtils.createRepoId;
import static google.registry.model.common.FeatureFlag.FeatureName.MINIMUM_DATASET_CONTACTS_PROHIBITED;
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
import com.google.common.collect.ImmutableSet;
import google.registry.config.RegistryConfig.Config;
import google.registry.flows.EppException;
import google.registry.flows.ExtensionManager;
import google.registry.flows.FlowModule.RegistrarId;
import google.registry.flows.FlowModule.TargetId;
import google.registry.flows.MutatingFlow;
import google.registry.flows.annotations.ReportingSpec;
import google.registry.flows.exceptions.ContactsProhibitedException;
import google.registry.flows.exceptions.ResourceAlreadyExistsForThisClientException;
import google.registry.flows.exceptions.ResourceCreateContentionException;
import google.registry.model.common.FeatureFlag;
import google.registry.model.contact.Contact;
import google.registry.model.contact.ContactCommand.Create;
import google.registry.model.contact.ContactHistory;
import google.registry.model.domain.metadata.MetadataExtension;
import google.registry.model.eppinput.ResourceCommand;
import google.registry.model.eppoutput.CreateData.ContactCreateData;
import google.registry.model.eppoutput.EppResponse;
import google.registry.model.reporting.HistoryEntry;
import google.registry.model.reporting.IcannReportingTypes.ActivityReportField;
import jakarta.inject.Inject;
import org.joda.time.DateTime;
/**
* An EPP flow that creates a new contact.
* An EPP flow meant to create a new contact.
*
* @error {@link google.registry.flows.FlowUtils.NotLoggedInException}
* @error {@link ContactsProhibitedException}
* @error {@link ResourceAlreadyExistsForThisClientException}
* @error {@link ResourceCreateContentionException}
* @error {@link ContactFlowUtils.BadInternationalizedPostalInfoException}
* @error {@link ContactFlowUtils.DeclineContactDisclosureFieldDisallowedPolicyException}
*/
@Deprecated
@ReportingSpec(ActivityReportField.CONTACT_CREATE)
public final class ContactCreateFlow implements MutatingFlow {
@Inject ResourceCommand resourceCommand;
@Inject ExtensionManager extensionManager;
@Inject @RegistrarId String registrarId;
@Inject @TargetId String targetId;
@Inject ContactHistory.Builder historyBuilder;
@Inject EppResponse.Builder responseBuilder;
@Inject @Config("contactAndHostRoidSuffix") String roidSuffix;
public final class ContactCreateFlow extends ContactsProhibitedFlow {
@Inject ContactCreateFlow() {}
@Override
public EppResponse run() throws EppException {
extensionManager.register(MetadataExtension.class);
validateRegistrarIsLoggedIn(registrarId);
extensionManager.validate();
if (FeatureFlag.isActiveNow(MINIMUM_DATASET_CONTACTS_PROHIBITED)) {
throw new ContactsProhibitedException();
}
Create command = (Create) resourceCommand;
DateTime now = tm().getTransactionTime();
verifyResourceDoesNotExist(Contact.class, targetId, now, registrarId);
Contact newContact =
new Contact.Builder()
.setContactId(targetId)
.setAuthInfo(command.getAuthInfo())
.setCreationRegistrarId(registrarId)
.setPersistedCurrentSponsorRegistrarId(registrarId)
.setRepoId(createRepoId(tm().allocateId(), roidSuffix))
.setFaxNumber(command.getFax())
.setVoiceNumber(command.getVoice())
.setDisclose(command.getDisclose())
.setEmailAddress(command.getEmail())
.setInternationalizedPostalInfo(command.getInternationalizedPostalInfo())
.setLocalizedPostalInfo(command.getLocalizedPostalInfo())
.build();
validateAsciiPostalInfo(newContact.getInternationalizedPostalInfo());
validateContactAgainstPolicy(newContact);
historyBuilder
.setType(HistoryEntry.Type.CONTACT_CREATE)
.setXmlBytes(null) // We don't want to store contact details in the history entry.
.setContact(newContact);
tm().insertAll(ImmutableSet.of(newContact, historyBuilder.build()));
return responseBuilder
.setResData(ContactCreateData.create(newContact.getContactId(), now))
.build();
}
}

View File

@@ -14,97 +14,20 @@
package google.registry.flows.contact;
import static google.registry.flows.FlowUtils.DELETE_PROHIBITED_STATUSES;
import static google.registry.flows.FlowUtils.validateRegistrarIsLoggedIn;
import static google.registry.flows.ResourceFlowUtils.checkLinkedDomains;
import static google.registry.flows.ResourceFlowUtils.loadAndVerifyExistence;
import static google.registry.flows.ResourceFlowUtils.verifyNoDisallowedStatuses;
import static google.registry.flows.ResourceFlowUtils.verifyOptionalAuthInfo;
import static google.registry.flows.ResourceFlowUtils.verifyResourceOwnership;
import static google.registry.model.ResourceTransferUtils.denyPendingTransfer;
import static google.registry.model.ResourceTransferUtils.handlePendingTransferOnDelete;
import static google.registry.model.eppoutput.Result.Code.SUCCESS;
import static google.registry.model.transfer.TransferStatus.SERVER_CANCELLED;
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
import com.google.common.collect.ImmutableSet;
import google.registry.flows.EppException;
import google.registry.flows.ExtensionManager;
import google.registry.flows.FlowModule.RegistrarId;
import google.registry.flows.FlowModule.Superuser;
import google.registry.flows.FlowModule.TargetId;
import google.registry.flows.MutatingFlow;
import google.registry.flows.annotations.ReportingSpec;
import google.registry.model.contact.Contact;
import google.registry.model.contact.ContactHistory;
import google.registry.model.domain.metadata.MetadataExtension;
import google.registry.model.eppcommon.AuthInfo;
import google.registry.model.eppcommon.StatusValue;
import google.registry.model.eppcommon.Trid;
import google.registry.model.eppoutput.EppResponse;
import google.registry.model.reporting.HistoryEntry.Type;
import google.registry.flows.exceptions.ContactsProhibitedException;
import google.registry.model.reporting.IcannReportingTypes.ActivityReportField;
import jakarta.inject.Inject;
import java.util.Optional;
import org.joda.time.DateTime;
/**
* An EPP flow that deletes a contact.
* An EPP flow that is meant to delete a contact.
*
* <p>Contacts that are in use by any domain cannot be deleted. The flow may return immediately if a
* quick smoke check determines that deletion is impossible due to an existing reference. However, a
* successful delete will always be asynchronous, as all existing domains must be checked for
* references to the host before the deletion is allowed to proceed. A poll message will be written
* with the success or failure message when the process is complete.
*
* @error {@link google.registry.flows.FlowUtils.NotLoggedInException}
* @error {@link google.registry.flows.ResourceFlowUtils.ResourceDoesNotExistException}
* @error {@link google.registry.flows.ResourceFlowUtils.ResourceNotOwnedException}
* @error {@link google.registry.flows.exceptions.ResourceStatusProhibitsOperationException}
* @error {@link google.registry.flows.exceptions.ResourceToDeleteIsReferencedException}
* @error {@link ContactsProhibitedException}
*/
@Deprecated
@ReportingSpec(ActivityReportField.CONTACT_DELETE)
public final class ContactDeleteFlow implements MutatingFlow {
@Inject ExtensionManager extensionManager;
@Inject @RegistrarId String registrarId;
@Inject @TargetId String targetId;
@Inject Trid trid;
@Inject @Superuser boolean isSuperuser;
@Inject Optional<AuthInfo> authInfo;
@Inject ContactHistory.Builder historyBuilder;
@Inject EppResponse.Builder responseBuilder;
public final class ContactDeleteFlow extends ContactsProhibitedFlow {
@Inject
ContactDeleteFlow() {}
@Override
public EppResponse run() throws EppException {
extensionManager.register(MetadataExtension.class);
validateRegistrarIsLoggedIn(registrarId);
extensionManager.validate();
DateTime now = tm().getTransactionTime();
checkLinkedDomains(targetId, now, Contact.class);
Contact existingContact = loadAndVerifyExistence(Contact.class, targetId, now);
verifyOptionalAuthInfo(authInfo, existingContact);
verifyNoDisallowedStatuses(existingContact, ImmutableSet.of(StatusValue.PENDING_DELETE));
if (!isSuperuser) {
verifyNoDisallowedStatuses(existingContact, DELETE_PROHIBITED_STATUSES);
verifyResourceOwnership(registrarId, existingContact);
}
// Handle pending transfers on contact deletion.
Contact newContact =
existingContact.getStatusValues().contains(StatusValue.PENDING_TRANSFER)
? denyPendingTransfer(existingContact, SERVER_CANCELLED, now, registrarId)
: existingContact;
// Wipe out PII on contact deletion.
newContact =
newContact.asBuilder().wipeOut().setStatusValues(null).setDeletionTime(now).build();
ContactHistory contactHistory =
historyBuilder.setType(Type.CONTACT_DELETE).setContact(newContact).build();
handlePendingTransferOnDelete(existingContact, newContact, now, contactHistory);
tm().insert(contactHistory);
tm().update(newContact);
return responseBuilder.setResultFromCode(SUCCESS).build();
}
}

View File

@@ -1,126 +0,0 @@
// Copyright 2017 The Nomulus Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package google.registry.flows.contact;
import static google.registry.model.contact.PostalInfo.Type.INTERNATIONALIZED;
import com.google.common.base.CharMatcher;
import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Sets;
import google.registry.flows.EppException;
import google.registry.flows.EppException.ParameterValuePolicyErrorException;
import google.registry.flows.EppException.ParameterValueSyntaxErrorException;
import google.registry.model.contact.Contact;
import google.registry.model.contact.ContactAddress;
import google.registry.model.contact.PostalInfo;
import google.registry.model.poll.PendingActionNotificationResponse.ContactPendingActionNotificationResponse;
import google.registry.model.poll.PollMessage;
import google.registry.model.reporting.HistoryEntry.HistoryEntryId;
import google.registry.model.transfer.TransferData;
import google.registry.model.transfer.TransferResponse.ContactTransferResponse;
import java.util.Set;
import javax.annotation.Nullable;
import org.joda.time.DateTime;
/** Static utility functions for contact flows. */
public class ContactFlowUtils {
/** Check that an internationalized postal info has only ascii characters. */
static void validateAsciiPostalInfo(@Nullable PostalInfo internationalized) throws EppException {
if (internationalized != null) {
Preconditions.checkState(INTERNATIONALIZED.equals(internationalized.getType()));
ContactAddress address = internationalized.getAddress();
Set<String> fields = Sets.newHashSet(
internationalized.getName(),
internationalized.getOrg(),
address.getCity(),
address.getCountryCode(),
address.getState(),
address.getZip());
fields.addAll(address.getStreet());
for (String field : fields) {
if (field != null && !CharMatcher.ascii().matchesAllOf(field)) {
throw new BadInternationalizedPostalInfoException();
}
}
}
}
/** Check contact's state against server policy. */
static void validateContactAgainstPolicy(Contact contact) throws EppException {
if (contact.getDisclose() != null && !contact.getDisclose().getFlag()) {
throw new DeclineContactDisclosureFieldDisallowedPolicyException();
}
}
/** Create a poll message for the gaining client in a transfer. */
static PollMessage createGainingTransferPollMessage(
String targetId, TransferData transferData, DateTime now, HistoryEntryId contactHistoryId) {
return new PollMessage.OneTime.Builder()
.setRegistrarId(transferData.getGainingRegistrarId())
.setEventTime(transferData.getPendingTransferExpirationTime())
.setMsg(transferData.getTransferStatus().getMessage())
.setResponseData(
ImmutableList.of(
createTransferResponse(targetId, transferData),
ContactPendingActionNotificationResponse.create(
targetId,
transferData.getTransferStatus().isApproved(),
transferData.getTransferRequestTrid(),
now)))
.setContactHistoryId(contactHistoryId)
.build();
}
/** Create a poll message for the losing client in a transfer. */
static PollMessage createLosingTransferPollMessage(
String targetId, TransferData transferData, HistoryEntryId contactHistoryId) {
return new PollMessage.OneTime.Builder()
.setRegistrarId(transferData.getLosingRegistrarId())
.setEventTime(transferData.getPendingTransferExpirationTime())
.setMsg(transferData.getTransferStatus().getMessage())
.setResponseData(ImmutableList.of(createTransferResponse(targetId, transferData)))
.setContactHistoryId(contactHistoryId)
.build();
}
/** Create a {@link ContactTransferResponse} off of the info in a {@link TransferData}. */
static ContactTransferResponse createTransferResponse(
String targetId, TransferData transferData) {
return new ContactTransferResponse.Builder()
.setContactId(targetId)
.setGainingRegistrarId(transferData.getGainingRegistrarId())
.setLosingRegistrarId(transferData.getLosingRegistrarId())
.setPendingTransferExpirationTime(transferData.getPendingTransferExpirationTime())
.setTransferRequestTime(transferData.getTransferRequestTime())
.setTransferStatus(transferData.getTransferStatus())
.build();
}
/** Declining contact disclosure is disallowed by server policy. */
static class DeclineContactDisclosureFieldDisallowedPolicyException
extends ParameterValuePolicyErrorException {
public DeclineContactDisclosureFieldDisallowedPolicyException() {
super("Declining contact disclosure is disallowed by server policy.");
}
}
/** Internationalized postal infos can only contain ASCII characters. */
static class BadInternationalizedPostalInfoException extends ParameterValueSyntaxErrorException {
public BadInternationalizedPostalInfoException() {
super("Internationalized postal infos can only contain ASCII characters");
}
}
}

View File

@@ -14,91 +14,20 @@
package google.registry.flows.contact;
import static google.registry.flows.FlowUtils.validateRegistrarIsLoggedIn;
import static google.registry.flows.ResourceFlowUtils.loadAndVerifyExistence;
import static google.registry.flows.ResourceFlowUtils.verifyResourceOwnership;
import static google.registry.model.EppResourceUtils.isLinked;
import com.google.common.collect.ImmutableSet;
import google.registry.flows.EppException;
import google.registry.flows.ExtensionManager;
import google.registry.flows.FlowModule.RegistrarId;
import google.registry.flows.FlowModule.Superuser;
import google.registry.flows.FlowModule.TargetId;
import google.registry.flows.TransactionalFlow;
import google.registry.flows.annotations.ReportingSpec;
import google.registry.model.contact.Contact;
import google.registry.model.contact.ContactInfoData;
import google.registry.model.eppcommon.AuthInfo;
import google.registry.model.eppcommon.StatusValue;
import google.registry.model.eppoutput.EppResponse;
import google.registry.flows.exceptions.ContactsProhibitedException;
import google.registry.model.reporting.IcannReportingTypes.ActivityReportField;
import google.registry.util.Clock;
import jakarta.inject.Inject;
import java.util.Optional;
import org.joda.time.DateTime;
/**
* An EPP flow that returns information about a contact.
* An EPP flow that is meant to return information about a contact.
*
* <p>The response includes the contact's postal info, phone numbers, emails, the authInfo which can
* be used to request a transfer and the details of the contact's most recent transfer if it has
* ever been transferred. Any registrar can see any contact's information, but the authInfo is only
* visible to the registrar that owns the contact or to a registrar that already supplied it.
*
* @error {@link google.registry.flows.FlowUtils.NotLoggedInException}
* @error {@link google.registry.flows.ResourceFlowUtils.ResourceDoesNotExistException}
* @error {@link google.registry.flows.ResourceFlowUtils.ResourceNotOwnedException}
* @error {@link ContactsProhibitedException}
*/
@Deprecated
@ReportingSpec(ActivityReportField.CONTACT_INFO)
public final class ContactInfoFlow implements TransactionalFlow {
@Inject ExtensionManager extensionManager;
@Inject Clock clock;
@Inject @RegistrarId String registrarId;
@Inject @TargetId String targetId;
@Inject Optional<AuthInfo> authInfo;
@Inject @Superuser boolean isSuperuser;
@Inject EppResponse.Builder responseBuilder;
public final class ContactInfoFlow extends ContactsProhibitedFlow {
@Inject
ContactInfoFlow() {}
@Override
public EppResponse run() throws EppException {
DateTime now = clock.nowUtc();
validateRegistrarIsLoggedIn(registrarId);
extensionManager.validate(); // There are no legal extensions for this flow.
Contact contact = loadAndVerifyExistence(Contact.class, targetId, now);
if (!isSuperuser) {
verifyResourceOwnership(registrarId, contact);
}
boolean includeAuthInfo =
registrarId.equals(contact.getCurrentSponsorRegistrarId()) || authInfo.isPresent();
ImmutableSet.Builder<StatusValue> statusValues = new ImmutableSet.Builder<>();
statusValues.addAll(contact.getStatusValues());
if (isLinked(contact.createVKey(), now)) {
statusValues.add(StatusValue.LINKED);
}
return responseBuilder
.setResData(
ContactInfoData.newBuilder()
.setContactId(contact.getContactId())
.setRepoId(contact.getRepoId())
.setStatusValues(statusValues.build())
.setPostalInfos(contact.getPostalInfosAsList())
.setVoiceNumber(contact.getVoiceNumber())
.setFaxNumber(contact.getFaxNumber())
.setEmailAddress(contact.getEmailAddress())
.setCurrentSponsorRegistrarId(contact.getCurrentSponsorRegistrarId())
.setCreationRegistrarId(contact.getCreationRegistrarId())
.setCreationTime(contact.getCreationTime())
.setLastEppUpdateRegistrarId(contact.getLastEppUpdateRegistrarId())
.setLastEppUpdateTime(contact.getLastEppUpdateTime())
.setLastTransferTime(contact.getLastTransferTime())
.setAuthInfo(includeAuthInfo ? contact.getAuthInfo() : null)
.setDisclose(contact.getDisclose())
.build())
.build();
}
}

View File

@@ -14,92 +14,19 @@
package google.registry.flows.contact;
import static google.registry.flows.FlowUtils.validateRegistrarIsLoggedIn;
import static google.registry.flows.ResourceFlowUtils.loadAndVerifyExistence;
import static google.registry.flows.ResourceFlowUtils.verifyHasPendingTransfer;
import static google.registry.flows.ResourceFlowUtils.verifyOptionalAuthInfo;
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.reporting.HistoryEntry.Type.CONTACT_TRANSFER_APPROVE;
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
import com.google.common.collect.ImmutableSet;
import google.registry.flows.EppException;
import google.registry.flows.ExtensionManager;
import google.registry.flows.FlowModule.RegistrarId;
import google.registry.flows.FlowModule.TargetId;
import google.registry.flows.MutatingFlow;
import google.registry.flows.annotations.ReportingSpec;
import google.registry.model.contact.Contact;
import google.registry.model.contact.ContactHistory;
import google.registry.model.domain.metadata.MetadataExtension;
import google.registry.model.eppcommon.AuthInfo;
import google.registry.model.eppinput.ResourceCommand;
import google.registry.model.eppoutput.EppResponse;
import google.registry.model.poll.PollMessage;
import google.registry.flows.exceptions.ContactsProhibitedException;
import google.registry.model.reporting.IcannReportingTypes.ActivityReportField;
import google.registry.model.transfer.TransferStatus;
import jakarta.inject.Inject;
import java.util.Optional;
import org.joda.time.DateTime;
/**
* An EPP flow that approves a pending transfer on a contact.
* An EPP flow that is meant to approve a pending transfer on a contact.
*
* <p>The "gaining" registrar requests a transfer from the "losing" (aka current) registrar. The
* losing registrar has a "transfer" time period to respond (by default five days) after which the
* transfer is automatically approved. Within that window, this flow allows the losing client to
* explicitly approve the transfer request, which then becomes effective immediately.
*
* @error {@link google.registry.flows.FlowUtils.NotLoggedInException}
* @error {@link google.registry.flows.ResourceFlowUtils.BadAuthInfoForResourceException}
* @error {@link google.registry.flows.ResourceFlowUtils.ResourceNotOwnedException}
* @error {@link google.registry.flows.ResourceFlowUtils.ResourceDoesNotExistException}
* @error {@link google.registry.flows.exceptions.NotPendingTransferException}
* @error {@link ContactsProhibitedException}
*/
@Deprecated
@ReportingSpec(ActivityReportField.CONTACT_TRANSFER_APPROVE)
public final class ContactTransferApproveFlow implements MutatingFlow {
@Inject ResourceCommand resourceCommand;
@Inject ExtensionManager extensionManager;
@Inject @RegistrarId String registrarId;
@Inject @TargetId String targetId;
@Inject Optional<AuthInfo> authInfo;
@Inject ContactHistory.Builder historyBuilder;
@Inject EppResponse.Builder responseBuilder;
public final class ContactTransferApproveFlow extends ContactsProhibitedFlow {
@Inject ContactTransferApproveFlow() {}
/**
* The logic in this flow, which handles client approvals, very closely parallels the logic in
* {@link Contact#cloneProjectedAtTime} which handles implicit server approvals.
*/
@Override
public EppResponse run() throws EppException {
extensionManager.register(MetadataExtension.class);
validateRegistrarIsLoggedIn(registrarId);
extensionManager.validate();
DateTime now = tm().getTransactionTime();
Contact existingContact = loadAndVerifyExistence(Contact.class, targetId, now);
verifyOptionalAuthInfo(authInfo, existingContact);
verifyHasPendingTransfer(existingContact);
verifyResourceOwnership(registrarId, existingContact);
Contact newContact =
approvePendingTransfer(existingContact, TransferStatus.CLIENT_APPROVED, now);
ContactHistory contactHistory =
historyBuilder.setType(CONTACT_TRANSFER_APPROVE).setContact(newContact).build();
// Create a poll message for the gaining client.
PollMessage gainingPollMessage =
createGainingTransferPollMessage(
targetId, newContact.getTransferData(), now, contactHistory.getHistoryEntryId());
tm().insertAll(ImmutableSet.of(contactHistory, 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());
return responseBuilder
.setResData(createTransferResponse(targetId, newContact.getTransferData()))
.build();
}
}

View File

@@ -14,88 +14,19 @@
package google.registry.flows.contact;
import static google.registry.flows.FlowUtils.validateRegistrarIsLoggedIn;
import static google.registry.flows.ResourceFlowUtils.loadAndVerifyExistence;
import static google.registry.flows.ResourceFlowUtils.verifyHasPendingTransfer;
import static google.registry.flows.ResourceFlowUtils.verifyOptionalAuthInfo;
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.reporting.HistoryEntry.Type.CONTACT_TRANSFER_CANCEL;
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
import com.google.common.collect.ImmutableSet;
import google.registry.flows.EppException;
import google.registry.flows.ExtensionManager;
import google.registry.flows.FlowModule.RegistrarId;
import google.registry.flows.FlowModule.TargetId;
import google.registry.flows.MutatingFlow;
import google.registry.flows.annotations.ReportingSpec;
import google.registry.model.contact.Contact;
import google.registry.model.contact.ContactHistory;
import google.registry.model.domain.metadata.MetadataExtension;
import google.registry.model.eppcommon.AuthInfo;
import google.registry.model.eppinput.ResourceCommand;
import google.registry.model.eppoutput.EppResponse;
import google.registry.model.poll.PollMessage;
import google.registry.flows.exceptions.ContactsProhibitedException;
import google.registry.model.reporting.IcannReportingTypes.ActivityReportField;
import google.registry.model.transfer.TransferStatus;
import jakarta.inject.Inject;
import java.util.Optional;
import org.joda.time.DateTime;
/**
* An EPP flow that cancels a pending transfer on a contact.
* An EPP flow that is meant to cancel a pending transfer on a contact.
*
* <p>The "gaining" registrar requests a transfer from the "losing" (aka current) registrar. The
* losing registrar has a "transfer" time period to respond (by default five days) after which the
* transfer is automatically approved. Within that window, this flow allows the gaining client to
* withdraw the transfer request.
*
* @error {@link google.registry.flows.FlowUtils.NotLoggedInException}
* @error {@link google.registry.flows.ResourceFlowUtils.BadAuthInfoForResourceException}
* @error {@link google.registry.flows.ResourceFlowUtils.ResourceDoesNotExistException}
* @error {@link google.registry.flows.exceptions.NotPendingTransferException}
* @error {@link google.registry.flows.exceptions.NotTransferInitiatorException}
* @error {@link ContactsProhibitedException}
*/
@Deprecated
@ReportingSpec(ActivityReportField.CONTACT_TRANSFER_CANCEL)
public final class ContactTransferCancelFlow implements MutatingFlow {
@Inject ResourceCommand resourceCommand;
@Inject ExtensionManager extensionManager;
@Inject Optional<AuthInfo> authInfo;
@Inject @RegistrarId String registrarId;
@Inject @TargetId String targetId;
@Inject ContactHistory.Builder historyBuilder;
@Inject EppResponse.Builder responseBuilder;
public final class ContactTransferCancelFlow extends ContactsProhibitedFlow {
@Inject ContactTransferCancelFlow() {}
@Override
public EppResponse run() throws EppException {
extensionManager.register(MetadataExtension.class);
validateRegistrarIsLoggedIn(registrarId);
extensionManager.validate();
DateTime now = tm().getTransactionTime();
Contact existingContact = loadAndVerifyExistence(Contact.class, targetId, now);
verifyOptionalAuthInfo(authInfo, existingContact);
verifyHasPendingTransfer(existingContact);
verifyTransferInitiator(registrarId, existingContact);
Contact newContact =
denyPendingTransfer(existingContact, TransferStatus.CLIENT_CANCELLED, now, registrarId);
ContactHistory contactHistory =
historyBuilder.setType(CONTACT_TRANSFER_CANCEL).setContact(newContact).build();
// Create a poll message for the losing client.
PollMessage losingPollMessage =
createLosingTransferPollMessage(
targetId, newContact.getTransferData(), contactHistory.getHistoryEntryId());
tm().insertAll(ImmutableSet.of(contactHistory, 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());
return responseBuilder
.setResData(createTransferResponse(targetId, newContact.getTransferData()))
.build();
}
}

View File

@@ -14,74 +14,19 @@
package google.registry.flows.contact;
import static google.registry.flows.FlowUtils.validateRegistrarIsLoggedIn;
import static google.registry.flows.ResourceFlowUtils.loadAndVerifyExistence;
import static google.registry.flows.ResourceFlowUtils.verifyOptionalAuthInfo;
import static google.registry.flows.contact.ContactFlowUtils.createTransferResponse;
import google.registry.flows.EppException;
import google.registry.flows.ExtensionManager;
import google.registry.flows.FlowModule.RegistrarId;
import google.registry.flows.FlowModule.TargetId;
import google.registry.flows.TransactionalFlow;
import google.registry.flows.annotations.ReportingSpec;
import google.registry.flows.exceptions.NoTransferHistoryToQueryException;
import google.registry.flows.exceptions.NotAuthorizedToViewTransferException;
import google.registry.model.contact.Contact;
import google.registry.model.eppcommon.AuthInfo;
import google.registry.model.eppoutput.EppResponse;
import google.registry.flows.exceptions.ContactsProhibitedException;
import google.registry.model.reporting.IcannReportingTypes.ActivityReportField;
import google.registry.util.Clock;
import jakarta.inject.Inject;
import java.util.Optional;
/**
* An EPP flow that queries a pending transfer on a contact.
* An EPP flow that is meant to query a pending transfer on a contact.
*
* <p>The "gaining" registrar requests a transfer from the "losing" (aka current) registrar. The
* losing registrar has a "transfer" time period to respond (by default five days) after which the
* transfer is automatically approved. This flow can be used by the gaining or losing registrars (or
* anyone with the correct authId) to see the status of a transfer, which may still be pending or
* may have been approved, rejected, cancelled or implicitly approved by virtue of the transfer
* period expiring.
*
* @error {@link google.registry.flows.FlowUtils.NotLoggedInException}
* @error {@link google.registry.flows.ResourceFlowUtils.BadAuthInfoForResourceException}
* @error {@link google.registry.flows.ResourceFlowUtils.ResourceDoesNotExistException}
* @error {@link google.registry.flows.exceptions.NoTransferHistoryToQueryException}
* @error {@link google.registry.flows.exceptions.NotAuthorizedToViewTransferException}
* @error {@link ContactsProhibitedException}
*/
@Deprecated
@ReportingSpec(ActivityReportField.CONTACT_TRANSFER_QUERY)
public final class ContactTransferQueryFlow implements TransactionalFlow {
@Inject ExtensionManager extensionManager;
@Inject Optional<AuthInfo> authInfo;
@Inject @RegistrarId String registrarId;
@Inject @TargetId String targetId;
@Inject Clock clock;
@Inject EppResponse.Builder responseBuilder;
public final class ContactTransferQueryFlow extends ContactsProhibitedFlow {
@Inject ContactTransferQueryFlow() {}
@Override
public EppResponse run() throws EppException {
validateRegistrarIsLoggedIn(registrarId);
extensionManager.validate(); // There are no legal extensions for this flow.
Contact contact = loadAndVerifyExistence(Contact.class, targetId, clock.nowUtc());
verifyOptionalAuthInfo(authInfo, contact);
// Most of the fields on the transfer response are required, so there's no way to return valid
// XML if the object has never been transferred (and hence the fields aren't populated).
if (contact.getTransferData().getTransferStatus() == null) {
throw new NoTransferHistoryToQueryException();
}
// Note that the authorization info on the command (if present) has already been verified. If
// it's present, then the other checks are unnecessary.
if (authInfo.isEmpty()
&& !registrarId.equals(contact.getTransferData().getGainingRegistrarId())
&& !registrarId.equals(contact.getTransferData().getLosingRegistrarId())) {
throw new NotAuthorizedToViewTransferException();
}
return responseBuilder
.setResData(createTransferResponse(targetId, contact.getTransferData()))
.build();
}
}

View File

@@ -14,85 +14,19 @@
package google.registry.flows.contact;
import static google.registry.flows.FlowUtils.validateRegistrarIsLoggedIn;
import static google.registry.flows.ResourceFlowUtils.loadAndVerifyExistence;
import static google.registry.flows.ResourceFlowUtils.verifyHasPendingTransfer;
import static google.registry.flows.ResourceFlowUtils.verifyOptionalAuthInfo;
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.reporting.HistoryEntry.Type.CONTACT_TRANSFER_REJECT;
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
import com.google.common.collect.ImmutableSet;
import google.registry.flows.EppException;
import google.registry.flows.ExtensionManager;
import google.registry.flows.FlowModule.RegistrarId;
import google.registry.flows.FlowModule.TargetId;
import google.registry.flows.MutatingFlow;
import google.registry.flows.annotations.ReportingSpec;
import google.registry.model.contact.Contact;
import google.registry.model.contact.ContactHistory;
import google.registry.model.domain.metadata.MetadataExtension;
import google.registry.model.eppcommon.AuthInfo;
import google.registry.model.eppoutput.EppResponse;
import google.registry.model.poll.PollMessage;
import google.registry.flows.exceptions.ContactsProhibitedException;
import google.registry.model.reporting.IcannReportingTypes.ActivityReportField;
import google.registry.model.transfer.TransferStatus;
import jakarta.inject.Inject;
import java.util.Optional;
import org.joda.time.DateTime;
/**
* An EPP flow that rejects a pending transfer on a contact.
* An EPP flow that is meant to reject a pending transfer on a contact.
*
* <p>The "gaining" registrar requests a transfer from the "losing" (aka current) registrar. The
* losing registrar has a "transfer" time period to respond (by default five days) after which the
* transfer is automatically approved. Within that window, this flow allows the losing client to
* reject the transfer request.
*
* @error {@link google.registry.flows.FlowUtils.NotLoggedInException}
* @error {@link google.registry.flows.ResourceFlowUtils.BadAuthInfoForResourceException}
* @error {@link google.registry.flows.ResourceFlowUtils.ResourceDoesNotExistException}
* @error {@link google.registry.flows.ResourceFlowUtils.ResourceNotOwnedException}
* @error {@link google.registry.flows.exceptions.NotPendingTransferException}
* @error {@link ContactsProhibitedException}
*/
@Deprecated
@ReportingSpec(ActivityReportField.CONTACT_TRANSFER_REJECT)
public final class ContactTransferRejectFlow implements MutatingFlow {
@Inject ExtensionManager extensionManager;
@Inject Optional<AuthInfo> authInfo;
@Inject @RegistrarId String registrarId;
@Inject @TargetId String targetId;
@Inject ContactHistory.Builder historyBuilder;
@Inject EppResponse.Builder responseBuilder;
public final class ContactTransferRejectFlow extends ContactsProhibitedFlow {
@Inject ContactTransferRejectFlow() {}
@Override
public EppResponse run() throws EppException {
extensionManager.register(MetadataExtension.class);
validateRegistrarIsLoggedIn(registrarId);
extensionManager.validate();
DateTime now = tm().getTransactionTime();
Contact existingContact = loadAndVerifyExistence(Contact.class, targetId, now);
verifyOptionalAuthInfo(authInfo, existingContact);
verifyHasPendingTransfer(existingContact);
verifyResourceOwnership(registrarId, existingContact);
Contact newContact =
denyPendingTransfer(existingContact, TransferStatus.CLIENT_REJECTED, now, registrarId);
ContactHistory contactHistory =
historyBuilder.setType(CONTACT_TRANSFER_REJECT).setContact(newContact).build();
PollMessage gainingPollMessage =
createGainingTransferPollMessage(
targetId, newContact.getTransferData(), now, contactHistory.getHistoryEntryId());
tm().insertAll(ImmutableSet.of(contactHistory, 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());
return responseBuilder
.setResData(createTransferResponse(targetId, newContact.getTransferData()))
.build();
}
}

View File

@@ -14,162 +14,20 @@
package google.registry.flows.contact;
import static google.registry.flows.FlowUtils.createHistoryEntryId;
import static google.registry.flows.FlowUtils.validateRegistrarIsLoggedIn;
import static google.registry.flows.ResourceFlowUtils.loadAndVerifyExistence;
import static google.registry.flows.ResourceFlowUtils.verifyAuthInfo;
import static google.registry.flows.ResourceFlowUtils.verifyAuthInfoPresentForResourceTransfer;
import static google.registry.flows.ResourceFlowUtils.verifyNoDisallowedStatuses;
import static google.registry.flows.contact.ContactFlowUtils.createGainingTransferPollMessage;
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.reporting.HistoryEntry.Type.CONTACT_TRANSFER_REQUEST;
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
import com.google.common.collect.ImmutableSet;
import google.registry.config.RegistryConfig.Config;
import google.registry.flows.EppException;
import google.registry.flows.ExtensionManager;
import google.registry.flows.FlowModule.RegistrarId;
import google.registry.flows.FlowModule.TargetId;
import google.registry.flows.MutatingFlow;
import google.registry.flows.annotations.ReportingSpec;
import google.registry.flows.exceptions.AlreadyPendingTransferException;
import google.registry.flows.exceptions.ObjectAlreadySponsoredException;
import google.registry.model.contact.Contact;
import google.registry.model.contact.ContactHistory;
import google.registry.model.domain.metadata.MetadataExtension;
import google.registry.model.eppcommon.AuthInfo;
import google.registry.model.eppcommon.StatusValue;
import google.registry.model.eppcommon.Trid;
import google.registry.model.eppoutput.EppResponse;
import google.registry.model.poll.PollMessage;
import google.registry.model.reporting.HistoryEntry.HistoryEntryId;
import google.registry.flows.exceptions.ContactsProhibitedException;
import google.registry.model.reporting.IcannReportingTypes.ActivityReportField;
import google.registry.model.transfer.ContactTransferData;
import google.registry.model.transfer.TransferStatus;
import jakarta.inject.Inject;
import java.util.Optional;
import org.joda.time.DateTime;
import org.joda.time.Duration;
/**
* An EPP flow that requests a transfer on a contact.
* An EPP flow that is meant to request a transfer on a contact.
*
* <p>The "gaining" registrar requests a transfer from the "losing" (aka current) registrar. The
* losing registrar has a "transfer" time period to respond (by default five days) after which the
* transfer is automatically approved. Within that window, the transfer might be approved explicitly
* by the losing registrar or rejected, and the gaining registrar can also cancel the transfer
* request.
*
* @error {@link google.registry.flows.FlowUtils.NotLoggedInException}
* @error {@link google.registry.flows.ResourceFlowUtils.BadAuthInfoForResourceException}
* @error {@link google.registry.flows.ResourceFlowUtils.ResourceDoesNotExistException}
* @error {@link google.registry.flows.exceptions.AlreadyPendingTransferException}
* @error {@link google.registry.flows.exceptions.MissingTransferRequestAuthInfoException}
* @error {@link google.registry.flows.exceptions.ObjectAlreadySponsoredException}
* @error {@link google.registry.flows.exceptions.ResourceStatusProhibitsOperationException}
* @error {@link ContactsProhibitedException}
*/
@Deprecated
@ReportingSpec(ActivityReportField.CONTACT_TRANSFER_REQUEST)
public final class ContactTransferRequestFlow implements MutatingFlow {
private static final ImmutableSet<StatusValue> DISALLOWED_STATUSES =
ImmutableSet.of(
StatusValue.CLIENT_TRANSFER_PROHIBITED,
StatusValue.PENDING_DELETE,
StatusValue.SERVER_TRANSFER_PROHIBITED);
@Inject ExtensionManager extensionManager;
@Inject Optional<AuthInfo> authInfo;
@Inject @RegistrarId String gainingClientId;
@Inject @TargetId String targetId;
@Inject
@Config("contactAutomaticTransferLength")
Duration automaticTransferLength;
@Inject ContactHistory.Builder historyBuilder;
@Inject Trid trid;
@Inject EppResponse.Builder responseBuilder;
public final class ContactTransferRequestFlow extends ContactsProhibitedFlow {
@Inject
ContactTransferRequestFlow() {}
@Override
public EppResponse run() throws EppException {
extensionManager.register(MetadataExtension.class);
validateRegistrarIsLoggedIn(gainingClientId);
extensionManager.validate();
DateTime now = tm().getTransactionTime();
Contact existingContact = loadAndVerifyExistence(Contact.class, targetId, now);
verifyAuthInfoPresentForResourceTransfer(authInfo);
verifyAuthInfo(authInfo.get(), existingContact);
// Verify that the resource does not already have a pending transfer.
if (TransferStatus.PENDING.equals(existingContact.getTransferData().getTransferStatus())) {
throw new AlreadyPendingTransferException(targetId);
}
String losingClientId = existingContact.getCurrentSponsorRegistrarId();
// Verify that this client doesn't already sponsor this resource.
if (gainingClientId.equals(losingClientId)) {
throw new ObjectAlreadySponsoredException();
}
verifyNoDisallowedStatuses(existingContact, DISALLOWED_STATUSES);
DateTime transferExpirationTime = now.plus(automaticTransferLength);
ContactTransferData serverApproveTransferData =
new ContactTransferData.Builder()
.setTransferRequestTime(now)
.setTransferRequestTrid(trid)
.setGainingRegistrarId(gainingClientId)
.setLosingRegistrarId(losingClientId)
.setPendingTransferExpirationTime(transferExpirationTime)
.setTransferStatus(TransferStatus.SERVER_APPROVED)
.build();
HistoryEntryId contactHistoryId = createHistoryEntryId(existingContact);
historyBuilder
.setRevisionId(contactHistoryId.getRevisionId())
.setType(CONTACT_TRANSFER_REQUEST);
// If the transfer is server approved, this message will be sent to the losing registrar. */
PollMessage serverApproveLosingPollMessage =
createLosingTransferPollMessage(targetId, serverApproveTransferData, contactHistoryId);
// If the transfer is server approved, this message will be sent to the gaining registrar. */
PollMessage serverApproveGainingPollMessage =
createGainingTransferPollMessage(
targetId, serverApproveTransferData, now, contactHistoryId);
ContactTransferData pendingTransferData =
serverApproveTransferData
.asBuilder()
.setTransferStatus(TransferStatus.PENDING)
.setServerApproveEntities(
serverApproveGainingPollMessage.getContactRepoId(),
contactHistoryId.getRevisionId(),
ImmutableSet.of(
serverApproveGainingPollMessage.createVKey(),
serverApproveLosingPollMessage.createVKey()))
.build();
// When a transfer is requested, a poll message is created to notify the losing registrar.
PollMessage requestPollMessage =
createLosingTransferPollMessage(targetId, pendingTransferData, contactHistoryId)
.asBuilder()
.setEventTime(now) // Unlike the serverApprove messages, this applies immediately.
.build();
Contact newContact =
existingContact
.asBuilder()
.setTransferData(pendingTransferData)
.addStatusValue(StatusValue.PENDING_TRANSFER)
.build();
tm().update(newContact);
tm().insertAll(
ImmutableSet.of(
historyBuilder.setContact(newContact).build(),
requestPollMessage,
serverApproveGainingPollMessage,
serverApproveLosingPollMessage));
return responseBuilder
.setResultFromCode(SUCCESS_WITH_ACTION_PENDING)
.setResData(createTransferResponse(targetId, newContact.getTransferData()))
.build();
}
}

View File

@@ -14,158 +14,19 @@
package google.registry.flows.contact;
import static com.google.common.collect.Sets.union;
import static google.registry.flows.FlowUtils.validateRegistrarIsLoggedIn;
import static google.registry.flows.ResourceFlowUtils.checkSameValuesNotAddedAndRemoved;
import static google.registry.flows.ResourceFlowUtils.loadAndVerifyExistence;
import static google.registry.flows.ResourceFlowUtils.verifyAllStatusesAreClientSettable;
import static google.registry.flows.ResourceFlowUtils.verifyNoDisallowedStatuses;
import static google.registry.flows.ResourceFlowUtils.verifyOptionalAuthInfo;
import static google.registry.flows.ResourceFlowUtils.verifyResourceOwnership;
import static google.registry.flows.contact.ContactFlowUtils.validateAsciiPostalInfo;
import static google.registry.flows.contact.ContactFlowUtils.validateContactAgainstPolicy;
import static google.registry.model.common.FeatureFlag.FeatureName.MINIMUM_DATASET_CONTACTS_PROHIBITED;
import static google.registry.model.reporting.HistoryEntry.Type.CONTACT_UPDATE;
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
import com.google.common.collect.ImmutableSet;
import google.registry.flows.EppException;
import google.registry.flows.ExtensionManager;
import google.registry.flows.FlowModule.RegistrarId;
import google.registry.flows.FlowModule.Superuser;
import google.registry.flows.FlowModule.TargetId;
import google.registry.flows.MutatingFlow;
import google.registry.flows.annotations.ReportingSpec;
import google.registry.flows.exceptions.ContactsProhibitedException;
import google.registry.flows.exceptions.ResourceHasClientUpdateProhibitedException;
import google.registry.model.common.FeatureFlag;
import google.registry.model.contact.Contact;
import google.registry.model.contact.ContactCommand.Update;
import google.registry.model.contact.ContactCommand.Update.Change;
import google.registry.model.contact.ContactHistory;
import google.registry.model.contact.PostalInfo;
import google.registry.model.domain.metadata.MetadataExtension;
import google.registry.model.eppcommon.AuthInfo;
import google.registry.model.eppcommon.StatusValue;
import google.registry.model.eppinput.ResourceCommand;
import google.registry.model.eppoutput.EppResponse;
import google.registry.model.reporting.IcannReportingTypes.ActivityReportField;
import jakarta.inject.Inject;
import java.util.Optional;
import javax.annotation.Nullable;
import org.joda.time.DateTime;
/**
* An EPP flow that updates a contact.
* An EPP flow meant to update a contact.
*
* @error {@link ContactsProhibitedException}
* @error {@link google.registry.flows.FlowUtils.NotLoggedInException}
* @error {@link google.registry.flows.ResourceFlowUtils.AddRemoveSameValueException}
* @error {@link google.registry.flows.ResourceFlowUtils.ResourceDoesNotExistException}
* @error {@link google.registry.flows.ResourceFlowUtils.ResourceNotOwnedException}
* @error {@link google.registry.flows.ResourceFlowUtils.StatusNotClientSettableException}
* @error {@link google.registry.flows.exceptions.ResourceHasClientUpdateProhibitedException}
* @error {@link google.registry.flows.exceptions.ResourceStatusProhibitsOperationException}
* @error {@link ContactFlowUtils.BadInternationalizedPostalInfoException}
* @error {@link ContactFlowUtils.DeclineContactDisclosureFieldDisallowedPolicyException}
*/
@Deprecated
@ReportingSpec(ActivityReportField.CONTACT_UPDATE)
public final class ContactUpdateFlow implements MutatingFlow {
/**
* Note that CLIENT_UPDATE_PROHIBITED is intentionally not in this list. This is because it
* requires special checking, since you must be able to clear the status off the object with an
* update.
*/
private static final ImmutableSet<StatusValue> DISALLOWED_STATUSES = ImmutableSet.of(
StatusValue.PENDING_DELETE,
StatusValue.SERVER_UPDATE_PROHIBITED);
@Inject ResourceCommand resourceCommand;
@Inject ExtensionManager extensionManager;
@Inject Optional<AuthInfo> authInfo;
@Inject @RegistrarId String registrarId;
@Inject @TargetId String targetId;
@Inject @Superuser boolean isSuperuser;
@Inject ContactHistory.Builder historyBuilder;
@Inject EppResponse.Builder responseBuilder;
public final class ContactUpdateFlow extends ContactsProhibitedFlow {
@Inject ContactUpdateFlow() {}
@Override
public EppResponse run() throws EppException {
extensionManager.register(MetadataExtension.class);
validateRegistrarIsLoggedIn(registrarId);
extensionManager.validate();
if (FeatureFlag.isActiveNow(MINIMUM_DATASET_CONTACTS_PROHIBITED)) {
throw new ContactsProhibitedException();
}
Update command = (Update) resourceCommand;
DateTime now = tm().getTransactionTime();
Contact existingContact = loadAndVerifyExistence(Contact.class, targetId, now);
verifyOptionalAuthInfo(authInfo, existingContact);
ImmutableSet<StatusValue> statusToRemove = command.getInnerRemove().getStatusValues();
ImmutableSet<StatusValue> statusesToAdd = command.getInnerAdd().getStatusValues();
if (!isSuperuser) { // The superuser can update any contact and set any status.
verifyResourceOwnership(registrarId, existingContact);
verifyAllStatusesAreClientSettable(union(statusesToAdd, statusToRemove));
}
verifyNoDisallowedStatuses(existingContact, DISALLOWED_STATUSES);
checkSameValuesNotAddedAndRemoved(statusesToAdd, statusToRemove);
Contact.Builder builder = existingContact.asBuilder();
Change change = command.getInnerChange();
// The spec requires the following behaviors:
// * If you update part of a postal info, the fields that you didn't update are unchanged.
// * If you update one postal info but not the other, the other is deleted.
// Therefore, if you want to preserve one postal info and update another you need to send the
// update and also something that technically updates the preserved one, even if it only
// "updates" it by setting just one field to the same value.
PostalInfo internationalized = change.getInternationalizedPostalInfo();
PostalInfo localized = change.getLocalizedPostalInfo();
if (internationalized != null) {
builder.overlayInternationalizedPostalInfo(internationalized);
if (localized == null) {
builder.setLocalizedPostalInfo(null);
}
}
if (localized != null) {
builder.overlayLocalizedPostalInfo(localized);
if (internationalized == null) {
builder.setInternationalizedPostalInfo(null);
}
}
Contact newContact =
builder
.setLastEppUpdateTime(now)
.setLastEppUpdateRegistrarId(registrarId)
.setAuthInfo(preferFirst(change.getAuthInfo(), existingContact.getAuthInfo()))
.setDisclose(preferFirst(change.getDisclose(), existingContact.getDisclose()))
.setEmailAddress(preferFirst(change.getEmail(), existingContact.getEmailAddress()))
.setFaxNumber(preferFirst(change.getFax(), existingContact.getFaxNumber()))
.setVoiceNumber(preferFirst(change.getVoice(), existingContact.getVoiceNumber()))
.addStatusValues(statusesToAdd)
.removeStatusValues(statusToRemove)
.build();
// If the resource is marked with clientUpdateProhibited, and this update did not clear that
// status, then the update must be disallowed (unless a superuser is requesting the change).
if (!isSuperuser
&& existingContact.getStatusValues().contains(StatusValue.CLIENT_UPDATE_PROHIBITED)
&& newContact.getStatusValues().contains(StatusValue.CLIENT_UPDATE_PROHIBITED)) {
throw new ResourceHasClientUpdateProhibitedException();
}
validateAsciiPostalInfo(newContact.getInternationalizedPostalInfo());
validateContactAgainstPolicy(newContact);
historyBuilder
.setType(CONTACT_UPDATE)
.setXmlBytes(null) // We don't want to store contact details in the history entry.
.setContact(newContact);
tm().insert(historyBuilder.build());
tm().update(newContact);
return responseBuilder.build();
}
/** Return the first non-null param, or null if both are null. */
@Nullable
private static <T> T preferFirst(@Nullable T a, @Nullable T b) {
return a != null ? a : b;
}
}

View File

@@ -0,0 +1,28 @@
// Copyright 2025 The Nomulus Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package google.registry.flows.contact;
import google.registry.flows.EppException;
import google.registry.flows.Flow;
import google.registry.flows.exceptions.ContactsProhibitedException;
import google.registry.model.eppoutput.EppResponse;
/** Nomulus follows the Minimum Dataset Requirements, meaning it stores no contact information. */
public abstract class ContactsProhibitedFlow implements Flow {
@Override
public EppResponse run() throws EppException {
throw new ContactsProhibitedException();
}
}

View File

@@ -186,12 +186,9 @@ import org.joda.time.Duration;
* @error {@link DomainFlowUtils.LinkedResourceInPendingDeleteProhibitsOperationException}
* @error {@link DomainFlowUtils.MalformedTcnIdException}
* @error {@link DomainFlowUtils.MaxSigLifeNotSupportedException}
* @error {@link DomainFlowUtils.MissingAdminContactException}
* @error {@link DomainFlowUtils.MissingBillingAccountMapException}
* @error {@link DomainFlowUtils.MissingClaimsNoticeException}
* @error {@link DomainFlowUtils.MissingContactTypeException}
* @error {@link DomainFlowUtils.MissingRegistrantException}
* @error {@link DomainFlowUtils.MissingTechnicalContactException}
* @error {@link DomainFlowUtils.NameserversNotAllowedForTldException}
* @error {@link DomainFlowUtils.NameserversNotSpecifiedForTldWithNameserverAllowListException}
* @error {@link DomainFlowUtils.PremiumNameBlockedException}

View File

@@ -24,8 +24,6 @@ import static com.google.common.collect.Sets.difference;
import static com.google.common.collect.Sets.intersection;
import static com.google.common.collect.Sets.union;
import static google.registry.bsa.persistence.BsaLabelUtils.isLabelBlocked;
import static google.registry.model.common.FeatureFlag.FeatureName.MINIMUM_DATASET_CONTACTS_OPTIONAL;
import static google.registry.model.common.FeatureFlag.FeatureName.MINIMUM_DATASET_CONTACTS_PROHIBITED;
import static google.registry.model.domain.Domain.MAX_REGISTRATION_YEARS;
import static google.registry.model.domain.token.AllocationToken.TokenType.REGISTER_BSA;
import static google.registry.model.tld.Tld.TldState.GENERAL_AVAILABILITY;
@@ -81,7 +79,6 @@ import google.registry.model.EppResource;
import google.registry.model.billing.BillingBase.Flag;
import google.registry.model.billing.BillingBase.Reason;
import google.registry.model.billing.BillingRecurrence;
import google.registry.model.common.FeatureFlag;
import google.registry.model.contact.Contact;
import google.registry.model.domain.DesignatedContact;
import google.registry.model.domain.DesignatedContact.Type;
@@ -138,7 +135,6 @@ import google.registry.util.Idn;
import java.math.BigDecimal;
import java.util.Collection;
import java.util.Comparator;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
@@ -486,31 +482,12 @@ public class DomainFlowUtils {
*/
static void validateCreateContactData(
Optional<VKey<Contact>> registrant, Set<DesignatedContact> contacts)
throws RequiredParameterMissingException, ParameterValuePolicyErrorException {
// TODO(b/353347632): Change these flag checks to a registry config check once minimum data set
// migration is completed.
if (FeatureFlag.isActiveNow(MINIMUM_DATASET_CONTACTS_PROHIBITED)) {
if (registrant.isPresent()) {
throw new RegistrantProhibitedException();
}
if (!contacts.isEmpty()) {
throw new ContactsProhibitedException();
}
} else if (!FeatureFlag.isActiveNow(MINIMUM_DATASET_CONTACTS_OPTIONAL)) {
if (registrant.isEmpty()) {
throw new MissingRegistrantException();
}
Set<Type> roles = new HashSet<>();
for (DesignatedContact contact : contacts) {
roles.add(contact.getType());
}
if (!roles.contains(Type.ADMIN)) {
throw new MissingAdminContactException();
}
if (!roles.contains(Type.TECH)) {
throw new MissingTechnicalContactException();
}
throws ParameterValuePolicyErrorException {
if (registrant.isPresent()) {
throw new RegistrantProhibitedException();
}
if (!contacts.isEmpty()) {
throw new ContactsProhibitedException();
}
}
@@ -523,33 +500,14 @@ public class DomainFlowUtils {
Optional<VKey<Contact>> newRegistrant,
Set<DesignatedContact> existingContacts,
Set<DesignatedContact> newContacts)
throws RequiredParameterMissingException, ParameterValuePolicyErrorException {
// TODO(b/353347632): Change these flag checks to a registry config check once minimum data set
// migration is completed.
if (FeatureFlag.isActiveNow(MINIMUM_DATASET_CONTACTS_PROHIBITED)) {
// Throw if the update specifies a new registrant that is different from the existing one.
if (newRegistrant.isPresent() && !newRegistrant.equals(existingRegistrant)) {
throw new RegistrantProhibitedException();
}
// Throw if the update specifies any new contacts that weren't already present on the domain.
if (!Sets.difference(newContacts, existingContacts).isEmpty()) {
throw new ContactsProhibitedException();
}
} else if (!FeatureFlag.isActiveNow(MINIMUM_DATASET_CONTACTS_OPTIONAL)) {
// Throw if the update empties out a registrant that had been present.
if (newRegistrant.isEmpty() && existingRegistrant.isPresent()) {
throw new MissingRegistrantException();
}
// Throw if the update contains no admin contact when one had been present.
if (existingContacts.stream().anyMatch(c -> c.getType().equals(Type.ADMIN))
&& newContacts.stream().noneMatch(c -> c.getType().equals(Type.ADMIN))) {
throw new MissingAdminContactException();
}
// Throw if the update contains no tech contact when one had been present.
if (existingContacts.stream().anyMatch(c -> c.getType().equals(Type.TECH))
&& newContacts.stream().noneMatch(c -> c.getType().equals(Type.TECH))) {
throw new MissingTechnicalContactException();
}
throws ParameterValuePolicyErrorException {
// Throw if the update specifies a new registrant that is different from the existing one.
if (newRegistrant.isPresent() && !newRegistrant.equals(existingRegistrant)) {
throw new RegistrantProhibitedException();
}
// Throw if the update specifies any new contacts that weren't already present on the domain.
if (!Sets.difference(newContacts, existingContacts).isEmpty()) {
throw new ContactsProhibitedException();
}
}
@@ -1282,49 +1240,49 @@ public class DomainFlowUtils {
}
/** Domain names can only contain a-z, 0-9, '.' and '-'. */
static class BadDomainNameCharacterException extends ParameterValuePolicyErrorException {
static class BadDomainNameCharacterException extends ParameterValueSyntaxErrorException {
public BadDomainNameCharacterException() {
super("Domain names can only contain a-z, 0-9, '.' and '-'");
}
}
/** Non-IDN domain names cannot contain hyphens in the third or fourth position. */
static class DashesInThirdAndFourthException extends ParameterValuePolicyErrorException {
static class DashesInThirdAndFourthException extends ParameterValueSyntaxErrorException {
public DashesInThirdAndFourthException() {
super("Non-IDN domain names cannot contain dashes in the third or fourth position");
}
}
/** Domain labels cannot begin with a dash. */
static class LeadingDashException extends ParameterValuePolicyErrorException {
static class LeadingDashException extends ParameterValueSyntaxErrorException {
public LeadingDashException() {
super("Domain labels cannot begin with a dash");
}
}
/** Domain labels cannot end with a dash. */
static class TrailingDashException extends ParameterValuePolicyErrorException {
static class TrailingDashException extends ParameterValueSyntaxErrorException {
public TrailingDashException() {
super("Domain labels cannot end with a dash");
}
}
/** Domain labels cannot be longer than 63 characters. */
static class DomainLabelTooLongException extends ParameterValuePolicyErrorException {
static class DomainLabelTooLongException extends ParameterValueSyntaxErrorException {
public DomainLabelTooLongException() {
super("Domain labels cannot be longer than 63 characters");
}
}
/** No part of a domain name can be empty. */
static class EmptyDomainNamePartException extends ParameterValuePolicyErrorException {
static class EmptyDomainNamePartException extends ParameterValueSyntaxErrorException {
public EmptyDomainNamePartException() {
super("No part of a domain name can be empty");
}
}
/** Domain name starts with xn-- but is not a valid IDN. */
static class InvalidPunycodeException extends ParameterValuePolicyErrorException {
static class InvalidPunycodeException extends ParameterValueSyntaxErrorException {
public InvalidPunycodeException() {
super("Domain name starts with xn-- but is not a valid IDN");
}
@@ -1398,13 +1356,6 @@ public class DomainFlowUtils {
}
}
/** Registrant is required. */
static class MissingRegistrantException extends RequiredParameterMissingException {
public MissingRegistrantException() {
super("Registrant is required");
}
}
/** Having a registrant is prohibited by registry policy. */
static class RegistrantProhibitedException extends ParameterValuePolicyErrorException {
public RegistrantProhibitedException() {
@@ -1412,20 +1363,6 @@ public class DomainFlowUtils {
}
}
/** Admin contact is required. */
static class MissingAdminContactException extends RequiredParameterMissingException {
public MissingAdminContactException() {
super("Admin contact is required");
}
}
/** Technical contact is required. */
static class MissingTechnicalContactException extends RequiredParameterMissingException {
public MissingTechnicalContactException() {
super("Technical contact is required");
}
}
/** Too many nameservers set on this domain. */
static class TooManyNameserversException extends ParameterValuePolicyErrorException {
public TooManyNameserversException(String message) {

View File

@@ -133,10 +133,9 @@ import org.joda.time.DateTime;
@ReportingSpec(ActivityReportField.DOMAIN_TRANSFER_REQUEST)
public final class DomainTransferRequestFlow implements MutatingFlow {
private static final ImmutableSet<StatusValue> DISALLOWED_STATUSES = ImmutableSet.of(
StatusValue.CLIENT_TRANSFER_PROHIBITED,
StatusValue.PENDING_DELETE,
StatusValue.SERVER_TRANSFER_PROHIBITED);
private static final ImmutableSet<StatusValue> NON_SUPERUSER_DISALLOWED_STATUSES =
ImmutableSet.of(
StatusValue.CLIENT_TRANSFER_PROHIBITED, StatusValue.SERVER_TRANSFER_PROHIBITED);
@Inject ResourceCommand resourceCommand;
@Inject ExtensionManager extensionManager;
@@ -299,8 +298,9 @@ public final class DomainTransferRequestFlow implements MutatingFlow {
DateTime now,
Optional<DomainTransferRequestSuperuserExtension> superuserExtension)
throws EppException {
verifyNoDisallowedStatuses(existingDomain, DISALLOWED_STATUSES);
verifyNoDisallowedStatuses(existingDomain, ImmutableSet.of(StatusValue.PENDING_DELETE));
if (!isSuperuser) {
verifyNoDisallowedStatuses(existingDomain, NON_SUPERUSER_DISALLOWED_STATUSES);
verifyAuthInfoPresentForResourceTransfer(authInfo);
verifyAuthInfo(authInfo.get(), existingDomain);
}

View File

@@ -39,8 +39,6 @@ import static google.registry.flows.domain.DomainFlowUtils.validateNoDuplicateCo
import static google.registry.flows.domain.DomainFlowUtils.validateUpdateContactData;
import static google.registry.flows.domain.DomainFlowUtils.verifyClientUpdateNotProhibited;
import static google.registry.flows.domain.DomainFlowUtils.verifyNotInPendingDelete;
import static google.registry.model.common.FeatureFlag.FeatureName.MINIMUM_DATASET_CONTACTS_OPTIONAL;
import static google.registry.model.common.FeatureFlag.FeatureName.MINIMUM_DATASET_CONTACTS_PROHIBITED;
import static google.registry.model.reporting.HistoryEntry.Type.DOMAIN_UPDATE;
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
@@ -61,13 +59,11 @@ import google.registry.flows.custom.DomainUpdateFlowCustomLogic;
import google.registry.flows.custom.DomainUpdateFlowCustomLogic.AfterValidationParameters;
import google.registry.flows.custom.DomainUpdateFlowCustomLogic.BeforeSaveParameters;
import google.registry.flows.custom.EntityChanges;
import google.registry.flows.domain.DomainFlowUtils.MissingRegistrantException;
import google.registry.flows.domain.DomainFlowUtils.NameserversNotSpecifiedForTldWithNameserverAllowListException;
import google.registry.flows.domain.DomainFlowUtils.RegistrantProhibitedException;
import google.registry.model.ImmutableObject;
import google.registry.model.billing.BillingBase.Reason;
import google.registry.model.billing.BillingEvent;
import google.registry.model.common.FeatureFlag;
import google.registry.model.contact.Contact;
import google.registry.model.domain.DesignatedContact;
import google.registry.model.domain.Domain;
@@ -123,10 +119,7 @@ import org.joda.time.DateTime;
* @error {@link DomainFlowUtils.LinkedResourcesDoNotExistException}
* @error {@link DomainFlowUtils.LinkedResourceInPendingDeleteProhibitsOperationException}
* @error {@link DomainFlowUtils.MaxSigLifeChangeNotSupportedException}
* @error {@link DomainFlowUtils.MissingAdminContactException}
* @error {@link DomainFlowUtils.MissingContactTypeException}
* @error {@link DomainFlowUtils.MissingTechnicalContactException}
* @error {@link DomainFlowUtils.MissingRegistrantException}
* @error {@link DomainFlowUtils.NameserversNotAllowedForTldException}
* @error {@link NameserversNotSpecifiedForTldWithNameserverAllowListException}
* @error {@link DomainFlowUtils.NotAuthorizedForTldException}
@@ -307,18 +300,11 @@ public final class DomainUpdateFlow implements MutatingFlow {
return domainBuilder.build();
}
private Optional<VKey<Contact>> determineUpdatedRegistrant(Change change, Domain domain)
throws EppException {
private Optional<VKey<Contact>> determineUpdatedRegistrant(Change change, Domain domain) {
// During or after the minimum dataset transition, allow registrant to be removed.
if (change.getRegistrantContactId().isPresent()
&& change.getRegistrantContactId().get().isEmpty()) {
// TODO(b/353347632): Change this flag check to a registry config check.
if (FeatureFlag.isActiveNow(MINIMUM_DATASET_CONTACTS_OPTIONAL)
|| FeatureFlag.isActiveNow(MINIMUM_DATASET_CONTACTS_PROHIBITED)) {
return Optional.empty();
} else {
throw new MissingRegistrantException();
}
return Optional.empty();
}
return change.getRegistrant().or(domain::getRegistrant);
}

View File

@@ -135,7 +135,6 @@ public class FlowPicker {
return switch (((Poll) innerCommand).getPollOp()) {
case ACK -> PollAckFlow.class;
case REQUEST -> PollRequestFlow.class;
default -> UnimplementedFlow.class;
};
}
};

View File

@@ -44,7 +44,7 @@ public interface Keyring extends AutoCloseable {
* Returns public key for encrypting escrow deposits being staged to cloud storage.
*
* <p>This adds an additional layer of security so cloud storage administrators won't be tempted
* to go poking around the App Engine Cloud Console and see a dump of the entire database.
* to go poking around the Pantheon Cloud Console and see a dump of the entire database.
*
* <p>This keypair should only be known to the domain registry shared registry system.
*

View File

@@ -76,7 +76,7 @@ public class Cursor extends UpdateAutoTimestampEntity {
*
* <p>The way we solve this problem is by having {@code RdeUploadAction} check this cursor
* before performing an upload for a given TLD. If the cursor is less than two hours old, the
* action will fail with a status code above 300 and App Engine will keep retrying the action
* action will fail with a status code above 300 and Cloud Tasks will keep retrying the action
* until it's ready.
*/
RDE_UPLOAD_SFTP(true),

View File

@@ -1034,12 +1034,13 @@ public class Tld extends ImmutableObject implements Buildable, UnsafeSerializabl
return this;
}
public static final Pattern ROID_SUFFIX_PATTERN = Pattern.compile("^[A-Z\\d_]{1,8}$");
public static final Pattern ROID_SUFFIX_PATTERN = Pattern.compile("^[A-Z\\d]{1,8}$");
public Builder setRoidSuffix(String roidSuffix) {
checkArgument(
ROID_SUFFIX_PATTERN.matcher(roidSuffix).matches(),
"ROID suffix must be in format %s",
"ROID suffix %s must be in format %s",
roidSuffix,
ROID_SUFFIX_PATTERN.pattern());
getInstance().roidSuffix = roidSuffix;
return this;

View File

@@ -40,6 +40,7 @@ import google.registry.keyring.api.KeyModule;
import google.registry.module.RegistryComponent.RegistryModule;
import google.registry.module.RequestComponent.RequestComponentModule;
import google.registry.monitoring.whitebox.StackdriverModule;
import google.registry.mosapi.module.MosApiModule;
import google.registry.persistence.PersistenceModule;
import google.registry.privileges.secretmanager.SecretManagerModule;
import google.registry.rde.JSchModule;
@@ -71,6 +72,7 @@ import jakarta.inject.Singleton;
GroupsModule.class,
GroupssettingsModule.class,
GsonModule.class,
MosApiModule.class,
JSchModule.class,
KeyModule.class,
KeyringModule.class,

View File

@@ -17,6 +17,7 @@ package google.registry.module;
import dagger.Module;
import dagger.Subcomponent;
import google.registry.batch.BatchModule;
import google.registry.batch.BulkDomainTransferAction;
import google.registry.batch.CannedScriptExecutionAction;
import google.registry.batch.DeleteExpiredDomainsAction;
import google.registry.batch.DeleteLoadTestDataAction;
@@ -171,6 +172,8 @@ interface RequestComponent {
BsaValidateAction bsaValidateAction();
BulkDomainTransferAction bulkDomainTransferAction();
CannedScriptExecutionAction cannedScriptExecutionAction();
CheckApiAction checkApiAction();

View File

@@ -28,7 +28,7 @@ import java.util.concurrent.TimeoutException;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.joda.time.DateTime;
/** Base for Servlets that handle all requests to our App Engine modules. */
/** Base for Servlets that handle all requests to our modules. */
public class ServletBase extends HttpServlet {
private final RequestHandler<?> requestHandler;

View File

@@ -0,0 +1,150 @@
// Copyright 2025 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.mosapi;
import static org.apache.beam.sdk.util.Preconditions.checkArgumentNotNull;
import com.google.common.base.Throwables;
import google.registry.config.RegistryConfig.Config;
import google.registry.mosapi.MosApiException.MosApiAuthorizationException;
import jakarta.inject.Inject;
import jakarta.inject.Named;
import jakarta.inject.Singleton;
import java.io.IOException;
import java.net.HttpURLConnection;
import java.util.Map;
import okhttp3.HttpUrl;
import okhttp3.MediaType;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.RequestBody;
import okhttp3.Response;
@Singleton
public class MosApiClient {
private final OkHttpClient httpClient;
private final String baseUrl;
@Inject
public MosApiClient(
@Named("mosapiHttpClient") OkHttpClient httpClient,
@Config("mosapiServiceUrl") String mosapiUrl,
@Config("mosapiEntityType") String entityType) {
this.httpClient = httpClient;
// Pre-calculate base URL and validate it to fail fast on bad config
String fullUrl = String.format("%s/%s", mosapiUrl, entityType);
checkArgumentNotNull(
HttpUrl.parse(fullUrl), "Invalid MoSAPI Service URL configuration: %s", fullUrl);
this.baseUrl = fullUrl;
}
/**
* Sends a GET request to the specified MoSAPI endpoint.
*
* @param entityId The TLD or registrar ID the request is for.
* @param endpoint The specific API endpoint path (e.g., "v2/monitoring/state").
* @param params A map of query parameters to be URL-encoded and appended to the request.
* @param headers A map of HTTP headers to be included in the request.
* @return The {@link Response} from the server if the request is successful. <b>The caller is
* responsible for closing this response.</b>
* @throws MosApiException if the request fails due to a network error or an unhandled HTTP
* status.
* @throws MosApiAuthorizationException if the server returns a 401 Unauthorized status.
*/
public Response sendGetRequest(
String entityId, String endpoint, Map<String, String> params, Map<String, String> headers)
throws MosApiException {
HttpUrl url = buildUri(entityId, endpoint, params);
Request.Builder requestBuilder = new Request.Builder().url(url).get();
headers.forEach(requestBuilder::addHeader);
try {
Response response = httpClient.newCall(requestBuilder.build()).execute();
return checkResponseForAuthError(response);
} catch (RuntimeException | IOException e) {
// Check if it's the specific authorization exception (re-thrown or caught here)
Throwables.throwIfInstanceOf(e, MosApiAuthorizationException.class);
// Otherwise, treat as a generic connection/API error
throw new MosApiException("Error during GET request to " + url, e);
}
}
/**
* Sends a POST request to the specified MoSAPI endpoint.
*
* <p><b>Note:</b> This method is for future use. There are currently no MoSAPI endpoints in the
* project scope that require a POST request.
*
* @param entityId The TLD or registrar ID the request is for.
* @param endpoint The specific API endpoint path.
* @param params A map of query parameters to be URL-encoded.
* @param headers A map of HTTP headers to be included in the request.
* @param body The request body to be sent with the POST request.
* @return The {@link Response} from the server. <b>The caller is responsible for closing this
* response.</b>
* @throws MosApiException if the request fails.
* @throws MosApiAuthorizationException if the server returns a 401 Unauthorized status.
*/
public Response sendPostRequest(
String entityId,
String endpoint,
Map<String, String> params,
Map<String, String> headers,
String body)
throws MosApiException {
HttpUrl url = buildUri(entityId, endpoint, params);
RequestBody requestBody = RequestBody.create(body, MediaType.parse("application/json"));
Request.Builder requestBuilder = new Request.Builder().url(url).post(requestBody);
headers.forEach(requestBuilder::addHeader);
try {
Response response = httpClient.newCall(requestBuilder.build()).execute();
return checkResponseForAuthError(response);
} catch (RuntimeException | IOException e) {
// Check if it's the specific authorization exception (re-thrown or caught here)
Throwables.throwIfInstanceOf(e, MosApiAuthorizationException.class);
// Otherwise, treat as a generic connection/API error
throw new MosApiException("Error during POST request to " + url, e);
}
}
private Response checkResponseForAuthError(Response response)
throws MosApiAuthorizationException {
if (response.code() == HttpURLConnection.HTTP_UNAUTHORIZED) {
response.close();
throw new MosApiAuthorizationException(
"Authorization failed for the requested resource. The client certificate may not be"
+ " authorized for the specified TLD or Registrar.");
}
return response;
}
/**
* Builds the full URL for a request, including the base URL, entityId, path, and query params.
*/
private HttpUrl buildUri(String entityId, String path, Map<String, String> queryParams) {
String sanitizedPath = path.startsWith("/") ? path.substring(1) : path;
// We can safely use get() here because we validated baseUrl in the constructor
HttpUrl.Builder urlBuilder =
HttpUrl.get(baseUrl).newBuilder().addPathSegment(entityId).addPathSegments(sanitizedPath);
if (queryParams != null) {
queryParams.forEach(urlBuilder::addQueryParameter);
}
return urlBuilder.build();
}
}

View File

@@ -0,0 +1,110 @@
// Copyright 2025 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.mosapi;
import static java.lang.annotation.ElementType.TYPE;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
import google.registry.mosapi.model.MosApiErrorResponse;
import java.io.IOException;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import java.util.Optional;
/** Custom exception for MoSAPI client errors. */
public class MosApiException extends IOException {
private final MosApiErrorResponse errorResponse;
public MosApiException(MosApiErrorResponse errorResponse) {
super(
String.format(
"MoSAPI returned an error (code: %s): %s",
errorResponse.resultCode(), errorResponse.message()));
this.errorResponse = errorResponse;
}
public MosApiException(String message, Throwable cause) {
super(message, cause);
this.errorResponse = null;
}
public Optional<MosApiErrorResponse> getErrorResponse() {
return Optional.ofNullable(errorResponse);
}
/** Annotation for associating a MoSAPI result code with an exception subclass. */
@Documented
@Retention(RUNTIME)
@Target(TYPE)
public @interface MosApiResultCode {
String value();
}
/** Thrown when MoSAPI returns a 401 Unauthorized error. */
public static class MosApiAuthorizationException extends MosApiException {
public MosApiAuthorizationException(String message) {
super(message, null);
}
}
/** Creates a specific exception based on the MoSAPI error response. */
public static MosApiException create(MosApiErrorResponse errorResponse) {
Optional<MosApiResponse> responseEnum = MosApiResponse.fromCode(errorResponse.resultCode());
if (responseEnum.isPresent()) {
return switch (responseEnum.get()) {
case DATE_DURATION_INVALID -> new DateDurationInvalidException(errorResponse);
case DATE_ORDER_INVALID -> new DateOrderInvalidException(errorResponse);
case START_DATE_SYNTAX_INVALID -> new StartDateSyntaxInvalidException(errorResponse);
case END_DATE_SYNTAX_INVALID -> new EndDateSyntaxInvalidException(errorResponse);
default -> new MosApiException(errorResponse);
};
}
return new MosApiException(errorResponse);
}
/** Thrown when the date duration in a MoSAPI request is invalid. */
@MosApiResultCode("2011")
public static class DateDurationInvalidException extends MosApiException {
public DateDurationInvalidException(MosApiErrorResponse errorResponse) {
super(errorResponse);
}
}
/** Thrown when the date order in a MoSAPI request is invalid. */
@MosApiResultCode("2012")
public static class DateOrderInvalidException extends MosApiException {
public DateOrderInvalidException(MosApiErrorResponse errorResponse) {
super(errorResponse);
}
}
/** Thrown when the startDate syntax in a MoSAPI request is invalid. */
@MosApiResultCode("2013")
public static class StartDateSyntaxInvalidException extends MosApiException {
public StartDateSyntaxInvalidException(MosApiErrorResponse errorResponse) {
super(errorResponse);
}
}
/** Thrown when the endDate syntax in a MoSAPI request is invalid. */
@MosApiResultCode("2014")
public static class EndDateSyntaxInvalidException extends MosApiException {
public EndDateSyntaxInvalidException(MosApiErrorResponse errorResponse) {
super(errorResponse);
}
}
}

View File

@@ -0,0 +1,62 @@
// Copyright 2024 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.mosapi;
import java.util.Arrays;
import java.util.Map;
import java.util.Optional;
import java.util.function.Function;
import java.util.stream.Collectors;
/**
* Represents known MoSAPI API result codes and their default messages.
*
* <p>The definitions for these codes can be found in the official ICANN MoSAPI Specification,
* specifically in the 'Result Codes' section.
*
* @see <a href="https://www.icann.org/mosapi-specification.pdf">ICANN MoSAPI Specification</a>
*/
public enum MosApiResponse {
DATE_DURATION_INVALID(
"2011", "The difference between endDate and startDate is " + "more than 31 days"),
DATE_ORDER_INVALID("2012", "The EndDate is before startDate"),
START_DATE_SYNTAX_INVALID("2013", "StartDate syntax is invalid"),
END_DATE_SYNTAX_INVALID("2014", "EndDate syntax is invalid");
private final String code;
private final String defaultMessage;
private static final Map<String, MosApiResponse> CODE_MAP =
Arrays.stream(values()).collect(Collectors.toMap(e -> e.code, Function.identity()));
MosApiResponse(String code, String defaultMessage) {
this.code = code;
this.defaultMessage = defaultMessage;
}
public String getCode() {
return code;
}
public String getDefaultMessage() {
return defaultMessage;
}
// Returns the enum constant associated with the given result code string
public static Optional<MosApiResponse> fromCode(String code) {
return Optional.ofNullable(CODE_MAP.get(code));
}
}

View File

@@ -0,0 +1,23 @@
// Copyright 2025 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.mosapi.model;
/**
* Represents the generic JSON error response from the MoSAPI service for a 400 Bad Request.
*
* @see <a href="https://www.icann.org/mosapi-specification.pdf">ICANN MoSAPI Specification, Section
* 8</a>
*/
public record MosApiErrorResponse(String resultCode, String message, String description) {}

View File

@@ -0,0 +1,187 @@
// Copyright 2025 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.mosapi.module;
import dagger.Module;
import dagger.Provides;
import google.registry.config.RegistryConfig.Config;
import google.registry.privileges.secretmanager.SecretManagerClient;
import jakarta.inject.Named;
import jakarta.inject.Provider;
import jakarta.inject.Singleton;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.StringReader;
import java.nio.charset.StandardCharsets;
import java.security.GeneralSecurityException;
import java.security.KeyStore;
import java.security.PrivateKey;
import java.security.cert.Certificate;
import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;
import java.util.Optional;
import javax.net.ssl.KeyManagerFactory;
import javax.net.ssl.SSLContext;
import javax.net.ssl.TrustManagerFactory;
import javax.net.ssl.X509TrustManager;
import okhttp3.OkHttpClient;
import org.bouncycastle.asn1.pkcs.PrivateKeyInfo;
import org.bouncycastle.openssl.PEMKeyPair;
import org.bouncycastle.openssl.PEMParser;
import org.bouncycastle.openssl.jcajce.JcaPEMKeyConverter;
@Module
public final class MosApiModule {
// Secret Manager constants
private static final String LATEST_SECRET_VERSION = "latest";
// @Named annotations for Dagger
private static final String MOSAPI_TLS_CERT = "mosapiTlsCert";
private static final String MOSAPI_TLS_KEY = "mosapiTlsKey";
private static final String MOSAPI_SSL_CONTEXT = "mosapiSslContext";
private static final String MOSAPI_HTTP_CLIENT = "mosapiHttpClient";
// Cryptography-related constants
private static final String CERTIFICATE_TYPE = "X.509";
private static final String KEY_STORE_TYPE = "PKCS12";
private static final String KEY_STORE_ALIAS = "client";
private static final String SSL_CONTEXT_PROTOCOL = "TLS";
/**
* Provides a Provider for the MoSAPI TLS Cert.
*
* <p>This method returns a Dagger {@link Provider} that can be used to fetch the TLS Certs for a
* MosAPI.
*
* @param secretManagerClient The injected Secret Manager client.
* @param tlsCertSecretName The name of the secret in Secret Manager (from config).
* @return A Provider for the MoSAPI TLS Certs.
*/
@Provides
@Named(MOSAPI_TLS_CERT)
public static String provideMosapiTlsCert(
SecretManagerClient secretManagerClient,
@Config("mosapiTlsCertSecretName") String tlsCertSecretName) {
return secretManagerClient.getSecretData(tlsCertSecretName, Optional.of(LATEST_SECRET_VERSION));
}
/**
* Provides a Provider for the MoSAPI TLS Key.
*
* <p>This method returns a Dagger {@link Provider} that can be used to fetch the TLS Key for a
* MosAPI.
*
* @param secretManagerClient The injected Secret Manager client.
* @param tlsKeySecretName The name of the secret in Secret Manager (from config).
* @return A Provider for the MoSAPI TLS Key.
*/
@Provides
@Named(MOSAPI_TLS_KEY)
public static String provideMosapiTlsKey(
SecretManagerClient secretManagerClient,
@Config("mosapiTlsKeySecretName") String tlsKeySecretName) {
return secretManagerClient.getSecretData(tlsKeySecretName, Optional.of(LATEST_SECRET_VERSION));
}
@Provides
static Certificate provideCertificate(@Named(MOSAPI_TLS_CERT) String tlsCert) {
try {
CertificateFactory cf = CertificateFactory.getInstance(CERTIFICATE_TYPE);
return cf.generateCertificate(
new ByteArrayInputStream(tlsCert.getBytes(StandardCharsets.UTF_8)));
} catch (CertificateException e) {
throw new RuntimeException("Could not create X.509 certificate from provided PEM", e);
}
}
@Provides
static PrivateKey providePrivateKey(@Named(MOSAPI_TLS_KEY) String tlsKey) {
try (PEMParser pemParser = new PEMParser(new StringReader(tlsKey))) {
Object parsedObj = pemParser.readObject();
JcaPEMKeyConverter converter = new JcaPEMKeyConverter();
if (parsedObj instanceof PEMKeyPair) {
return converter.getPrivateKey(((PEMKeyPair) parsedObj).getPrivateKeyInfo());
} else if (parsedObj instanceof PrivateKeyInfo) {
return converter.getPrivateKey((PrivateKeyInfo) parsedObj);
}
throw new IllegalArgumentException(
String.format(
"Could not parse TLS private key; unexpected format %s",
parsedObj != null ? parsedObj.getClass().getName() : "null"));
} catch (IOException e) {
throw new RuntimeException("Could not parse TLS private key from PEM string", e);
}
}
@Provides
static KeyStore provideKeyStore(PrivateKey privateKey, Certificate certificate) {
try {
KeyStore keyStore = KeyStore.getInstance(KEY_STORE_TYPE);
keyStore.load(null, null);
keyStore.setKeyEntry(
KEY_STORE_ALIAS, privateKey, new char[0], new Certificate[] {certificate});
return keyStore;
} catch (GeneralSecurityException | IOException e) {
throw new RuntimeException("Could not create KeyStore for mTLS", e);
}
}
@Provides
static KeyManagerFactory provideKeyManagerFactory(KeyStore keyStore) {
try {
KeyManagerFactory kmf =
KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
kmf.init(keyStore, new char[0]);
return kmf;
} catch (GeneralSecurityException e) {
throw new RuntimeException("Could not initialize KeyManagerFactory", e);
}
}
@Provides
@Named(MOSAPI_SSL_CONTEXT)
static SSLContext provideSslContext(KeyManagerFactory keyManagerFactory) {
try {
SSLContext sslContext = SSLContext.getInstance(SSL_CONTEXT_PROTOCOL);
sslContext.init(keyManagerFactory.getKeyManagers(), null, null);
return sslContext;
} catch (GeneralSecurityException e) {
throw new RuntimeException("Could not initialize SSLContext", e);
}
}
@Provides
static X509TrustManager provideTrustManager() {
try {
TrustManagerFactory trustManagerFactory =
TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
trustManagerFactory.init((KeyStore) null);
return (X509TrustManager) trustManagerFactory.getTrustManagers()[0];
} catch (GeneralSecurityException e) {
throw new RuntimeException("Could not initialize TrustManager", e);
}
}
@Provides
@Singleton
@Named(MOSAPI_HTTP_CLIENT)
static OkHttpClient provideMosapiHttpClient(
@Named(MOSAPI_SSL_CONTEXT) SSLContext sslContext, X509TrustManager trustManager) {
return new OkHttpClient.Builder()
.sslSocketFactory(sslContext.getSocketFactory(), trustManager)
.build();
}
}

View File

@@ -0,0 +1,16 @@
// Copyright 2025 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.
@javax.annotation.ParametersAreNonnullByDefault
package google.registry.mosapi;

View File

@@ -33,10 +33,10 @@ import org.joda.time.Duration;
* Runner applying guaranteed reliability to an {@link EscrowTask}.
*
* <p>This class implements the <i>Locking Rolling Cursor</i> pattern, which solves the problem of
* how to reliably execute App Engine tasks which can't be made idempotent.
* how to reliably execute Cloud Tasks which can't be made idempotent.
*
* <p>{@link LockHandler} is used to ensure only one task executes at a time for a given {@code
* LockedCursorTask} subclass + TLD combination. This is necessary because App Engine tasks might
* LockedCursorTask} subclass + TLD combination. This is necessary because Cloud Task tasks might
* double-execute. Normally tasks solve this by being idempotent, but that's not possible for RDE,
* which writes to a GCS filename with a deterministic name. So locks are used to guarantee
* isolation. If we can't acquire the lock, it means the task is already running, so {@link

View File

@@ -100,8 +100,8 @@ import org.joda.time.Duration;
*
* <h2>Logging</h2>
*
* <p>To identify the reduce worker request for a deposit in App Engine's log viewer, you can use
* search text like {@code tld=soy}, {@code watermark=2015-01-01}, and {@code mode=FULL}.
* <p>To identify the reduce worker request for a deposit in the log viewer, you can use search text
* like {@code tld=soy}, {@code watermark=2015-01-01}, and {@code mode=FULL}.
*
* <h3>Error Handling</h3>
*

View File

@@ -107,7 +107,7 @@ public final class TransactionsReportingQueryBuilder implements QueryBuilder {
queriesBuilder.put(
getTableName(TRANSACTION_TRANSFER_LOSING, yearMonth), transactionTransferLosingQuery);
// App Engine log table suffixes use YYYYMMDD format
// Log table suffixes use YYYYMMDD format
DateTimeFormatter logTableFormatter = DateTimeFormat.forPattern("yyyyMMdd");
String attemptedAddsQuery =
SqlTemplate.create(getQueryFromFile(ATTEMPTED_ADDS + ".sql"))

View File

@@ -73,8 +73,8 @@ public abstract class HttpException extends RuntimeException {
/**
* Exception that causes a 204 response.
*
* <p>This is useful for App Engine task queue handlers that want to display an error, but don't
* want the task to automatically retry, since the status code is less than 300.
* <p>This is useful for task queue handlers that want to display an error, but don't want the
* task to automatically retry, since the status code is less than 300.
*/
public static final class NoContentException extends HttpException {
public NoContentException(String message) {

View File

@@ -25,7 +25,7 @@ import java.net.HttpURLConnection;
import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.SSLContext;
/** Dagger modules for App Engine services and other vendor classes. */
/** Dagger modules for services and other vendor classes. */
public final class Modules {
/** Dagger module for {@link UrlConnectionService}. */

View File

@@ -69,7 +69,7 @@ final class Router {
ImmutableSortedMap.Builder<String, Route> routes =
new ImmutableSortedMap.Builder<>(Ordering.natural());
for (Method method : componentClass.getMethods()) {
// Make App Engine's security manager happy.
// Make the security manager happy.
method.setAccessible(true);
if (!isDaggerInstantiatorOfType(Runnable.class, method)) {
continue;

View File

@@ -27,12 +27,12 @@ import java.util.Map;
/**
* Utility class to help in dumping routing maps.
*
* <p>Each of the App Engine services (frontend, backend, and tools) has a Dagger component used for
* routing requests (e.g., FrontendRequestComponent). This class produces a text file representation
* of the routing configuration, showing what paths map to what action classes, as well as the
* properties of the action classes' annotations (which cover things like allowable HTTP methods,
* authentication settings, etc.). The text file can be useful for documentation, and is also used
* in unit tests to check against golden routing maps to find possibly unexpected changes.
* <p>The request-handling service has a Dagger component (RequestComponent) used for routing
* requests. This class produces a text file representation of the routing configuration, showing
* what paths map to what action classes, as well as the properties of the action classes'
* annotations (which cover things like allowable HTTP methods, authentication settings, etc.). The
* text file can be useful for documentation, and is also used in unit tests to check against golden
* routing maps to find possibly unexpected changes.
*
* <p>The file has fixed-width columns with a header row. The width of the columns is determined by
* the content to be displayed. The columns are:

View File

@@ -43,6 +43,15 @@ public enum IdnTableEnum {
*/
UNCONFUSABLE_LATIN("unconfusable_latin.txt"),
/**
* ICANN LGR 2025 Latin, but with confusable characters removed.
*
* <p>This is based on <a
* href="https://www.icann.org/sites/default/files/packages/lgr/lgr-second-level-latin-full-variant-script-24jan24-en.html">ICANN's
* LGR table</a>, but is simpler.
*/
AUGMENTED_LATIN("augmented_latin.txt"),
/**
* Japanese, as used on our existing TLD launches prior to 2023.
*

View File

@@ -1,4 +1,4 @@
# URL: https://www.iana.org/domains/idn-tables/tables/google_latn_1.0.txt
# URL: https://www.iana.org/domains/idn-tables/tables/google_latn_3.0.txt
# Policy: https://www.registry.google/about/policies/domainabuse/
U+002D # HYPHEN-MINUS
U+0030 # DIGIT ZERO

View File

@@ -112,7 +112,7 @@ public final class NordnVerifyAction implements Runnable {
logger.atInfo().log(
"LORDN verify task %s response: HTTP response code %d", actionLogId, responseCode);
if (responseCode == SC_NO_CONTENT) {
// Send a 400+ status code so App Engine will retry the task.
// Send a 400+ status code so Cloud Tasks will retry the task.
throw new ConflictException("Not ready");
}
if (responseCode != SC_OK) {

View File

@@ -0,0 +1,158 @@
// Copyright 2025 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.tools;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.collect.ImmutableList.toImmutableList;
import static java.nio.charset.StandardCharsets.UTF_8;
import com.beust.jcommander.Parameter;
import com.beust.jcommander.Parameters;
import com.google.common.base.Splitter;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Iterables;
import com.google.common.io.Files;
import com.google.common.net.MediaType;
import com.google.gson.Gson;
import google.registry.batch.BulkDomainTransferAction;
import google.registry.model.registrar.Registrar;
import google.registry.util.DomainNameUtils;
import java.io.File;
import java.io.IOException;
import java.util.List;
/**
* A command to bulk-transfer any number of domains from one registrar to another.
*
* <p>This should be used as part of the BTAPPA (Bulk Transfer After a Partial Portfolio
* Acquisition) process in order to transfer a (possibly large) list of domains from one registrar
* to another, though it may be used in other situations as well.
*
* <p>For a true bulk transfer of domains, one should pass in a file with a list of domains (one per
* line) but if we need to do an ad-hoc transfer of one domain we can do that as well.
*
* <p>For BTAPPA purposes, we expect "requestedByRegistrar" to be true; this may not be the case for
* other purposes e.g. legal compliance transfers.
*/
@Parameters(
separators = " =",
commandDescription = "Transfer domain(s) in bulk with immediate effect.")
public class BulkDomainTransferCommand extends ConfirmingCommand implements CommandWithConnection {
// we don't need any configuration on the Gson because all we need is a list of strings
private static final Gson GSON = new Gson();
private static final int DOMAIN_TRANSFER_BATCH_SIZE = 1000;
@Parameter(
names = {"--domains"},
description =
"Comma-separated list of domains to transfer, otherwise use --domain_names_file to"
+ " specify a possibly-large list of domains")
private List<String> domains;
@Parameter(
names = {"-d", "--domain_names_file"},
description = "A file with a list of newline-delimited domain names to create tokens for")
private String domainNamesFile;
@Parameter(
names = {"-g", "--gaining_registrar_id"},
description = "The ID of the registrar to which domains should be transferred",
required = true)
private String gainingRegistrarId;
@Parameter(
names = {"-l", "--losing_registrar_id"},
description = "The ID of the registrar from which domains should be transferred",
required = true)
private String losingRegistrarId;
@Parameter(
names = {"--reason"},
description = "Reason to transfer the domains",
required = true)
private String reason;
@Parameter(
names = {"--registrar_request"},
description = "Whether the change was requested by a registrar.")
private boolean requestedByRegistrar = false;
@Parameter(
names = {"--max_qps"},
description =
"Maximum queries to run per second, otherwise the default (maxQps) will be used")
private int maxQps;
private ServiceConnection connection;
@Override
public void setConnection(ServiceConnection connection) {
this.connection = connection;
}
@Override
protected String prompt() throws Exception {
checkArgument(
domainNamesFile != null ^ (domains != null && !domains.isEmpty()),
"Must specify exactly one input method, either --domains or --domain_names_file");
return String.format("Attempt to transfer %d domains?", getDomainList().size());
}
@Override
protected String execute() throws Exception {
checkArgument(
Registrar.loadByRegistrarIdCached(gainingRegistrarId).isPresent(),
"Gaining registrar %s doesn't exist",
gainingRegistrarId);
checkArgument(
Registrar.loadByRegistrarIdCached(losingRegistrarId).isPresent(),
"Losing registrar %s doesn't exist",
losingRegistrarId);
ImmutableMap.Builder<String, Object> paramsBuilder = new ImmutableMap.Builder<>();
paramsBuilder.put("gainingRegistrarId", gainingRegistrarId);
paramsBuilder.put("losingRegistrarId", losingRegistrarId);
paramsBuilder.put("requestedByRegistrar", requestedByRegistrar);
paramsBuilder.put("reason", reason);
if (maxQps > 0) {
paramsBuilder.put("maxQps", maxQps);
}
ImmutableMap<String, Object> params = paramsBuilder.build();
for (List<String> batch : Iterables.partition(getDomainList(), DOMAIN_TRANSFER_BATCH_SIZE)) {
System.out.printf("Sending batch of %d domains\n", batch.size());
byte[] domainsList = GSON.toJson(batch).getBytes(UTF_8);
System.out.println(
connection.sendPostRequest(
BulkDomainTransferAction.PATH, params, MediaType.PLAIN_TEXT_UTF_8, domainsList));
}
return "";
}
private ImmutableList<String> getDomainList() throws IOException {
return domainNamesFile == null ? ImmutableList.copyOf(domains) : loadDomainsFromFile();
}
private ImmutableList<String> loadDomainsFromFile() throws IOException {
return Splitter.on('\n')
.omitEmptyStrings()
.trimResults()
.splitToStream(Files.asCharSource(new File(domainNamesFile), UTF_8).read())
.map(DomainNameUtils::canonicalizeHostname)
.collect(toImmutableList());
}
}

View File

@@ -16,17 +16,12 @@ package google.registry.tools;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Strings.isNullOrEmpty;
import static google.registry.model.common.FeatureFlag.FeatureName.MINIMUM_DATASET_CONTACTS_OPTIONAL;
import static google.registry.model.common.FeatureFlag.FeatureName.MINIMUM_DATASET_CONTACTS_PROHIBITED;
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
import static google.registry.pricing.PricingEngineProxy.getPricesForDomainName;
import static google.registry.util.PreconditionsUtils.checkArgumentNotNull;
import static org.joda.time.DateTimeZone.UTC;
import com.beust.jcommander.Parameter;
import com.beust.jcommander.Parameters;
import com.google.template.soy.data.SoyMapData;
import google.registry.model.common.FeatureFlag;
import google.registry.model.pricing.PremiumPricingEngine.DomainPrices;
import google.registry.tools.soy.DomainCreateSoyInfo;
import google.registry.util.StringGenerator;
@@ -62,15 +57,6 @@ final class CreateDomainCommand extends CreateOrUpdateDomainCommand {
@Override
protected void initMutatingEppToolCommand() {
tm().transact(
() -> {
if (!FeatureFlag.isActiveNow(MINIMUM_DATASET_CONTACTS_OPTIONAL)
&& !FeatureFlag.isActiveNow(MINIMUM_DATASET_CONTACTS_PROHIBITED)) {
checkArgumentNotNull(registrant, "Registrant must be specified");
checkArgument(!admins.isEmpty(), "At least one admin must be specified");
checkArgument(!techs.isEmpty(), "At least one tech must be specified");
}
});
if (isNullOrEmpty(password)) {
password = passwordGenerator.createString(PASSWORD_LENGTH);
}

View File

@@ -19,12 +19,12 @@ import static com.google.common.base.Strings.isNullOrEmpty;
import static com.google.common.base.Verify.verify;
import static com.google.common.collect.ImmutableList.toImmutableList;
import static com.google.common.collect.ImmutableSet.toImmutableSet;
import static google.registry.util.DomainNameUtils.canonicalizeHostname;
import static google.registry.util.RegistrarUtils.normalizeRegistrarName;
import static java.nio.charset.StandardCharsets.US_ASCII;
import static org.joda.time.DateTimeZone.UTC;
import com.beust.jcommander.Parameter;
import com.google.common.base.Ascii;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import google.registry.flows.certs.CertificateChecker;
@@ -305,20 +305,16 @@ abstract class CreateOrUpdateRegistrarCommand extends MutatingCommand {
if (!allowedTlds.isEmpty()) {
checkArgument(
addAllowedTlds.isEmpty(), "Can't specify both --allowedTlds and --addAllowedTlds");
ImmutableSet.Builder<String> allowedTldsBuilder = new ImmutableSet.Builder<>();
for (String allowedTld : allowedTlds) {
allowedTldsBuilder.add(canonicalizeHostname(allowedTld));
}
builder.setAllowedTlds(allowedTldsBuilder.build());
builder.setAllowedTlds(
allowedTlds.stream().map(Ascii::toLowerCase).collect(toImmutableSet()));
}
if (!addAllowedTlds.isEmpty()) {
ImmutableSet.Builder<String> allowedTldsBuilder = new ImmutableSet.Builder<>();
if (oldRegistrar != null) {
allowedTldsBuilder.addAll(oldRegistrar.getAllowedTlds());
}
for (String allowedTld : addAllowedTlds) {
allowedTldsBuilder.add(canonicalizeHostname(allowedTld));
}
allowedTldsBuilder.addAll(
addAllowedTlds.stream().map(Ascii::toLowerCase).collect(toImmutableSet()));
builder.setAllowedTlds(allowedTldsBuilder.build());
}
if (ipAllowList != null) {

View File

@@ -0,0 +1,84 @@
// Copyright 2025 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.tools;
import static com.google.common.base.Preconditions.checkArgument;
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
import com.beust.jcommander.Parameter;
import com.beust.jcommander.Parameters;
import google.registry.model.common.FeatureFlag;
import java.util.List;
/**
* Command to remove a {@link FeatureFlag} from the database entirely.
*
* <p>This should be used when a flag has been deprecated entirely, and we want to remove it, to
* avoid having old invalid data in the database.
*
* <p>This command uses the native query format so that it is able to delete values that are no
* longer part of the {@link FeatureFlag} enum.
*
* <p>This uses {@link ConfirmingCommand} instead of {@link MutatingCommand} because of the
* nonstandard deletion flow required by the fact that the enum constant may already have been
* removed.
*/
@Parameters(separators = " =", commandDescription = "Delete a FeatureFlag from the database")
public class DeleteFeatureFlagCommand extends ConfirmingCommand {
@Parameter(description = "Feature flag to delete", required = true)
private List<String> mainParameters;
@Override
protected boolean checkExecutionState() {
checkArgument(
mainParameters != null && !mainParameters.isEmpty() && !mainParameters.getFirst().isBlank(),
"Must provide a non-blank feature flag as the main parameter");
boolean exists =
tm().transact(
() ->
(long)
tm().getEntityManager()
.createNativeQuery(
"SELECT COUNT(*) FROM \"FeatureFlag\" WHERE feature_name ="
+ " :featureName",
long.class)
.setParameter("featureName", mainParameters.getFirst())
.getSingleResult()
> 0);
if (!exists) {
System.out.printf("No flag found with name '%s'", mainParameters.getFirst());
}
return exists;
}
@Override
protected String prompt() throws Exception {
return String.format("Delete feature flag named '%s'?", mainParameters.getFirst());
}
@Override
protected String execute() throws Exception {
String featureName = mainParameters.getFirst();
tm().transact(
() ->
tm().getEntityManager()
.createNativeQuery(
"DELETE FROM \"FeatureFlag\" WHERE feature_name = :featureName")
.setParameter("featureName", featureName)
.executeUpdate());
return String.format("Deleted feature flag with name '%s'", featureName);
}
}

View File

@@ -0,0 +1,100 @@
// Copyright 2025 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.tools;
import static com.google.common.base.Preconditions.checkState;
import com.beust.jcommander.Parameter;
import com.beust.jcommander.Parameters;
import com.google.common.collect.ImmutableMap;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonElement;
import com.google.gson.JsonParser;
import google.registry.config.RegistryConfig.Config;
import google.registry.request.Action.Service;
import jakarta.inject.Inject;
import java.io.IOException;
import java.util.Optional;
import javax.annotation.Nullable;
/** Command to manually perform an authenticated RDAP query. */
@Parameters(separators = " =", commandDescription = "Manually perform an authenticated RDAP query")
public final class RdapQueryCommand implements CommandWithConnection {
private static final Gson GSON = new GsonBuilder().setPrettyPrinting().create();
enum RdapQueryType {
DOMAIN("/rdap/domain/%s"),
DOMAIN_SEARCH("/rdap/domains", "name"),
NAMESERVER("/rdap/nameserver/%s"),
NAMESERVER_SEARCH("/rdap/nameservers", "name"),
ENTITY("/rdap/entity/%s"),
ENTITY_SEARCH("/rdap/entities", "fn");
private final String pathFormat;
private final Optional<String> searchParamKey;
RdapQueryType(String pathFormat) {
this(pathFormat, null);
}
RdapQueryType(String pathFormat, @Nullable String searchParamKey) {
this.pathFormat = pathFormat;
this.searchParamKey = Optional.ofNullable(searchParamKey);
}
String getQueryPath(String queryTerm) {
return String.format(pathFormat, queryTerm);
}
ImmutableMap<String, String> getQueryParameters(String queryTerm) {
return searchParamKey.map(key -> ImmutableMap.of(key, queryTerm)).orElse(ImmutableMap.of());
}
}
@Parameter(names = "--type", description = "The type of RDAP query to perform.", required = true)
private RdapQueryType type;
@Parameter(
description = "The main query term (e.g., a domain name or search pattern).",
required = true)
private String queryTerm;
@Inject ServiceConnection defaultConnection;
@Inject
@Config("useCanary")
boolean useCanary;
@Override
public void setConnection(ServiceConnection connection) {
this.defaultConnection = connection;
}
@Override
public void run() throws IOException {
checkState(defaultConnection != null, "ServiceConnection was not set by RegistryCli.");
String path = type.getQueryPath(queryTerm);
ImmutableMap<String, String> queryParams = type.getQueryParameters(queryTerm);
ServiceConnection pubapiConnection = defaultConnection.withService(Service.PUBAPI, useCanary);
String rdapResponse = pubapiConnection.sendGetRequest(path, queryParams);
JsonElement rdapJson = JsonParser.parseString(rdapResponse);
System.out.println(GSON.toJson(rdapJson));
}
}

View File

@@ -70,9 +70,6 @@ final class RegistryCli implements CommandRunner {
+ "Beam pipelines")
private String sqlAccessInfoFile = null;
@Parameter(names = "--gae", description = "Whether to use GAE runtime, instead of GKE")
private boolean useGae = false;
@Parameter(names = "--canary", description = "Whether to connect to the canary instances")
private boolean useCanary = false;
@@ -169,7 +166,6 @@ final class RegistryCli implements CommandRunner {
DaggerRegistryToolComponent.builder()
.credentialFilePath(credentialJson)
.sqlAccessInfoFile(sqlAccessInfoFile)
.useGke(!useGae)
.useCanary(useCanary)
.build();
@@ -203,7 +199,8 @@ final class RegistryCli implements CommandRunner {
"""
This error is likely the result of having another instance of
nomulus running at the same time. Check your system, shut down
the other instance, and try again.""");
the other instance, and try again.\
""");
System.err.println("===================================================================");
} else {
throw e;
@@ -213,7 +210,7 @@ final class RegistryCli implements CommandRunner {
}
private ServiceConnection getConnection() {
// Get the App Engine connection, advise the user if they are not currently logged in.
// Get the service connection, advise the user if they are not currently logged in.
if (connection == null) {
connection = component.serviceConnection();
}

View File

@@ -30,6 +30,7 @@ public final class RegistryTool {
public static final ImmutableMap<String, Class<? extends Command>> COMMAND_MAP =
new ImmutableMap.Builder<String, Class<? extends Command>>()
.put("ack_poll_messages", AckPollMessagesCommand.class)
.put("bulk_domain_transfer", BulkDomainTransferCommand.class)
.put("canonicalize_labels", CanonicalizeLabelsCommand.class)
.put("check_domain", CheckDomainCommand.class)
.put("check_domain_claims", CheckDomainClaimsCommand.class)
@@ -54,6 +55,7 @@ public final class RegistryTool {
.put("curl", CurlCommand.class)
.put("delete_allocation_tokens", DeleteAllocationTokensCommand.class)
.put("delete_domain", DeleteDomainCommand.class)
.put("delete_feature_flag", DeleteFeatureFlagCommand.class)
.put("delete_host", DeleteHostCommand.class)
.put("delete_premium_list", DeletePremiumListCommand.class)
.put("delete_reserved_list", DeleteReservedListCommand.class)
@@ -98,6 +100,7 @@ public final class RegistryTool {
.put("login", LoginCommand.class)
.put("logout", LogoutCommand.class)
.put("pending_escrow", PendingEscrowCommand.class)
.put("rdap_query", RdapQueryCommand.class)
.put("recreate_billing_recurrences", RecreateBillingRecurrencesCommand.class)
.put("registrar_poc", RegistrarPocCommand.class)
.put("renew_domain", RenewDomainCommand.class)

View File

@@ -135,6 +135,8 @@ interface RegistryToolComponent {
void inject(PendingEscrowCommand command);
void inject(RdapQueryCommand command);
void inject(RenewDomainCommand command);
void inject(SaveSqlCredentialCommand command);
@@ -180,9 +182,6 @@ interface RegistryToolComponent {
@BindsInstance
Builder sqlAccessInfoFile(@Nullable @Config("sqlAccessInfoFile") String sqlAccessInfoFile);
@BindsInstance
Builder useGke(@Config("useGke") boolean useGke);
@BindsInstance
Builder useCanary(@Config("useCanary") boolean useCanary);

View File

@@ -29,8 +29,8 @@ import google.registry.util.OidcTokenUtils;
/**
* Module for providing the HttpRequestFactory.
*
* <p>Localhost connections go to the App Engine dev server. The dev server differs from most HTTP
* connections in that they don't require OAuth2 credentials, but instead require a special cookie.
* <p>Localhost connections go to the dev server. The dev server differs from most HTTP connections
* in that it doesn't require OAuth2 credentials, but instead requires a special cookie.
*/
@Module
final class RequestFactoryModule {
@@ -61,10 +61,9 @@ final class RequestFactoryModule {
AUTHORIZATION,
"Bearer "
+ OidcTokenUtils.createOidcToken(credentialsBundle, oauthClientId));
// GAE request times out after 10 min, so here we set the timeout to 10 min. This is
// Requests time out after 10 min, so here we set the timeout to 10 min. This is
// needed to support some nomulus commands like updating premium lists that take
// a lot of time to complete.
// See
// a lot of time to complete. See
// https://developers.google.com/api-client-library/java/google-api-java-client/errors
request.setConnectTimeout(REQUEST_TIMEOUT_MS);
request.setReadTimeout(REQUEST_TIMEOUT_MS);

View File

@@ -47,8 +47,8 @@ import org.json.simple.JSONValue;
/**
* An HTTP connection to a service.
*
* <p>By default - connects to the TOOLS service in GAE and the BACKEND service in GKE. To create a
* Connection to another service, call the {@link #withService} function.
* <p>By default - connects the BACKEND service. To create a connection to another service, call the
* {@link #withService} function.
*/
public class ServiceConnection {

View File

@@ -22,7 +22,6 @@ import static google.registry.request.Action.Method.GET;
import static google.registry.request.Action.Method.POST;
import static google.registry.request.RequestParameters.PARAM_TLDS;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import google.registry.model.EppResourceUtils;
@@ -31,7 +30,6 @@ import google.registry.request.Action;
import google.registry.request.Parameter;
import google.registry.request.auth.Auth;
import google.registry.util.Clock;
import google.registry.util.NonFinalForTesting;
import jakarta.inject.Inject;
/** An action that lists domains, for use by the {@code nomulus list_domains} command. */
@@ -42,9 +40,6 @@ import jakarta.inject.Inject;
auth = Auth.AUTH_ADMIN)
public final class ListDomainsAction extends ListObjectsAction<Domain> {
/** An App Engine limitation on how many subqueries can be used in a single query. */
@VisibleForTesting @NonFinalForTesting static int maxNumSubqueries = 30;
public static final String PATH = "/_dr/admin/list/domains";
@Inject

View File

@@ -19,7 +19,7 @@
-- monthly GKE logs, searching for all create commands and associating
-- them with their corresponding registrars.
-- Example log generated by FlowReporter in App Engine and GKE logs:
-- Example log generated by FlowReporter in GKE logs:
--google.registry.flows.FlowReporter
-- recordToLogs: FLOW-LOG-SIGNATURE-METADATA:
--{"serverTrid":"oNwL2J2eRya7bh7c9oHIzg==-2360a","clientId":"ipmirror"

View File

@@ -0,0 +1,152 @@
// Copyright 2025 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.testing.DatabaseHelper.createTld;
import static google.registry.testing.DatabaseHelper.loadByEntity;
import static google.registry.testing.DatabaseHelper.persistDeletedDomain;
import static google.registry.testing.DatabaseHelper.persistDomainWithDependentResources;
import static google.registry.testing.DatabaseHelper.persistResource;
import static org.mockito.Mockito.mock;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.common.util.concurrent.RateLimiter;
import google.registry.flows.DaggerEppTestComponent;
import google.registry.flows.EppController;
import google.registry.flows.EppTestComponent.FakesAndMocksModule;
import google.registry.model.domain.Domain;
import google.registry.model.eppcommon.StatusValue;
import google.registry.persistence.transaction.JpaTestExtensions;
import google.registry.persistence.transaction.JpaTestExtensions.JpaIntegrationTestExtension;
import google.registry.testing.FakeClock;
import google.registry.testing.FakeLockHandler;
import google.registry.testing.FakeResponse;
import google.registry.util.DateTimeUtils;
import org.joda.time.DateTime;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;
/** Tests for {@link BulkDomainTransferAction}. */
public class BulkDomainTransferActionTest {
private final FakeClock fakeClock = new FakeClock(DateTime.parse("2024-01-01T00:00:00.000Z"));
@RegisterExtension
final JpaIntegrationTestExtension jpa =
new JpaTestExtensions.Builder().withClock(fakeClock).buildIntegrationTestExtension();
private final FakeResponse response = new FakeResponse();
private final RateLimiter rateLimiter = mock(RateLimiter.class);
private Domain activeDomain;
private Domain alreadyTransferredDomain;
private Domain pendingDeleteDomain;
private Domain deletedDomain;
@BeforeEach
void beforeEach() throws Exception {
createTld("tld");
DateTime now = fakeClock.nowUtc();
// The default registrar is TheRegistrar, which will be the losing registrar
activeDomain =
persistDomainWithDependentResources(
"active", "tld", null, now, now.minusDays(1), DateTimeUtils.END_OF_TIME);
alreadyTransferredDomain =
persistResource(
persistDomainWithDependentResources(
"alreadytransferred",
"tld",
null,
now,
now.minusDays(1),
DateTimeUtils.END_OF_TIME)
.asBuilder()
.setPersistedCurrentSponsorRegistrarId("NewRegistrar")
.build());
pendingDeleteDomain =
persistResource(
persistDomainWithDependentResources(
"pendingdelete", "tld", null, now, now.minusDays(1), now.plusMonths(1))
.asBuilder()
.setStatusValues(ImmutableSet.of(StatusValue.PENDING_DELETE))
.build());
deletedDomain = persistDeletedDomain("deleted.tld", now.minusMonths(1));
}
@Test
void testSuccess_normalRun() {
assertThat(activeDomain.getCurrentSponsorRegistrarId()).isEqualTo("TheRegistrar");
assertThat(alreadyTransferredDomain.getCurrentSponsorRegistrarId()).isEqualTo("NewRegistrar");
assertThat(pendingDeleteDomain.getCurrentSponsorRegistrarId()).isEqualTo("TheRegistrar");
assertThat(deletedDomain.getCurrentSponsorRegistrarId()).isEqualTo("TheRegistrar");
DateTime preRunTime = fakeClock.nowUtc();
BulkDomainTransferAction action =
createAction("active.tld", "alreadytransferred.tld", "pendingdelete.tld", "deleted.tld");
fakeClock.advanceOneMilli();
DateTime runTime = fakeClock.nowUtc();
action.run();
fakeClock.advanceOneMilli();
DateTime now = fakeClock.nowUtc();
// The active domain should have a new update timestamp and current registrar
// The cloneProjectedAtTime calls are necessary to resolve the transfers, even though the
// transfers have a time period of 0
activeDomain = loadByEntity(activeDomain);
assertThat(activeDomain.cloneProjectedAtTime(now).getCurrentSponsorRegistrarId())
.isEqualTo("NewRegistrar");
assertThat(activeDomain.getUpdateTimestamp().getTimestamp()).isEqualTo(runTime);
// The other three domains shouldn't change
alreadyTransferredDomain = loadByEntity(alreadyTransferredDomain);
assertThat(alreadyTransferredDomain.cloneProjectedAtTime(now).getCurrentSponsorRegistrarId())
.isEqualTo("NewRegistrar");
assertThat(alreadyTransferredDomain.getUpdateTimestamp().getTimestamp()).isEqualTo(preRunTime);
pendingDeleteDomain = loadByEntity(pendingDeleteDomain);
assertThat(pendingDeleteDomain.cloneProjectedAtTime(now).getCurrentSponsorRegistrarId())
.isEqualTo("TheRegistrar");
assertThat(pendingDeleteDomain.getUpdateTimestamp().getTimestamp()).isEqualTo(preRunTime);
deletedDomain = loadByEntity(deletedDomain);
assertThat(deletedDomain.cloneProjectedAtTime(now).getCurrentSponsorRegistrarId())
.isEqualTo("TheRegistrar");
assertThat(deletedDomain.getUpdateTimestamp().getTimestamp()).isEqualTo(preRunTime);
}
private BulkDomainTransferAction createAction(String... domains) {
EppController eppController =
DaggerEppTestComponent.builder()
.fakesAndMocksModule(FakesAndMocksModule.create(new FakeClock()))
.build()
.startRequest()
.eppController();
return new BulkDomainTransferAction(
eppController,
new FakeLockHandler(true),
rateLimiter,
ImmutableList.copyOf(domains),
"NewRegistrar",
"TheRegistrar",
true,
"reason",
response);
}
}

View File

@@ -335,7 +335,7 @@ class InvoicingPipelineTest {
.build();
persistResource(registrar);
Tld test =
newTld("test", "_TEST", ImmutableSortedMap.of(START_OF_TIME, GENERAL_AVAILABILITY))
newTld("test", "TEST", ImmutableSortedMap.of(START_OF_TIME, GENERAL_AVAILABILITY))
.asBuilder()
.setInvoicingEnabled(true)
.build();
@@ -391,7 +391,7 @@ class InvoicingPipelineTest {
// Test that comments are removed from the .sql file correctly
assertThat(InvoicingPipeline.makeCloudSqlQuery("2017-10"))
.isEqualTo(
"""
"""
SELECT b, r FROM BillingEvent b
JOIN Registrar r ON b.clientId = r.registrarId
@@ -449,13 +449,13 @@ AND cr.id IS NULL
persistResource(registrar3);
Tld test =
newTld("test", "_TEST", ImmutableSortedMap.of(START_OF_TIME, GENERAL_AVAILABILITY))
newTld("test", "TEST", ImmutableSortedMap.of(START_OF_TIME, GENERAL_AVAILABILITY))
.asBuilder()
.setInvoicingEnabled(true)
.build();
persistResource(test);
Tld hello =
newTld("hello", "_HELLO", ImmutableSortedMap.of(START_OF_TIME, GENERAL_AVAILABILITY))
newTld("hello", "HELLO", ImmutableSortedMap.of(START_OF_TIME, GENERAL_AVAILABILITY))
.asBuilder()
.setInvoicingEnabled(true)
.build();

View File

@@ -163,7 +163,7 @@ public class RegistryJpaReadTest {
}
private void setupForJoinQuery() {
Tld registry = newTld("com", "ABCD_APP");
Tld registry = newTld("com", "ABCDAPP");
Registrar registrar =
makeRegistrar1()
.asBuilder()

View File

@@ -17,6 +17,7 @@ package google.registry.bsa;
import static com.google.common.truth.Truth.assertThat;
import static google.registry.testing.DatabaseHelper.createTld;
import static google.registry.testing.DatabaseHelper.persistResource;
import static google.registry.tldconfig.idn.IdnTableEnum.AUGMENTED_LATIN;
import static google.registry.tldconfig.idn.IdnTableEnum.EXTENDED_LATIN;
import static google.registry.tldconfig.idn.IdnTableEnum.JA;
import static google.registry.tldconfig.idn.IdnTableEnum.UNCONFUSABLE_LATIN;
@@ -43,6 +44,7 @@ public class IdnCheckerTest {
Tld jaonly;
Tld jandelatin;
Tld strictlatin;
Tld auglatin;
IdnChecker idnChecker;
@BeforeEach
@@ -50,6 +52,7 @@ public class IdnCheckerTest {
jaonly = createTld("jaonly");
jandelatin = createTld("jandelatin");
strictlatin = createTld("strictlatin");
auglatin = createTld("auglatin");
jaonly =
persistResource(
@@ -72,6 +75,13 @@ public class IdnCheckerTest {
.setBsaEnrollStartTime(Optional.of(fakeClock.nowUtc()))
.setIdnTables(ImmutableSet.of(UNCONFUSABLE_LATIN))
.build());
auglatin =
persistResource(
auglatin
.asBuilder()
.setBsaEnrollStartTime(Optional.of(fakeClock.nowUtc()))
.setIdnTables(ImmutableSet.of(AUGMENTED_LATIN))
.build());
fakeClock.advanceOneMilli();
idnChecker = new IdnChecker(fakeClock);
}
@@ -79,12 +89,13 @@ public class IdnCheckerTest {
@Test
void getAllValidIdns_allTlds() {
assertThat(idnChecker.getAllValidIdns("all"))
.containsExactly(EXTENDED_LATIN, JA, UNCONFUSABLE_LATIN);
.containsExactly(EXTENDED_LATIN, JA, UNCONFUSABLE_LATIN, AUGMENTED_LATIN);
}
@Test
void getAllValidIdns_notJa() {
assertThat(idnChecker.getAllValidIdns("à")).containsExactly(EXTENDED_LATIN, UNCONFUSABLE_LATIN);
assertThat(idnChecker.getAllValidIdns("à"))
.containsExactly(EXTENDED_LATIN, UNCONFUSABLE_LATIN, AUGMENTED_LATIN);
}
@Test
@@ -116,6 +127,7 @@ public class IdnCheckerTest {
@Test
void getForbiddingTlds_success() {
assertThat(idnChecker.getForbiddingTlds(ImmutableSet.of("JA"))).containsExactly(strictlatin);
assertThat(idnChecker.getForbiddingTlds(ImmutableSet.of("JA")))
.containsExactly(strictlatin, auglatin);
}
}

View File

@@ -1,110 +0,0 @@
// Copyright 2017 The Nomulus Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package google.registry.flows;
import static google.registry.model.eppoutput.Result.Code.SUCCESS;
import static google.registry.model.eppoutput.Result.Code.SUCCESS_WITH_ACK_MESSAGE;
import static google.registry.model.eppoutput.Result.Code.SUCCESS_WITH_NO_MESSAGES;
import static google.registry.testing.EppMetricSubject.assertThat;
import com.google.common.collect.ImmutableMap;
import google.registry.persistence.transaction.JpaTestExtensions;
import google.registry.persistence.transaction.JpaTestExtensions.JpaIntegrationTestExtension;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;
/** Tests for contact lifecycle. */
class EppLifecycleContactTest extends EppTestCase {
@RegisterExtension
final JpaIntegrationTestExtension jpa =
new JpaTestExtensions.Builder().withClock(clock).buildIntegrationTestExtension();
@Test
void testContactLifecycle() throws Exception {
assertThatLoginSucceeds("NewRegistrar", "foo-BAR2");
assertThatCommand("contact_create_sh8013.xml")
.atTime("2000-06-01T00:00:00Z")
.hasResponse(
"contact_create_response_sh8013.xml",
ImmutableMap.of("CRDATE", "2000-06-01T00:00:00Z"));
assertThat(getRecordedEppMetric())
.hasClientId("NewRegistrar")
.and()
.hasNoTld()
.and()
.hasCommandName("ContactCreate")
.and()
.hasStatus(SUCCESS);
assertThatCommand("contact_info.xml")
.atTime("2000-06-01T00:01:00Z")
.hasResponse("contact_info_from_create_response.xml");
assertThat(getRecordedEppMetric())
.hasClientId("NewRegistrar")
.and()
.hasCommandName("ContactInfo")
.and()
.hasStatus(SUCCESS);
assertThatCommand("contact_delete_sh8013.xml")
.hasResponse("contact_delete_response_sh8013.xml");
assertThat(getRecordedEppMetric())
.hasClientId("NewRegistrar")
.and()
.hasCommandName("ContactDelete")
.and()
.hasStatus(SUCCESS);
assertThatLogoutSucceeds();
}
@Test
void testContactTransferPollMessage() throws Exception {
assertThatLoginSucceeds("NewRegistrar", "foo-BAR2");
assertThatCommand("contact_create_sh8013.xml")
.atTime("2000-06-01T00:00:00Z")
.hasResponse(
"contact_create_response_sh8013.xml",
ImmutableMap.of("CRDATE", "2000-06-01T00:00:00Z"));
assertThatLogoutSucceeds();
// Initiate a transfer of the newly created contact.
assertThatLoginSucceeds("TheRegistrar", "password2");
assertThatCommand("contact_transfer_request.xml")
.atTime("2000-06-08T22:00:00Z")
.hasResponse("contact_transfer_request_response_alternate.xml");
assertThatLogoutSucceeds();
// Log back in with the losing registrar, read the poll message, and then ack it.
assertThatLoginSucceeds("NewRegistrar", "foo-BAR2");
assertThatCommand("poll.xml")
.atTime("2000-06-08T22:01:00Z")
.hasResponse("poll_response_contact_transfer.xml");
assertThat(getRecordedEppMetric())
.hasClientId("NewRegistrar")
.and()
.hasCommandName("PollRequest")
.and()
.hasStatus(SUCCESS_WITH_ACK_MESSAGE);
assertThatCommand("poll_ack.xml", ImmutableMap.of("ID", "6-2000"))
.atTime("2000-06-08T22:02:00Z")
.hasResponse("poll_ack_response_empty.xml");
assertThat(getRecordedEppMetric())
.hasClientId("NewRegistrar")
.and()
.hasCommandName("PollAck")
.and()
.hasStatus(SUCCESS_WITH_NO_MESSAGES);
assertThatLogoutSucceeds();
}
}

View File

@@ -16,8 +16,6 @@ package google.registry.flows;
import static com.google.common.truth.Truth.assertThat;
import static google.registry.model.ForeignKeyUtils.loadResource;
import static google.registry.model.common.FeatureFlag.FeatureName.MINIMUM_DATASET_CONTACTS_OPTIONAL;
import static google.registry.model.common.FeatureFlag.FeatureStatus.INACTIVE;
import static google.registry.model.eppoutput.Result.Code.SUCCESS;
import static google.registry.model.eppoutput.Result.Code.SUCCESS_AND_CLOSE;
import static google.registry.model.eppoutput.Result.Code.SUCCESS_WITH_ACTION_PENDING;
@@ -42,7 +40,6 @@ import com.google.re2j.Matcher;
import com.google.re2j.Pattern;
import google.registry.model.billing.BillingBase.Reason;
import google.registry.model.billing.BillingEvent;
import google.registry.model.common.FeatureFlag;
import google.registry.model.domain.Domain;
import google.registry.model.domain.DomainHistory;
import google.registry.model.reporting.HistoryEntry.Type;
@@ -76,19 +73,12 @@ class EppLifecycleDomainTest extends EppTestCase {
@BeforeEach
void beforeEach() {
persistResource(
new FeatureFlag()
.asBuilder()
.setFeatureName(MINIMUM_DATASET_CONTACTS_OPTIONAL)
.setStatusMap(ImmutableSortedMap.of(START_OF_TIME, INACTIVE))
.build());
createTlds("example", "tld");
}
@Test
void testDomainDeleteRestore() throws Exception {
assertThatLoginSucceeds("NewRegistrar", "foo-BAR2");
createContacts(DateTime.parse("2000-06-01T00:00:00Z"));
// Create domain example.tld
assertThatCommand(
@@ -148,7 +138,6 @@ class EppLifecycleDomainTest extends EppTestCase {
@Test
void testDomainDeleteRestore_duringAutorenewGracePeriod() throws Exception {
assertThatLoginSucceeds("NewRegistrar", "foo-BAR2");
createContacts(DateTime.parse("2000-06-01T00:00:00Z"));
// Create domain example.tld
assertThatCommand(
@@ -222,7 +211,6 @@ class EppLifecycleDomainTest extends EppTestCase {
@Test
void testDomainDeleteRestore_duringRenewalGracePeriod() throws Exception {
assertThatLoginSucceeds("NewRegistrar", "foo-BAR2");
createContacts(DateTime.parse("2000-06-01T00:00:00Z"));
// Create domain example.tld
assertThatCommand(
@@ -304,7 +292,6 @@ class EppLifecycleDomainTest extends EppTestCase {
@Test
void testDomainDelete_duringAddAndRenewalGracePeriod_deletesImmediately() throws Exception {
assertThatLoginSucceeds("NewRegistrar", "foo-BAR2");
createContacts(DateTime.parse("2000-06-01T00:00:00Z"));
DateTime createTime = DateTime.parse("2000-06-01T00:02:00Z");
// Create domain example.tld
@@ -396,7 +383,6 @@ class EppLifecycleDomainTest extends EppTestCase {
@Test
void testDomainDeletion_withinAddGracePeriod_deletesImmediately() throws Exception {
assertThatLoginSucceeds("NewRegistrar", "foo-BAR2");
createContacts(DateTime.parse("2000-06-01T00:00:00Z"));
// Create domain example.tld
DateTime createTime = DateTime.parse("2000-06-01T00:02:00Z");
@@ -450,7 +436,6 @@ class EppLifecycleDomainTest extends EppTestCase {
@Test
void testDomainDeletion_outsideAddGracePeriod_showsRedemptionPeriod() throws Exception {
assertThatLoginSucceeds("NewRegistrar", "foo-BAR2");
createContacts(DateTime.parse("2000-06-01T00:00:00Z"));
DateTime createTime = DateTime.parse("2000-06-01T00:02:00Z");
// Create domain example.tld
@@ -509,7 +494,6 @@ class EppLifecycleDomainTest extends EppTestCase {
@Test
void testEapDomainDeletion_withinAddGracePeriod_eapFeeIsNotRefunded() throws Exception {
assertThatCommand("login_valid_fee_extension.xml").hasSuccessfulLogin();
createContacts(DateTime.parse("2000-06-01T00:00:00Z"));
// Set the EAP schedule.
persistResource(
@@ -697,7 +681,7 @@ class EppLifecycleDomainTest extends EppTestCase {
assertThatLoginSucceeds("NewRegistrar", "foo-BAR2");
createContactsAndHosts();
createHosts();
assertThatCommand("domain_create_sunrise_encoded_mark.xml")
.atTime(sunriseDate.minusDays(1))
@@ -760,11 +744,11 @@ class EppLifecycleDomainTest extends EppTestCase {
.hasResponse(
"poll_response_autorenew.xml",
ImmutableMap.of(
"ID", "15-2002",
"ID", "11-2002",
"QDATE", "2002-06-01T00:04:00Z",
"DOMAIN", "fakesite.example",
"EXDATE", "2003-06-01T00:04:00Z"));
assertThatCommand("poll_ack.xml", ImmutableMap.of("ID", "15-2002"))
assertThatCommand("poll_ack.xml", ImmutableMap.of("ID", "11-2002"))
.atTime("2002-07-01T00:02:00Z")
.hasResponse("poll_ack_response_empty.xml");
@@ -778,13 +762,13 @@ class EppLifecycleDomainTest extends EppTestCase {
.hasResponse(
"poll_response_autorenew.xml",
ImmutableMap.of(
"ID", "15-2003", // Note -- Year is different from previous ID.
"ID", "11-2003", // Note -- Year is different from previous ID.
"QDATE", "2003-06-01T00:04:00Z",
"DOMAIN", "fakesite.example",
"EXDATE", "2004-06-01T00:04:00Z"));
// Ack the second poll message and verify that none remain.
assertThatCommand("poll_ack.xml", ImmutableMap.of("ID", "15-2003"))
assertThatCommand("poll_ack.xml", ImmutableMap.of("ID", "11-2003"))
.atTime("2003-07-01T00:05:05Z")
.hasResponse("poll_ack_response_empty.xml");
assertThatCommand("poll.xml")
@@ -814,7 +798,7 @@ class EppLifecycleDomainTest extends EppTestCase {
// As the losing registrar, read the request poll message, and then ack it.
assertThatLoginSucceeds("NewRegistrar", "foo-BAR2");
String messageId = "24-2001";
String messageId = "20-2001";
assertThatCommand("poll.xml")
.atTime("2001-01-01T00:01:00Z")
.hasResponse("poll_response_domain_transfer_request.xml", ImmutableMap.of("ID", messageId));
@@ -823,7 +807,7 @@ class EppLifecycleDomainTest extends EppTestCase {
.hasResponse("poll_ack_response_empty.xml");
// Five days in the future, expect a server approval poll message to the loser, and ack it.
messageId = "23-2001";
messageId = "19-2001";
assertThatCommand("poll.xml")
.atTime("2001-01-06T00:01:00Z")
.hasResponse(
@@ -835,7 +819,7 @@ class EppLifecycleDomainTest extends EppTestCase {
assertThatLogoutSucceeds();
// Also expect a server approval poll message to the winner, with the transfer request trid.
messageId = "22-2001";
messageId = "18-2001";
assertThatLoginSucceeds("TheRegistrar", "password2");
assertThatCommand("poll.xml")
.atTime("2001-01-06T00:02:00Z")
@@ -1113,7 +1097,6 @@ class EppLifecycleDomainTest extends EppTestCase {
createTlds("bar.foo.tld", "foo.tld");
assertThatLoginSucceeds("NewRegistrar", "foo-BAR2");
createContacts(DateTime.parse("2000-06-01T00:00:00.000Z"));
// Create domain example.bar.foo.tld
assertThatCommand(
@@ -1157,7 +1140,6 @@ class EppLifecycleDomainTest extends EppTestCase {
createTld("tld.foo");
assertThatLoginSucceeds("NewRegistrar", "foo-BAR2");
createContacts(DateTime.parse("2000-06-01T00:00:00.000Z"));
// Create domain example.tld.foo
assertThatCommand(
@@ -1207,7 +1189,7 @@ class EppLifecycleDomainTest extends EppTestCase {
.atTime(sunriseDate.minusDays(3))
.hasSuccessfulLogin();
createContactsAndHosts();
createHosts();
// During pre-delegation, any create should fail both with and without mark
assertThatCommand("domain_create_sunrise_encoded_mark.xml", ImmutableMap.of("SMD", ENCODED_SMD))
@@ -1235,9 +1217,10 @@ class EppLifecycleDomainTest extends EppTestCase {
.hasResponse(
"response_error.xml",
ImmutableMap.of(
"CODE", "2306",
"CODE",
"2306",
"MSG",
"Declared launch extension phase does not match the current registry phase"));
"Declared launch extension phase does not match the current registry phase"));
// During sunrise, create with mark will succeed but without will fail.
// We also test we can delete without a mark.
@@ -1255,8 +1238,7 @@ class EppLifecycleDomainTest extends EppTestCase {
.hasResponse("generic_success_response.xml");
assertThatCommand(
"domain_create_no_hosts_or_dsdata.xml",
ImmutableMap.of("DOMAIN", "general.example"))
"domain_create_no_hosts_or_dsdata.xml", ImmutableMap.of("DOMAIN", "general.example"))
.atTime(sunriseDate.plusDays(2))
.hasResponse(
"response_error.xml",
@@ -1270,9 +1252,10 @@ class EppLifecycleDomainTest extends EppTestCase {
.hasResponse(
"response_error.xml",
ImmutableMap.of(
"CODE", "2306",
"CODE",
"2306",
"MSG",
"Declared launch extension phase does not match the current registry phase"));
"Declared launch extension phase does not match the current registry phase"));
assertThatCommand(
"domain_create_no_hosts_or_dsdata.xml", ImmutableMap.of("DOMAIN", "general.example"))
@@ -1304,7 +1287,7 @@ class EppLifecycleDomainTest extends EppTestCase {
.atTime(sunriseDate.minusDays(3))
.hasSuccessfulLogin();
createContactsAndHosts();
createHosts();
// During start-date sunrise, create with mark will succeed but without will fail.
// We also test we can delete without a mark.
@@ -1459,7 +1442,6 @@ class EppLifecycleDomainTest extends EppTestCase {
void testDomainUpdateBySuperuser_sendsPollMessage() throws Exception {
setIsSuperuser(false);
assertThatLoginSucceeds("NewRegistrar", "foo-BAR2");
createContacts(DateTime.parse("2000-06-01T00:00:00Z"));
// Create domain example.tld
assertThatCommand(

View File

@@ -16,19 +16,13 @@ package google.registry.flows;
import static com.google.common.truth.Truth.assertThat;
import static google.registry.model.ForeignKeyUtils.loadResource;
import static google.registry.model.common.FeatureFlag.FeatureName.MINIMUM_DATASET_CONTACTS_OPTIONAL;
import static google.registry.model.common.FeatureFlag.FeatureStatus.INACTIVE;
import static google.registry.model.eppoutput.Result.Code.SUCCESS;
import static google.registry.testing.DatabaseHelper.createTld;
import static google.registry.testing.DatabaseHelper.createTlds;
import static google.registry.testing.DatabaseHelper.persistResource;
import static google.registry.testing.EppMetricSubject.assertThat;
import static google.registry.testing.HostSubject.assertAboutHosts;
import static google.registry.util.DateTimeUtils.START_OF_TIME;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSortedMap;
import google.registry.model.common.FeatureFlag;
import google.registry.model.domain.Domain;
import google.registry.model.host.Host;
import google.registry.persistence.transaction.JpaTestExtensions;
@@ -94,23 +88,9 @@ class EppLifecycleHostTest extends EppTestCase {
@Test
void testRenamingHostToExistingHost_fails() throws Exception {
persistResource(
new FeatureFlag()
.asBuilder()
.setFeatureName(MINIMUM_DATASET_CONTACTS_OPTIONAL)
.setStatusMap(ImmutableSortedMap.of(START_OF_TIME, INACTIVE))
.build());
createTld("example");
assertThatLoginSucceeds("NewRegistrar", "foo-BAR2");
// Create the fakesite domain.
assertThatCommand("contact_create_sh8013.xml")
.atTime("2000-06-01T00:00:00Z")
.hasResponse(
"contact_create_response_sh8013.xml",
ImmutableMap.of("CRDATE", "2000-06-01T00:00:00Z"));
assertThatCommand("contact_create_jd1234.xml")
.atTime("2000-06-01T00:01:00Z")
.hasResponse("contact_create_response_jd1234.xml");
assertThatCommand("domain_create_fakesite_no_nameservers.xml")
.atTime("2000-06-01T00:04:00Z")
.hasResponse(
@@ -150,25 +130,10 @@ class EppLifecycleHostTest extends EppTestCase {
@Test
void testSuccess_multipartTldsWithSharedSuffixes() throws Exception {
persistResource(
new FeatureFlag()
.asBuilder()
.setFeatureName(MINIMUM_DATASET_CONTACTS_OPTIONAL)
.setStatusMap(ImmutableSortedMap.of(START_OF_TIME, INACTIVE))
.build());
createTlds("bar.foo.tld", "foo.tld", "tld");
assertThatLoginSucceeds("NewRegistrar", "foo-BAR2");
assertThatCommand("contact_create_sh8013.xml")
.atTime("2000-06-01T00:00:00Z")
.hasResponse(
"contact_create_response_sh8013.xml",
ImmutableMap.of("CRDATE", "2000-06-01T00:00:00Z"));
assertThatCommand("contact_create_jd1234.xml")
.atTime("2000-06-01T00:01:00Z")
.hasResponse("contact_create_response_jd1234.xml");
// Create domain example.bar.foo.tld
assertThatCommand(
"domain_create_no_hosts_or_dsdata.xml",

View File

@@ -17,24 +17,18 @@ package google.registry.flows;
import static com.google.common.truth.Truth.assertThat;
import static google.registry.model.EppResourceUtils.loadAtPointInTime;
import static google.registry.model.ImmutableObjectSubject.assertAboutImmutableObjects;
import static google.registry.model.common.FeatureFlag.FeatureName.MINIMUM_DATASET_CONTACTS_OPTIONAL;
import static google.registry.model.common.FeatureFlag.FeatureStatus.INACTIVE;
import static google.registry.testing.DatabaseHelper.createTld;
import static google.registry.testing.DatabaseHelper.loadAllOf;
import static google.registry.testing.DatabaseHelper.loadByEntity;
import static google.registry.testing.DatabaseHelper.persistActiveContact;
import static google.registry.testing.DatabaseHelper.persistActiveHost;
import static google.registry.testing.DatabaseHelper.persistResource;
import static google.registry.util.DateTimeUtils.START_OF_TIME;
import static java.nio.charset.StandardCharsets.UTF_8;
import static org.joda.time.DateTimeZone.UTC;
import static org.joda.time.Duration.standardDays;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSortedMap;
import com.google.common.collect.Iterables;
import google.registry.flows.EppTestComponent.FakesAndMocksModule;
import google.registry.model.common.FeatureFlag;
import google.registry.model.domain.Domain;
import google.registry.monitoring.whitebox.EppMetric;
import google.registry.persistence.transaction.JpaTestExtensions;
@@ -60,12 +54,6 @@ class EppPointInTimeTest {
@BeforeEach
void beforeEach() {
persistResource(
new FeatureFlag()
.asBuilder()
.setFeatureName(MINIMUM_DATASET_CONTACTS_OPTIONAL)
.setStatusMap(ImmutableSortedMap.of(START_OF_TIME, INACTIVE))
.build());
createTld("tld");
}

View File

@@ -223,10 +223,9 @@ public class EppTestCase {
return eppMetricBuilder.build();
}
/** Create the two administrative contacts and two hosts. */
void createContactsAndHosts() throws Exception {
/** Create the two hosts. */
void createHosts() throws Exception {
DateTime createTime = DateTime.parse("2000-06-01T00:00:00Z");
createContacts(createTime);
assertThatCommand("host_create.xml", ImmutableMap.of("HOSTNAME", "ns1.example.external"))
.atTime(createTime.plusMinutes(2))
.hasResponse(
@@ -243,21 +242,9 @@ public class EppTestCase {
"CRDATE", createTime.plusMinutes(3).toString()));
}
protected void createContacts(DateTime createTime) throws Exception {
assertThatCommand("contact_create_sh8013.xml")
.atTime(createTime)
.hasResponse(
"contact_create_response_sh8013.xml", ImmutableMap.of("CRDATE", createTime.toString()));
assertThatCommand("contact_create_jd1234.xml")
.atTime(createTime.plusMinutes(1))
.hasResponse(
"contact_create_response_jd1234.xml",
ImmutableMap.of("CRDATE", createTime.plusMinutes(1).toString()));
}
/** Creates the domain fakesite.example with two nameservers on it. */
void createFakesite() throws Exception {
createContactsAndHosts();
createHosts();
assertThatCommand("domain_create_fakesite.xml")
.atTime("2000-06-01T00:04:00Z")
.hasResponse(

View File

@@ -14,85 +14,25 @@
package google.registry.flows.contact;
import static google.registry.model.eppoutput.CheckData.ContactCheck.create;
import static google.registry.testing.DatabaseHelper.persistActiveContact;
import static google.registry.testing.DatabaseHelper.persistDeletedContact;
import static google.registry.testing.EppExceptionSubject.assertAboutEppExceptions;
import static org.junit.jupiter.api.Assertions.assertThrows;
import google.registry.flows.EppException;
import google.registry.flows.FlowUtils.NotLoggedInException;
import google.registry.flows.ResourceCheckFlowTestCase;
import google.registry.flows.exceptions.TooManyResourceChecksException;
import google.registry.model.contact.Contact;
import google.registry.flows.FlowTestCase;
import google.registry.flows.exceptions.ContactsProhibitedException;
import org.junit.jupiter.api.Test;
/** Unit tests for {@link ContactCheckFlow}. */
class ContactCheckFlowTest extends ResourceCheckFlowTestCase<ContactCheckFlow, Contact> {
class ContactCheckFlowTest extends FlowTestCase<ContactCheckFlow> {
ContactCheckFlowTest() {
setEppInput("contact_check.xml");
}
@Test
void testNotLoggedIn() {
sessionMetadata.setRegistrarId(null);
EppException thrown = assertThrows(NotLoggedInException.class, this::runFlow);
assertAboutEppExceptions().that(thrown).marshalsToXml();
void testThrowsException() {
assertAboutEppExceptions()
.that(assertThrows(ContactsProhibitedException.class, this::runFlow))
.marshalsToXml();
}
@Test
void testNothingExists() throws Exception {
// These ids come from the check xml.
doCheckTest(
create(true, "sh8013", null),
create(true, "sah8013", null),
create(true, "8013sah", null));
}
@Test
void testOneExists() throws Exception {
persistActiveContact("sh8013");
// These ids come from the check xml.
doCheckTest(
create(false, "sh8013", "In use"),
create(true, "sah8013", null),
create(true, "8013sah", null));
}
@Test
void testOneExistsButWasDeleted() throws Exception {
persistDeletedContact("sh8013", clock.nowUtc().minusDays(1));
// These ids come from the check xml.
doCheckTest(
create(true, "sh8013", null),
create(true, "sah8013", null),
create(true, "8013sah", null));
}
@Test
void testXmlMatches() throws Exception {
persistActiveContact("sah8013");
runFlowAssertResponse(loadFile("contact_check_response.xml"));
}
@Test
void test50IdsAllowed() throws Exception {
// Make sure we don't have a regression that reduces the number of allowed checks.
setEppInput("contact_check_50.xml");
runFlow();
}
@Test
void testTooManyIds() {
setEppInput("contact_check_51.xml");
EppException thrown = assertThrows(TooManyResourceChecksException.class, this::runFlow);
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
void testIcannActivityReportField_getsLogged() throws Exception {
runFlow();
assertIcannReportingActivityFieldLogged("srs-cont-check");
}
}

View File

@@ -14,141 +14,24 @@
package google.registry.flows.contact;
import static com.google.common.truth.Truth.assertThat;
import static google.registry.model.common.FeatureFlag.FeatureName.MINIMUM_DATASET_CONTACTS_PROHIBITED;
import static google.registry.model.common.FeatureFlag.FeatureStatus.ACTIVE;
import static google.registry.model.common.FeatureFlag.FeatureStatus.INACTIVE;
import static google.registry.testing.ContactSubject.assertAboutContacts;
import static google.registry.testing.DatabaseHelper.assertNoBillingEvents;
import static google.registry.testing.DatabaseHelper.newContact;
import static google.registry.testing.DatabaseHelper.persistActiveContact;
import static google.registry.testing.DatabaseHelper.persistDeletedContact;
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.junit.jupiter.api.Assertions.assertThrows;
import com.google.common.collect.ImmutableSortedMap;
import google.registry.flows.EppException;
import google.registry.flows.FlowUtils.NotLoggedInException;
import google.registry.flows.ResourceFlowTestCase;
import google.registry.flows.contact.ContactFlowUtils.BadInternationalizedPostalInfoException;
import google.registry.flows.contact.ContactFlowUtils.DeclineContactDisclosureFieldDisallowedPolicyException;
import google.registry.flows.FlowTestCase;
import google.registry.flows.exceptions.ContactsProhibitedException;
import google.registry.flows.exceptions.ResourceAlreadyExistsForThisClientException;
import google.registry.flows.exceptions.ResourceCreateContentionException;
import google.registry.model.common.FeatureFlag;
import google.registry.model.contact.Contact;
import org.joda.time.DateTime;
import org.junit.jupiter.api.Test;
/** Unit tests for {@link ContactCreateFlow}. */
class ContactCreateFlowTest extends ResourceFlowTestCase<ContactCreateFlow, Contact> {
class ContactCreateFlowTest extends FlowTestCase<ContactCreateFlow> {
ContactCreateFlowTest() {
setEppInput("contact_create.xml");
clock.setTo(DateTime.parse("1999-04-03T22:00:00.0Z"));
}
private void doSuccessfulTest() throws Exception {
assertMutatingFlow(true);
runFlowAssertResponse(loadFile("contact_create_response.xml"));
// Check that the contact was created and persisted with a history entry.
Contact contact = reloadResourceByForeignKey();
assertAboutContacts().that(contact).hasOnlyOneHistoryEntryWhich().hasNoXml();
assertNoBillingEvents();
assertLastHistoryContainsResource(contact);
}
@Test
void testNotLoggedIn() {
sessionMetadata.setRegistrarId(null);
EppException thrown = assertThrows(NotLoggedInException.class, this::runFlow);
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
void testDryRun() throws Exception {
dryRunFlowAssertResponse(loadFile("contact_create_response.xml"));
}
@Test
void testSuccess_neverExisted() throws Exception {
doSuccessfulTest();
}
@Test
void testSuccess_existedButWasDeleted() throws Exception {
persistDeletedContact(getUniqueIdFromCommand(), clock.nowUtc().minusDays(1));
clock.advanceOneMilli();
doSuccessfulTest();
}
@Test
void testFailure_alreadyExists() throws Exception {
persistActiveContact(getUniqueIdFromCommand());
ResourceAlreadyExistsForThisClientException thrown =
assertThrows(ResourceAlreadyExistsForThisClientException.class, this::runFlow);
assertThat(thrown)
.hasMessageThat()
.contains(
String.format("Object with given ID (%s) already exists", getUniqueIdFromCommand()));
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
void testFailure_minimumDatasetPhase2_cannotCreateContacts() throws Exception {
persistResource(
new FeatureFlag.Builder()
.setFeatureName(MINIMUM_DATASET_CONTACTS_PROHIBITED)
.setStatusMap(
ImmutableSortedMap.of(START_OF_TIME, INACTIVE, clock.nowUtc().minusDays(5), ACTIVE))
.build());
EppException thrown = assertThrows(ContactsProhibitedException.class, this::runFlow);
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
void testFailure_resourceContention() throws Exception {
String targetId = getUniqueIdFromCommand();
persistResource(
newContact(targetId)
.asBuilder()
.setPersistedCurrentSponsorRegistrarId("NewRegistrar")
.build());
ResourceCreateContentionException thrown =
assertThrows(ResourceCreateContentionException.class, this::runFlow);
assertThat(thrown)
.hasMessageThat()
.contains(String.format("Object with given ID (%s) already exists", targetId));
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
void testSuccess_nonAsciiInLocAddress() throws Exception {
setEppInput("contact_create_hebrew_loc.xml");
doSuccessfulTest();
}
@Test
void testFailure_nonAsciiInIntAddress() {
setEppInput("contact_create_hebrew_int.xml");
EppException thrown =
assertThrows(BadInternationalizedPostalInfoException.class, this::runFlow);
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
void testFailure_declineDisclosure() {
setEppInput("contact_create_decline_disclosure.xml");
EppException thrown =
assertThrows(DeclineContactDisclosureFieldDisallowedPolicyException.class, this::runFlow);
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
void testIcannActivityReportField_getsLogged() throws Exception {
runFlow();
assertIcannReportingActivityFieldLogged("srs-cont-create");
void testThrowsException() {
assertAboutEppExceptions()
.that(assertThrows(ContactsProhibitedException.class, this::runFlow))
.marshalsToXml();
}
}

View File

@@ -14,269 +14,24 @@
package google.registry.flows.contact;
import static com.google.common.collect.MoreCollectors.onlyElement;
import static com.google.common.truth.Truth.assertThat;
import static google.registry.testing.ContactSubject.assertAboutContacts;
import static google.registry.testing.DatabaseHelper.assertNoBillingEvents;
import static google.registry.testing.DatabaseHelper.createTld;
import static google.registry.testing.DatabaseHelper.getPollMessages;
import static google.registry.testing.DatabaseHelper.newContact;
import static google.registry.testing.DatabaseHelper.persistActiveContact;
import static google.registry.testing.DatabaseHelper.persistContactWithPendingTransfer;
import static google.registry.testing.DatabaseHelper.persistDeletedContact;
import static google.registry.testing.DatabaseHelper.persistResource;
import static google.registry.testing.EppExceptionSubject.assertAboutEppExceptions;
import static org.junit.jupiter.api.Assertions.assertThrows;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
import google.registry.flows.EppException;
import google.registry.flows.FlowUtils.NotLoggedInException;
import google.registry.flows.ResourceFlowTestCase;
import google.registry.flows.ResourceFlowUtils.ResourceDoesNotExistException;
import google.registry.flows.ResourceFlowUtils.ResourceNotOwnedException;
import google.registry.flows.exceptions.ResourceStatusProhibitsOperationException;
import google.registry.flows.exceptions.ResourceToDeleteIsReferencedException;
import google.registry.model.contact.Contact;
import google.registry.model.eppcommon.StatusValue;
import google.registry.model.eppcommon.Trid;
import google.registry.model.poll.PendingActionNotificationResponse;
import google.registry.model.poll.PollMessage;
import google.registry.model.reporting.HistoryEntry;
import google.registry.model.reporting.HistoryEntry.Type;
import google.registry.model.tld.Tld;
import google.registry.model.transfer.TransferData;
import google.registry.model.transfer.TransferResponse;
import google.registry.model.transfer.TransferStatus;
import google.registry.testing.DatabaseHelper;
import org.joda.time.DateTime;
import org.junit.jupiter.api.BeforeEach;
import google.registry.flows.FlowTestCase;
import google.registry.flows.exceptions.ContactsProhibitedException;
import org.junit.jupiter.api.Test;
/** Unit tests for {@link ContactDeleteFlow}. */
class ContactDeleteFlowTest extends ResourceFlowTestCase<ContactDeleteFlow, Contact> {
class ContactDeleteFlowTest extends FlowTestCase<ContactDeleteFlow> {
@BeforeEach
void initFlowTest() {
ContactDeleteFlowTest() {
setEppInput("contact_delete.xml");
}
@Test
void testNotLoggedIn() {
sessionMetadata.setRegistrarId(null);
EppException thrown = assertThrows(NotLoggedInException.class, this::runFlow);
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
void testDryRun() throws Exception {
persistActiveContact(getUniqueIdFromCommand());
dryRunFlowAssertResponse(loadFile("contact_delete_response.xml"));
}
@Test
void testSuccess() throws Exception {
persistActiveContact(getUniqueIdFromCommand());
clock.advanceOneMilli();
assertMutatingFlow(true);
runFlowAssertResponse(loadFile("contact_delete_response.xml"));
assertSqlDeleteSuccess();
}
@Test
void testSuccess_pendingTransfer_sql() throws Exception {
DateTime transferRequestTime = clock.nowUtc().minusDays(3);
TransferData oldTransferData =
persistContactWithPendingTransfer(
persistActiveContact(getUniqueIdFromCommand()),
transferRequestTime,
transferRequestTime.plus(Tld.DEFAULT_TRANSFER_GRACE_PERIOD),
clock.nowUtc())
.getTransferData();
clock.advanceOneMilli();
assertMutatingFlow(true);
runFlowAssertResponse(loadFile("contact_delete_response.xml"));
assertSqlDeleteSuccess(Type.CONTACT_DELETE, Type.CONTACT_TRANSFER_REQUEST);
Contact softDeletedContact = reloadResourceByForeignKey(clock.nowUtc().minusMillis(1));
assertThat(softDeletedContact.getTransferData())
.isEqualTo(
oldTransferData
.copyConstantFieldsToBuilder()
.setTransferStatus(TransferStatus.SERVER_CANCELLED)
.setPendingTransferExpirationTime(softDeletedContact.getDeletionTime())
.build());
PollMessage gainingPollMessage =
Iterables.getOnlyElement(getPollMessages("NewRegistrar", clock.nowUtc()));
assertThat(gainingPollMessage.getEventTime()).isEqualTo(clock.nowUtc());
assertThat(
gainingPollMessage.getResponseData().stream()
.filter(TransferResponse.class::isInstance)
.map(TransferResponse.class::cast)
.collect(onlyElement())
.getTransferStatus())
.isEqualTo(TransferStatus.SERVER_CANCELLED);
PendingActionNotificationResponse panData =
gainingPollMessage.getResponseData().stream()
.filter(PendingActionNotificationResponse.class::isInstance)
.map(PendingActionNotificationResponse.class::cast)
.collect(onlyElement());
assertThat(panData.getTrid())
.isEqualTo(Trid.create("transferClient-trid", "transferServer-trid"));
assertThat(panData.getActionResult()).isFalse();
}
@Test
void testSuccess_clTridNotSpecified() throws Exception {
setEppInput("contact_delete_no_cltrid.xml");
persistActiveContact(getUniqueIdFromCommand());
clock.advanceOneMilli();
assertMutatingFlow(true);
runFlowAssertResponse(loadFile("contact_delete_response_no_cltrid.xml"));
assertSqlDeleteSuccess();
}
@Test
void testFailure_neverExisted() throws Exception {
ResourceDoesNotExistException thrown =
assertThrows(ResourceDoesNotExistException.class, this::runFlow);
assertThat(thrown).hasMessageThat().contains(String.format("(%s)", getUniqueIdFromCommand()));
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
void testFailure_existedButWasDeleted() throws Exception {
persistDeletedContact(getUniqueIdFromCommand(), clock.nowUtc().minusDays(1));
ResourceDoesNotExistException thrown =
assertThrows(ResourceDoesNotExistException.class, this::runFlow);
assertThat(thrown).hasMessageThat().contains(String.format("(%s)", getUniqueIdFromCommand()));
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
void testFailure_existedButWasClientDeleteProhibited() throws Exception {
doFailingStatusTest(
StatusValue.CLIENT_DELETE_PROHIBITED, ResourceStatusProhibitsOperationException.class);
}
@Test
void testFailure_existedButWasServerDeleteProhibited() throws Exception {
doFailingStatusTest(
StatusValue.SERVER_DELETE_PROHIBITED, ResourceStatusProhibitsOperationException.class);
}
@Test
void testFailure_existedButWasPendingDelete() throws Exception {
doFailingStatusTest(
StatusValue.PENDING_DELETE, ResourceStatusProhibitsOperationException.class);
}
private void doFailingStatusTest(StatusValue statusValue, Class<? extends EppException> exception)
throws Exception {
persistResource(
newContact(getUniqueIdFromCommand())
.asBuilder()
.setStatusValues(ImmutableSet.of(statusValue))
.build());
EppException thrown = assertThrows(exception, this::runFlow);
assertThat(thrown).hasMessageThat().contains(statusValue.getXmlName());
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
void testSuccess_clientDeleteProhibited_superuser() throws Exception {
persistResource(
persistActiveContact(getUniqueIdFromCommand())
.asBuilder()
.addStatusValue(StatusValue.CLIENT_DELETE_PROHIBITED)
.build());
runFlowAssertResponse(
CommitMode.LIVE, UserPrivileges.SUPERUSER, loadFile("contact_delete_response.xml"));
}
@Test
void testSuccess_serverDeleteProhibited_superuser() throws Exception {
persistResource(
persistActiveContact(getUniqueIdFromCommand())
.asBuilder()
.addStatusValue(StatusValue.SERVER_DELETE_PROHIBITED)
.build());
runFlowAssertResponse(
CommitMode.LIVE, UserPrivileges.SUPERUSER, loadFile("contact_delete_response.xml"));
}
@Test
void testFailure_pendingDelete_superuser() throws Exception {
persistResource(
persistActiveContact(getUniqueIdFromCommand())
.asBuilder()
.addStatusValue(StatusValue.PENDING_DELETE)
.build());
void testThrowsException() {
assertAboutEppExceptions()
.that(
assertThrows(
ResourceStatusProhibitsOperationException.class,
() -> runFlow(CommitMode.LIVE, UserPrivileges.SUPERUSER)))
.that(assertThrows(ContactsProhibitedException.class, this::runFlow))
.marshalsToXml();
}
@Test
void testFailure_unauthorizedClient() throws Exception {
sessionMetadata.setRegistrarId("NewRegistrar");
persistActiveContact(getUniqueIdFromCommand());
EppException thrown = assertThrows(ResourceNotOwnedException.class, this::runFlow);
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
void testSuccess_superuserUnauthorizedClient() throws Exception {
sessionMetadata.setRegistrarId("NewRegistrar");
persistActiveContact(getUniqueIdFromCommand());
clock.advanceOneMilli();
runFlowAssertResponse(
CommitMode.LIVE, UserPrivileges.SUPERUSER, loadFile("contact_delete_response.xml"));
assertSqlDeleteSuccess();
}
@Test
void testFailure_failfastWhenLinkedToDomain() throws Exception {
createTld("tld");
persistResource(
DatabaseHelper.newDomain("example.tld", persistActiveContact(getUniqueIdFromCommand())));
EppException thrown = assertThrows(ResourceToDeleteIsReferencedException.class, this::runFlow);
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
void testIcannActivityReportField_getsLogged() throws Exception {
persistActiveContact(getUniqueIdFromCommand());
clock.advanceOneMilli();
runFlow();
assertIcannReportingActivityFieldLogged("srs-cont-delete");
}
private void assertSqlDeleteSuccess(HistoryEntry.Type... historyEntryTypes) throws Exception {
assertThat(reloadResourceByForeignKey()).isNull();
assertAboutContacts()
.that(reloadResourceByForeignKey(clock.nowUtc().minusMillis(1)))
.isNotActiveAt(clock.nowUtc())
.and()
.hasNullLocalizedPostalInfo()
.and()
.hasNullInternationalizedPostalInfo()
.and()
.hasNullEmailAddress()
.and()
.hasNullVoiceNumber()
.and()
.hasNullFaxNumber()
.and()
.hasExactlyStatusValues(StatusValue.OK)
.and()
.hasOneHistoryEntryEachOfTypes(historyEntryTypes);
assertNoBillingEvents();
}
private void assertSqlDeleteSuccess() throws Exception {
assertSqlDeleteSuccess(Type.CONTACT_DELETE);
}
}

View File

@@ -14,202 +14,24 @@
package google.registry.flows.contact;
import static com.google.common.truth.Truth.assertThat;
import static google.registry.model.EppResourceUtils.isDeleted;
import static google.registry.testing.DatabaseHelper.assertNoBillingEvents;
import static google.registry.testing.DatabaseHelper.createTld;
import static google.registry.testing.DatabaseHelper.persistResource;
import static google.registry.testing.EppExceptionSubject.assertAboutEppExceptions;
import static org.junit.jupiter.api.Assertions.assertThrows;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import google.registry.flows.EppException;
import google.registry.flows.FlowUtils.NotLoggedInException;
import google.registry.flows.ResourceFlowTestCase;
import google.registry.flows.ResourceFlowUtils.ResourceDoesNotExistException;
import google.registry.flows.ResourceFlowUtils.ResourceNotOwnedException;
import google.registry.model.contact.Contact;
import google.registry.model.contact.ContactAddress;
import google.registry.model.contact.ContactAuthInfo;
import google.registry.model.contact.ContactPhoneNumber;
import google.registry.model.contact.Disclose;
import google.registry.model.contact.PostalInfo;
import google.registry.model.contact.PostalInfo.Type;
import google.registry.model.eppcommon.AuthInfo.PasswordAuth;
import google.registry.model.eppcommon.PresenceMarker;
import google.registry.model.eppcommon.StatusValue;
import google.registry.testing.DatabaseHelper;
import org.joda.time.DateTime;
import google.registry.flows.FlowTestCase;
import google.registry.flows.exceptions.ContactsProhibitedException;
import org.junit.jupiter.api.Test;
/** Unit tests for {@link ContactInfoFlow}. */
class ContactInfoFlowTest extends ResourceFlowTestCase<ContactInfoFlow, Contact> {
class ContactInfoFlowTest extends FlowTestCase<ContactInfoFlow> {
ContactInfoFlowTest() {
setEppInput("contact_info.xml");
}
private Contact persistContact(boolean active) {
Contact contact =
persistResource(
new Contact.Builder()
.setContactId("sh8013")
.setRepoId("2FF-ROID")
.setDeletionTime(active ? null : clock.nowUtc().minusDays(1))
.setStatusValues(ImmutableSet.of(StatusValue.CLIENT_DELETE_PROHIBITED))
.setInternationalizedPostalInfo(
new PostalInfo.Builder()
.setType(Type.INTERNATIONALIZED)
.setName("John Doe")
.setOrg("Example Inc.")
.setAddress(
new ContactAddress.Builder()
.setStreet(ImmutableList.of("123 Example Dr.", "Suite 100"))
.setCity("Dulles")
.setState("VA")
.setZip("20166-6503")
.setCountryCode("US")
.build())
.build())
.setVoiceNumber(
new ContactPhoneNumber.Builder()
.setPhoneNumber("+1.7035555555")
.setExtension("1234")
.build())
.setFaxNumber(
new ContactPhoneNumber.Builder().setPhoneNumber("+1.7035555556").build())
.setEmailAddress("jdoe@example.com")
.setPersistedCurrentSponsorRegistrarId("TheRegistrar")
.setCreationRegistrarId("NewRegistrar")
.setLastEppUpdateRegistrarId("NewRegistrar")
.setCreationTimeForTest(DateTime.parse("1999-04-03T22:00:00.0Z"))
.setLastEppUpdateTime(DateTime.parse("1999-12-03T09:00:00.0Z"))
.setLastTransferTime(DateTime.parse("2000-04-08T09:00:00.0Z"))
.setAuthInfo(ContactAuthInfo.create(PasswordAuth.create("2fooBAR")))
.setDisclose(
new Disclose.Builder()
.setFlag(true)
.setVoice(new PresenceMarker())
.setEmail(new PresenceMarker())
.build())
.build());
assertThat(isDeleted(contact, clock.nowUtc())).isNotEqualTo(active);
return contact;
}
@Test
void testNotLoggedIn() {
sessionMetadata.setRegistrarId(null);
EppException thrown = assertThrows(NotLoggedInException.class, this::runFlow);
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
void testSuccess() throws Exception {
persistContact(true);
// Check that the persisted contact info was returned.
assertMutatingFlow(false);
runFlowAssertResponse(
loadFile("contact_info_response.xml"),
// We use a different roid scheme than the samples so ignore it.
"epp.response.resData.infData.roid");
assertNoHistory();
assertNoBillingEvents();
}
@Test
void testSuccess_linked() throws Exception {
createTld("foobar");
persistResource(DatabaseHelper.newDomain("example.foobar", persistContact(true)));
// Check that the persisted contact info was returned.
assertMutatingFlow(false);
runFlowAssertResponse(
loadFile("contact_info_response_linked.xml"),
// We use a different roid scheme than the samples so ignore it.
"epp.response.resData.infData.roid");
assertNoHistory();
assertNoBillingEvents();
}
@Test
void testSuccess_owningRegistrarWithoutAuthInfo_seesAuthInfo() throws Exception {
setEppInput("contact_info_no_authinfo.xml");
persistContact(true);
// Check that the persisted contact info was returned.
assertMutatingFlow(false);
runFlowAssertResponse(
loadFile("contact_info_response.xml"),
// We use a different roid scheme than the samples so ignore it.
"epp.response.resData.infData.roid");
assertNoHistory();
assertNoBillingEvents();
}
@Test
void testFailure_otherRegistrar_notAuthorized() throws Exception {
setRegistrarIdForFlow("NewRegistrar");
persistContact(true);
// Check that the persisted contact info was returned.
assertMutatingFlow(false);
ResourceNotOwnedException thrown = assertThrows(ResourceNotOwnedException.class, this::runFlow);
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
void testSuccess_otherRegistrarWithoutAuthInfoAsSuperuser_doesNotSeeAuthInfo() throws Exception {
setRegistrarIdForFlow("NewRegistrar");
setEppInput("contact_info_no_authinfo.xml");
persistContact(true);
// Check that the persisted contact info was returned.
assertMutatingFlow(false);
runFlowAssertResponse(
CommitMode.LIVE,
UserPrivileges.SUPERUSER,
loadFile("contact_info_response_no_authinfo.xml"),
// We use a different roid scheme than the samples so ignore it.
"epp.response.resData.infData.roid");
assertNoHistory();
assertNoBillingEvents();
}
@Test
void testSuccess_otherRegistrarWithAuthInfoAsSuperuser_seesAuthInfo() throws Exception {
setRegistrarIdForFlow("NewRegistrar");
persistContact(true);
// Check that the persisted contact info was returned.
assertMutatingFlow(false);
runFlowAssertResponse(
CommitMode.LIVE,
UserPrivileges.SUPERUSER,
loadFile("contact_info_response.xml"),
// We use a different roid scheme than the samples so ignore it.
"epp.response.resData.infData.roid");
assertNoHistory();
assertNoBillingEvents();
}
@Test
void testFailure_neverExisted() throws Exception {
ResourceDoesNotExistException thrown =
assertThrows(ResourceDoesNotExistException.class, this::runFlow);
assertThat(thrown).hasMessageThat().contains(String.format("(%s)", getUniqueIdFromCommand()));
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
void testFailure_existedButWasDeleted() throws Exception {
persistContact(false);
ResourceDoesNotExistException thrown =
assertThrows(ResourceDoesNotExistException.class, this::runFlow);
assertThat(thrown).hasMessageThat().contains(String.format("(%s)", getUniqueIdFromCommand()));
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
void testIcannActivityReportField_getsLogged() throws Exception {
persistContact(true);
runFlow();
assertIcannReportingActivityFieldLogged("srs-cont-info");
void testThrowsException() {
assertAboutEppExceptions()
.that(assertThrows(ContactsProhibitedException.class, this::runFlow))
.marshalsToXml();
}
}

View File

@@ -14,254 +14,24 @@
package google.registry.flows.contact;
import static com.google.common.collect.MoreCollectors.onlyElement;
import static com.google.common.truth.Truth.assertThat;
import static google.registry.testing.ContactSubject.assertAboutContacts;
import static google.registry.testing.DatabaseHelper.assertNoBillingEvents;
import static google.registry.testing.DatabaseHelper.createTld;
import static google.registry.testing.DatabaseHelper.getOnlyPollMessage;
import static google.registry.testing.DatabaseHelper.getPollMessages;
import static google.registry.testing.DatabaseHelper.persistResource;
import static google.registry.testing.EppExceptionSubject.assertAboutEppExceptions;
import static org.junit.jupiter.api.Assertions.assertThrows;
import google.registry.flows.EppException;
import google.registry.flows.FlowUtils.NotLoggedInException;
import google.registry.flows.ResourceFlowUtils.BadAuthInfoForResourceException;
import google.registry.flows.ResourceFlowUtils.ResourceDoesNotExistException;
import google.registry.flows.ResourceFlowUtils.ResourceNotOwnedException;
import google.registry.flows.exceptions.NotPendingTransferException;
import google.registry.model.contact.Contact;
import google.registry.model.contact.ContactAuthInfo;
import google.registry.model.eppcommon.AuthInfo.PasswordAuth;
import google.registry.model.eppcommon.Trid;
import google.registry.model.poll.PendingActionNotificationResponse;
import google.registry.model.poll.PollMessage;
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 org.junit.jupiter.api.BeforeEach;
import google.registry.flows.FlowTestCase;
import google.registry.flows.exceptions.ContactsProhibitedException;
import org.junit.jupiter.api.Test;
/** Unit tests for {@link ContactTransferApproveFlow}. */
class ContactTransferApproveFlowTest
extends ContactTransferFlowTestCase<ContactTransferApproveFlow, Contact> {
class ContactTransferApproveFlowTest extends FlowTestCase<ContactTransferApproveFlow> {
@BeforeEach
void setUp() {
ContactTransferApproveFlowTest() {
setEppInput("contact_transfer_approve.xml");
setRegistrarIdForFlow("TheRegistrar");
setupContactWithPendingTransfer();
clock.advanceOneMilli();
createTld("foobar");
}
private void doSuccessfulTest(String commandFilename, String expectedXmlFilename)
throws Exception {
setEppInput(commandFilename);
// Look in the future and make sure the poll messages for implicit ack are there.
assertThat(getPollMessages("NewRegistrar", clock.nowUtc().plusMonths(1)))
.hasSize(1);
assertThat(getPollMessages("TheRegistrar", clock.nowUtc().plusMonths(1)))
.hasSize(1);
// Setup done; run the test.
contact = reloadResourceByForeignKey();
TransferData originalTransferData = contact.getTransferData();
assertMutatingFlow(true);
runFlowAssertResponse(loadFile(expectedXmlFilename));
// Transfer should have succeeded. Verify correct fields were set.
contact = reloadResourceByForeignKey();
assertAboutContacts()
.that(contact)
.hasCurrentSponsorRegistrarId("NewRegistrar")
.and()
.hasLastTransferTime(clock.nowUtc())
.and()
.hasOneHistoryEntryEachOfTypes(
HistoryEntry.Type.CONTACT_TRANSFER_REQUEST, HistoryEntry.Type.CONTACT_TRANSFER_APPROVE);
assertThat(contact.getTransferData())
.isEqualTo(
originalTransferData.copyConstantFieldsToBuilder()
.setTransferStatus(TransferStatus.CLIENT_APPROVED)
.setPendingTransferExpirationTime(clock.nowUtc())
.build());
assertNoBillingEvents();
// The poll message (in the future) to the losing registrar for implicit ack should be gone.
assertThat(getPollMessages("TheRegistrar", clock.nowUtc().plusMonths(1))).isEmpty();
// The poll message in the future to the gaining registrar should be gone too, but there
// should be one at the current time to the gaining registrar.
PollMessage gainingPollMessage = getOnlyPollMessage("NewRegistrar");
assertThat(gainingPollMessage.getEventTime()).isEqualTo(clock.nowUtc());
assertThat(
gainingPollMessage
.getResponseData()
.stream()
.filter(TransferResponse.class::isInstance)
.map(TransferResponse.class::cast)
.collect(onlyElement())
.getTransferStatus())
.isEqualTo(TransferStatus.CLIENT_APPROVED);
PendingActionNotificationResponse panData =
gainingPollMessage
.getResponseData()
.stream()
.filter(PendingActionNotificationResponse.class::isInstance)
.map(PendingActionNotificationResponse.class::cast)
.collect(onlyElement());
assertThat(panData.getTrid())
.isEqualTo(Trid.create("transferClient-trid", "transferServer-trid"));
assertThat(panData.getActionResult()).isTrue();
assertLastHistoryContainsResource(contact);
}
private void doFailingTest(String commandFilename) throws Exception {
setEppInput(commandFilename);
// Setup done; run the test.
assertMutatingFlow(true);
runFlow();
}
@Test
void testNotLoggedIn() {
sessionMetadata.setRegistrarId(null);
EppException thrown = assertThrows(NotLoggedInException.class, this::runFlow);
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
void testDryRun() throws Exception {
setEppInput("contact_transfer_approve.xml");
dryRunFlowAssertResponse(loadFile("contact_transfer_approve_response.xml"));
}
@Test
void testSuccess() throws Exception {
doSuccessfulTest("contact_transfer_approve.xml", "contact_transfer_approve_response.xml");
}
@Test
void testSuccess_withAuthinfo() throws Exception {
doSuccessfulTest("contact_transfer_approve_with_authinfo.xml",
"contact_transfer_approve_response.xml");
}
@Test
void testFailure_badContactPassword() {
// Change the contact's password so it does not match the password in the file.
contact = persistResource(
contact.asBuilder()
.setAuthInfo(ContactAuthInfo.create(PasswordAuth.create("badpassword")))
.build());
EppException thrown =
assertThrows(
BadAuthInfoForResourceException.class,
() -> doFailingTest("contact_transfer_approve_with_authinfo.xml"));
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
void testFailure_neverBeenTransferred() {
changeTransferStatus(null);
EppException thrown =
assertThrows(
NotPendingTransferException.class, () -> doFailingTest("contact_transfer_approve.xml"));
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
void testFailure_clientApproved() {
changeTransferStatus(TransferStatus.CLIENT_APPROVED);
EppException thrown =
assertThrows(
NotPendingTransferException.class, () -> doFailingTest("contact_transfer_approve.xml"));
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
void testFailure_clientRejected() {
changeTransferStatus(TransferStatus.CLIENT_REJECTED);
EppException thrown =
assertThrows(
NotPendingTransferException.class, () -> doFailingTest("contact_transfer_approve.xml"));
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
void testFailure_clientCancelled() {
changeTransferStatus(TransferStatus.CLIENT_CANCELLED);
EppException thrown =
assertThrows(
NotPendingTransferException.class, () -> doFailingTest("contact_transfer_approve.xml"));
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
void testFailure_serverApproved() {
changeTransferStatus(TransferStatus.SERVER_APPROVED);
EppException thrown =
assertThrows(
NotPendingTransferException.class, () -> doFailingTest("contact_transfer_approve.xml"));
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
void testFailure_serverCancelled() {
changeTransferStatus(TransferStatus.SERVER_CANCELLED);
EppException thrown =
assertThrows(
NotPendingTransferException.class, () -> doFailingTest("contact_transfer_approve.xml"));
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
void testFailure_gainingClient() {
setRegistrarIdForFlow("NewRegistrar");
EppException thrown =
assertThrows(
ResourceNotOwnedException.class, () -> doFailingTest("contact_transfer_approve.xml"));
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
void testFailure_unrelatedClient() {
setRegistrarIdForFlow("ClientZ");
EppException thrown =
assertThrows(
ResourceNotOwnedException.class, () -> doFailingTest("contact_transfer_approve.xml"));
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
void testFailure_deletedContact() throws Exception {
contact = persistResource(
contact.asBuilder().setDeletionTime(clock.nowUtc().minusDays(1)).build());
ResourceDoesNotExistException thrown =
assertThrows(
ResourceDoesNotExistException.class,
() -> doFailingTest("contact_transfer_approve.xml"));
assertThat(thrown).hasMessageThat().contains(String.format("(%s)", getUniqueIdFromCommand()));
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
void testFailure_nonexistentContact() throws Exception {
persistResource(contact.asBuilder().setDeletionTime(clock.nowUtc().minusDays(1)).build());
contact = persistResource(
contact.asBuilder().setDeletionTime(clock.nowUtc().minusDays(1)).build());
ResourceDoesNotExistException thrown =
assertThrows(
ResourceDoesNotExistException.class,
() -> doFailingTest("contact_transfer_approve.xml"));
assertThat(thrown).hasMessageThat().contains(String.format("(%s)", getUniqueIdFromCommand()));
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
void testIcannActivityReportField_getsLogged() throws Exception {
runFlow();
assertIcannReportingActivityFieldLogged("srs-cont-transfer-approve");
void testThrowsException() {
assertAboutEppExceptions()
.that(assertThrows(ContactsProhibitedException.class, this::runFlow))
.marshalsToXml();
}
}

View File

@@ -14,240 +14,24 @@
package google.registry.flows.contact;
import static com.google.common.collect.MoreCollectors.onlyElement;
import static com.google.common.truth.Truth.assertThat;
import static google.registry.testing.ContactSubject.assertAboutContacts;
import static google.registry.testing.DatabaseHelper.assertNoBillingEvents;
import static google.registry.testing.DatabaseHelper.getOnlyPollMessage;
import static google.registry.testing.DatabaseHelper.getPollMessages;
import static google.registry.testing.DatabaseHelper.persistResource;
import static google.registry.testing.EppExceptionSubject.assertAboutEppExceptions;
import static org.junit.jupiter.api.Assertions.assertThrows;
import google.registry.flows.EppException;
import google.registry.flows.FlowUtils.NotLoggedInException;
import google.registry.flows.ResourceFlowUtils.BadAuthInfoForResourceException;
import google.registry.flows.ResourceFlowUtils.ResourceDoesNotExistException;
import google.registry.flows.exceptions.NotPendingTransferException;
import google.registry.flows.exceptions.NotTransferInitiatorException;
import google.registry.model.contact.Contact;
import google.registry.model.contact.ContactAuthInfo;
import google.registry.model.eppcommon.AuthInfo.PasswordAuth;
import google.registry.model.poll.PollMessage;
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 org.junit.jupiter.api.BeforeEach;
import google.registry.flows.FlowTestCase;
import google.registry.flows.exceptions.ContactsProhibitedException;
import org.junit.jupiter.api.Test;
/** Unit tests for {@link ContactTransferCancelFlow}. */
class ContactTransferCancelFlowTest
extends ContactTransferFlowTestCase<ContactTransferCancelFlow, Contact> {
class ContactTransferCancelFlowTest extends FlowTestCase<ContactTransferCancelFlow> {
@BeforeEach
void setUp() {
this.setEppInput("contact_transfer_cancel.xml");
setRegistrarIdForFlow("NewRegistrar");
setupContactWithPendingTransfer();
clock.advanceOneMilli();
}
private void doSuccessfulTest(String commandFilename, String expectedXmlFilename)
throws Exception {
this.setEppInput(commandFilename);
// Look in the future and make sure the poll messages for implicit ack are there.
assertThat(getPollMessages("NewRegistrar", clock.nowUtc().plusMonths(1))).hasSize(1);
assertThat(getPollMessages("TheRegistrar", clock.nowUtc().plusMonths(1))).hasSize(1);
// Setup done; run the test.
contact = reloadResourceByForeignKey();
TransferData originalTransferData = contact.getTransferData();
assertMutatingFlow(true);
runFlowAssertResponse(loadFile(expectedXmlFilename));
// Transfer should have been cancelled. Verify correct fields were set.
contact = reloadResourceByForeignKey();
assertAboutContacts()
.that(contact)
.hasCurrentSponsorRegistrarId("TheRegistrar")
.and()
.hasLastTransferTimeNotEqualTo(clock.nowUtc())
.and()
.hasOneHistoryEntryEachOfTypes(
HistoryEntry.Type.CONTACT_TRANSFER_REQUEST, HistoryEntry.Type.CONTACT_TRANSFER_CANCEL);
assertThat(contact.getTransferData())
.isEqualTo(
originalTransferData.copyConstantFieldsToBuilder()
.setTransferStatus(TransferStatus.CLIENT_CANCELLED)
.setPendingTransferExpirationTime(clock.nowUtc())
.build());
assertNoBillingEvents();
// The poll message (in the future) to the gaining registrar for implicit ack should be gone.
assertThat(getPollMessages("NewRegistrar", clock.nowUtc().plusMonths(1))).isEmpty();
// The poll message in the future to the losing registrar should be gone too, but there
// should be one at the current time to the losing registrar.
PollMessage losingPollMessage = getOnlyPollMessage("TheRegistrar");
assertThat(losingPollMessage.getEventTime()).isEqualTo(clock.nowUtc());
assertThat(
losingPollMessage
.getResponseData()
.stream()
.filter(TransferResponse.class::isInstance)
.map(TransferResponse.class::cast)
.collect(onlyElement())
.getTransferStatus())
.isEqualTo(TransferStatus.CLIENT_CANCELLED);
assertLastHistoryContainsResource(contact);
}
private void doFailingTest(String commandFilename) throws Exception {
this.setEppInput(commandFilename);
// Setup done; run the test.
assertMutatingFlow(true);
runFlow();
}
@Test
void testNotLoggedIn() {
sessionMetadata.setRegistrarId(null);
EppException thrown = assertThrows(NotLoggedInException.class, this::runFlow);
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
void testDryRun() throws Exception {
ContactTransferCancelFlowTest() {
setEppInput("contact_transfer_cancel.xml");
dryRunFlowAssertResponse(loadFile("contact_transfer_cancel_response.xml"));
}
@Test
void testSuccess() throws Exception {
doSuccessfulTest("contact_transfer_cancel.xml", "contact_transfer_cancel_response.xml");
}
@Test
void testSuccess_withAuthinfo() throws Exception {
doSuccessfulTest("contact_transfer_cancel_with_authinfo.xml",
"contact_transfer_cancel_response.xml");
}
@Test
void testFailure_badContactPassword() {
// Change the contact's password so it does not match the password in the file.
contact =
persistResource(
contact
.asBuilder()
.setAuthInfo(ContactAuthInfo.create(PasswordAuth.create("badpassword")))
.build());
EppException thrown =
assertThrows(
BadAuthInfoForResourceException.class,
() -> doFailingTest("contact_transfer_cancel_with_authinfo.xml"));
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
void testFailure_neverBeenTransferred() {
changeTransferStatus(null);
EppException thrown =
assertThrows(
NotPendingTransferException.class, () -> doFailingTest("contact_transfer_cancel.xml"));
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
void testFailure_clientApproved() {
changeTransferStatus(TransferStatus.CLIENT_APPROVED);
EppException thrown =
assertThrows(
NotPendingTransferException.class, () -> doFailingTest("contact_transfer_cancel.xml"));
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
void testFailure_clientRejected() {
changeTransferStatus(TransferStatus.CLIENT_REJECTED);
EppException thrown =
assertThrows(
NotPendingTransferException.class, () -> doFailingTest("contact_transfer_cancel.xml"));
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
void testFailure_clientCancelled() {
changeTransferStatus(TransferStatus.CLIENT_CANCELLED);
EppException thrown =
assertThrows(
NotPendingTransferException.class, () -> doFailingTest("contact_transfer_cancel.xml"));
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
void testFailure_serverApproved() {
changeTransferStatus(TransferStatus.SERVER_APPROVED);
EppException thrown =
assertThrows(
NotPendingTransferException.class, () -> doFailingTest("contact_transfer_cancel.xml"));
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
void testFailure_serverCancelled() {
changeTransferStatus(TransferStatus.SERVER_CANCELLED);
EppException thrown =
assertThrows(
NotPendingTransferException.class, () -> doFailingTest("contact_transfer_cancel.xml"));
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
void testFailure_sponsoringClient() {
setRegistrarIdForFlow("TheRegistrar");
EppException thrown =
assertThrows(
NotTransferInitiatorException.class,
() -> doFailingTest("contact_transfer_cancel.xml"));
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
void testFailure_unrelatedClient() {
setRegistrarIdForFlow("ClientZ");
EppException thrown =
assertThrows(
NotTransferInitiatorException.class,
() -> doFailingTest("contact_transfer_cancel.xml"));
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
void testFailure_deletedContact() throws Exception {
contact =
persistResource(contact.asBuilder().setDeletionTime(clock.nowUtc().minusDays(1)).build());
ResourceDoesNotExistException thrown =
assertThrows(
ResourceDoesNotExistException.class,
() -> doFailingTest("contact_transfer_cancel.xml"));
assertThat(thrown).hasMessageThat().contains(String.format("(%s)", getUniqueIdFromCommand()));
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
void testFailure_nonexistentContact() throws Exception {
persistResource(contact.asBuilder().setDeletionTime(clock.nowUtc().minusDays(1)).build());
ResourceDoesNotExistException thrown =
assertThrows(
ResourceDoesNotExistException.class,
() -> doFailingTest("contact_transfer_cancel.xml"));
assertThat(thrown).hasMessageThat().contains(String.format("(%s)", getUniqueIdFromCommand()));
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
void testIcannActivityReportField_getsLogged() throws Exception {
runFlow();
assertIcannReportingActivityFieldLogged("srs-cont-transfer-cancel");
void testThrowsException() {
assertAboutEppExceptions()
.that(assertThrows(ContactsProhibitedException.class, this::runFlow))
.marshalsToXml();
}
}

View File

@@ -1,93 +0,0 @@
// Copyright 2017 The Nomulus Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package google.registry.flows.contact;
import static com.google.common.base.Preconditions.checkState;
import static google.registry.testing.DatabaseHelper.newContact;
import static google.registry.testing.DatabaseHelper.persistContactWithPendingTransfer;
import static google.registry.testing.DatabaseHelper.persistResource;
import google.registry.flows.Flow;
import google.registry.flows.ResourceFlowTestCase;
import google.registry.model.EppResource;
import google.registry.model.contact.Contact;
import google.registry.model.tld.Tld;
import google.registry.model.transfer.TransferStatus;
import google.registry.persistence.transaction.JpaTransactionManagerExtension;
import org.joda.time.DateTime;
import org.joda.time.Duration;
import org.junit.jupiter.api.BeforeEach;
/**
* Base class for contact transfer flow unit tests.
*
* @param <F> the flow type
* @param <R> the resource type
*/
abstract class ContactTransferFlowTestCase<F extends Flow, R extends EppResource>
extends ResourceFlowTestCase<F, R> {
// Transfer is requested on the 6th and expires on the 11th.
// The "now" of this flow is on the 9th, 3 days in.
private static final DateTime TRANSFER_REQUEST_TIME = DateTime.parse("2000-06-06T22:00:00.0Z");
private static final DateTime TRANSFER_EXPIRATION_TIME =
TRANSFER_REQUEST_TIME.plus(Tld.DEFAULT_TRANSFER_GRACE_PERIOD);
private static final Duration TIME_SINCE_REQUEST = Duration.standardDays(3);
protected Contact contact;
ContactTransferFlowTestCase() {
checkState(!Tld.DEFAULT_TRANSFER_GRACE_PERIOD.isShorterThan(TIME_SINCE_REQUEST));
clock.setTo(TRANSFER_REQUEST_TIME.plus(TIME_SINCE_REQUEST));
}
@BeforeEach
void beforeEachContactTransferFlowTestCase() {
// Registrar ClientZ is used in tests that need another registrar that definitely doesn't own
// the resources in question.
persistResource(
JpaTransactionManagerExtension.makeRegistrar1()
.asBuilder()
.setRegistrarId("ClientZ")
.build());
}
/** Adds a contact that has a pending transfer on it from TheRegistrar to NewRegistrar. */
void setupContactWithPendingTransfer() {
contact =
persistContactWithPendingTransfer(
newContact("sh8013"),
TRANSFER_REQUEST_TIME,
TRANSFER_EXPIRATION_TIME,
TRANSFER_REQUEST_TIME);
}
/** Changes the transfer status on the persisted contact. */
protected void changeTransferStatus(TransferStatus transferStatus) {
contact = persistResource(
contact.asBuilder()
.setTransferData(
contact.getTransferData().asBuilder().setTransferStatus(transferStatus).build())
.build());
clock.advanceOneMilli();
}
/** Changes the client ID that the flow will run as. */
@Override
protected void setRegistrarIdForFlow(String registrarId) {
sessionMetadata.setRegistrarId(registrarId);
}
}

View File

@@ -14,206 +14,24 @@
package google.registry.flows.contact;
import static com.google.common.truth.Truth.assertThat;
import static google.registry.testing.ContactSubject.assertAboutContacts;
import static google.registry.testing.DatabaseHelper.assertNoBillingEvents;
import static google.registry.testing.DatabaseHelper.persistResource;
import static google.registry.testing.EppExceptionSubject.assertAboutEppExceptions;
import static org.junit.jupiter.api.Assertions.assertThrows;
import google.registry.flows.EppException;
import google.registry.flows.FlowUtils.NotLoggedInException;
import google.registry.flows.ResourceFlowUtils.BadAuthInfoForResourceException;
import google.registry.flows.ResourceFlowUtils.ResourceDoesNotExistException;
import google.registry.flows.exceptions.NoTransferHistoryToQueryException;
import google.registry.flows.exceptions.NotAuthorizedToViewTransferException;
import google.registry.model.contact.Contact;
import google.registry.model.contact.ContactAuthInfo;
import google.registry.model.eppcommon.AuthInfo.PasswordAuth;
import google.registry.model.reporting.HistoryEntry;
import google.registry.model.transfer.TransferStatus;
import org.joda.time.DateTime;
import org.junit.jupiter.api.BeforeEach;
import google.registry.flows.FlowTestCase;
import google.registry.flows.exceptions.ContactsProhibitedException;
import org.junit.jupiter.api.Test;
/** Unit tests for {@link ContactTransferQueryFlow}. */
class ContactTransferQueryFlowTest
extends ContactTransferFlowTestCase<ContactTransferQueryFlow, Contact> {
class ContactTransferQueryFlowTest extends FlowTestCase<ContactTransferQueryFlow> {
@BeforeEach
void setUp() {
ContactTransferQueryFlowTest() {
setEppInput("contact_transfer_query.xml");
clock.setTo(DateTime.parse("2000-06-10T22:00:00.0Z"));
setRegistrarIdForFlow("NewRegistrar");
setupContactWithPendingTransfer();
}
private void doSuccessfulTest(String commandFilename, String expectedXmlFilename)
throws Exception {
setEppInput(commandFilename);
eppLoader.replaceAll("JD1234-REP", contact.getRepoId());
// Setup done; run the test.
assertMutatingFlow(false);
runFlowAssertResponse(loadFile(expectedXmlFilename));
assertAboutContacts().that(reloadResourceByForeignKey(clock.nowUtc().minusDays(1)))
.hasOneHistoryEntryEachOfTypes(HistoryEntry.Type.CONTACT_TRANSFER_REQUEST);
assertNoBillingEvents();
}
private void doFailingTest(String commandFilename) throws Exception {
setEppInput(commandFilename);
eppLoader.replaceAll("JD1234-REP", contact.getRepoId());
// Setup done; run the test.
assertMutatingFlow(false);
runFlow();
}
@Test
void testNotLoggedIn() {
sessionMetadata.setRegistrarId(null);
EppException thrown = assertThrows(NotLoggedInException.class, this::runFlow);
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
void testSuccess() throws Exception {
doSuccessfulTest("contact_transfer_query.xml", "contact_transfer_query_response.xml");
}
@Test
void testSuccess_withContactRoid() throws Exception {
doSuccessfulTest("contact_transfer_query_with_roid.xml", "contact_transfer_query_response.xml");
}
@Test
void testSuccess_sponsoringClient() throws Exception {
setRegistrarIdForFlow("TheRegistrar");
doSuccessfulTest("contact_transfer_query.xml", "contact_transfer_query_response.xml");
}
@Test
void testSuccess_withAuthinfo() throws Exception {
setRegistrarIdForFlow("ClientZ");
doSuccessfulTest("contact_transfer_query_with_authinfo.xml",
"contact_transfer_query_response.xml");
}
@Test
void testSuccess_clientApproved() throws Exception {
changeTransferStatus(TransferStatus.CLIENT_APPROVED);
doSuccessfulTest("contact_transfer_query.xml",
"contact_transfer_query_response_client_approved.xml");
}
@Test
void testSuccess_clientRejected() throws Exception {
changeTransferStatus(TransferStatus.CLIENT_REJECTED);
doSuccessfulTest("contact_transfer_query.xml",
"contact_transfer_query_response_client_rejected.xml");
}
@Test
void testSuccess_clientCancelled() throws Exception {
changeTransferStatus(TransferStatus.CLIENT_CANCELLED);
doSuccessfulTest("contact_transfer_query.xml",
"contact_transfer_query_response_client_cancelled.xml");
}
@Test
void testSuccess_serverApproved() throws Exception {
changeTransferStatus(TransferStatus.SERVER_APPROVED);
doSuccessfulTest("contact_transfer_query.xml",
"contact_transfer_query_response_server_approved.xml");
}
@Test
void testSuccess_serverCancelled() throws Exception {
changeTransferStatus(TransferStatus.SERVER_CANCELLED);
doSuccessfulTest("contact_transfer_query.xml",
"contact_transfer_query_response_server_cancelled.xml");
}
@Test
void testFailure_pendingDeleteContact() throws Exception {
changeTransferStatus(TransferStatus.SERVER_CANCELLED);
contact = persistResource(
contact.asBuilder().setDeletionTime(clock.nowUtc().plusDays(1)).build());
doSuccessfulTest("contact_transfer_query.xml",
"contact_transfer_query_response_server_cancelled.xml");
}
@Test
void testFailure_badContactPassword() {
// Change the contact's password so it does not match the password in the file.
contact =
persistResource(
contact
.asBuilder()
.setAuthInfo(ContactAuthInfo.create(PasswordAuth.create("badpassword")))
.build());
EppException thrown =
assertThrows(
BadAuthInfoForResourceException.class,
() -> doFailingTest("contact_transfer_query_with_authinfo.xml"));
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
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.
contact = contact.asBuilder().setRepoId("DEADBEEF_TLD-ROID").build();
EppException thrown =
assertThrows(
BadAuthInfoForResourceException.class,
() -> doFailingTest("contact_transfer_query_with_roid.xml"));
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
void testFailure_neverBeenTransferred() {
changeTransferStatus(null);
EppException thrown =
assertThrows(
NoTransferHistoryToQueryException.class,
() -> doFailingTest("contact_transfer_query.xml"));
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
void testFailure_unrelatedClient() {
setRegistrarIdForFlow("ClientZ");
EppException thrown =
assertThrows(
NotAuthorizedToViewTransferException.class,
() -> doFailingTest("contact_transfer_query.xml"));
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
void testFailure_deletedContact() throws Exception {
contact =
persistResource(contact.asBuilder().setDeletionTime(clock.nowUtc().minusDays(1)).build());
ResourceDoesNotExistException thrown =
assertThrows(
ResourceDoesNotExistException.class, () -> doFailingTest("contact_transfer_query.xml"));
assertThat(thrown).hasMessageThat().contains(String.format("(%s)", getUniqueIdFromCommand()));
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
void testFailure_nonexistentContact() throws Exception {
persistResource(contact.asBuilder().setDeletionTime(clock.nowUtc().minusDays(1)).build());
ResourceDoesNotExistException thrown =
assertThrows(
ResourceDoesNotExistException.class, () -> doFailingTest("contact_transfer_query.xml"));
assertThat(thrown).hasMessageThat().contains(String.format("(%s)", getUniqueIdFromCommand()));
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
void testIcannActivityReportField_getsLogged() throws Exception {
runFlow();
assertIcannReportingActivityFieldLogged("srs-cont-transfer-query");
void testThrowsException() {
assertAboutEppExceptions()
.that(assertThrows(ContactsProhibitedException.class, this::runFlow))
.marshalsToXml();
}
}

View File

@@ -14,253 +14,24 @@
package google.registry.flows.contact;
import static com.google.common.collect.MoreCollectors.onlyElement;
import static com.google.common.truth.Truth.assertThat;
import static google.registry.testing.ContactSubject.assertAboutContacts;
import static google.registry.testing.DatabaseHelper.assertNoBillingEvents;
import static google.registry.testing.DatabaseHelper.getOnlyPollMessage;
import static google.registry.testing.DatabaseHelper.getPollMessages;
import static google.registry.testing.DatabaseHelper.persistResource;
import static google.registry.testing.EppExceptionSubject.assertAboutEppExceptions;
import static org.junit.jupiter.api.Assertions.assertThrows;
import google.registry.flows.EppException;
import google.registry.flows.FlowUtils.NotLoggedInException;
import google.registry.flows.ResourceFlowUtils.BadAuthInfoForResourceException;
import google.registry.flows.ResourceFlowUtils.ResourceDoesNotExistException;
import google.registry.flows.ResourceFlowUtils.ResourceNotOwnedException;
import google.registry.flows.exceptions.NotPendingTransferException;
import google.registry.model.contact.Contact;
import google.registry.model.contact.ContactAuthInfo;
import google.registry.model.eppcommon.AuthInfo.PasswordAuth;
import google.registry.model.eppcommon.Trid;
import google.registry.model.poll.PendingActionNotificationResponse;
import google.registry.model.poll.PollMessage;
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 org.junit.jupiter.api.BeforeEach;
import google.registry.flows.FlowTestCase;
import google.registry.flows.exceptions.ContactsProhibitedException;
import org.junit.jupiter.api.Test;
/** Unit tests for {@link ContactTransferRejectFlow}. */
class ContactTransferRejectFlowTest
extends ContactTransferFlowTestCase<ContactTransferRejectFlow, Contact> {
class ContactTransferRejectFlowTest extends FlowTestCase<ContactTransferRejectFlow> {
@BeforeEach
void setUp() {
ContactTransferRejectFlowTest() {
setEppInput("contact_transfer_reject.xml");
setRegistrarIdForFlow("TheRegistrar");
setupContactWithPendingTransfer();
clock.advanceOneMilli();
}
private void doSuccessfulTest(String commandFilename, String expectedXmlFilename)
throws Exception {
setEppInput(commandFilename);
// Look in the future and make sure the poll messages for implicit ack are there.
assertThat(getPollMessages("NewRegistrar", clock.nowUtc().plusMonths(1)))
.hasSize(1);
assertThat(getPollMessages("TheRegistrar", clock.nowUtc().plusMonths(1)))
.hasSize(1);
// Setup done; run the test.
contact = reloadResourceByForeignKey();
TransferData originalTransferData = contact.getTransferData();
assertMutatingFlow(true);
runFlowAssertResponse(loadFile(expectedXmlFilename));
// Transfer should have failed. Verify correct fields were set.
contact = reloadResourceByForeignKey();
assertAboutContacts()
.that(contact)
.hasCurrentSponsorRegistrarId("TheRegistrar")
.and()
.hasLastTransferTimeNotEqualTo(clock.nowUtc())
.and()
.hasOneHistoryEntryEachOfTypes(
HistoryEntry.Type.CONTACT_TRANSFER_REQUEST, HistoryEntry.Type.CONTACT_TRANSFER_REJECT);
assertThat(contact.getTransferData())
.isEqualTo(
originalTransferData.copyConstantFieldsToBuilder()
.setTransferStatus(TransferStatus.CLIENT_REJECTED)
.setPendingTransferExpirationTime(clock.nowUtc())
.build());
// The poll message (in the future) to the losing registrar for implicit ack should be gone.
assertThat(getPollMessages("TheRegistrar", clock.nowUtc().plusMonths(1)))
.isEmpty();
// The poll message in the future to the gaining registrar should be gone too, but there
// should be one at the current time to the gaining registrar.
PollMessage gainingPollMessage = getOnlyPollMessage("NewRegistrar");
assertThat(gainingPollMessage.getEventTime()).isEqualTo(clock.nowUtc());
assertThat(
gainingPollMessage
.getResponseData()
.stream()
.filter(TransferResponse.class::isInstance)
.map(TransferResponse.class::cast)
.collect(onlyElement())
.getTransferStatus())
.isEqualTo(TransferStatus.CLIENT_REJECTED);
PendingActionNotificationResponse panData =
gainingPollMessage
.getResponseData()
.stream()
.filter(PendingActionNotificationResponse.class::isInstance)
.map(PendingActionNotificationResponse.class::cast)
.collect(onlyElement());
assertThat(panData.getTrid())
.isEqualTo(Trid.create("transferClient-trid", "transferServer-trid"));
assertThat(panData.getActionResult()).isFalse();
assertNoBillingEvents();
assertLastHistoryContainsResource(contact);
}
private void doFailingTest(String commandFilename) throws Exception {
setEppInput(commandFilename);
// Setup done; run the test.
assertMutatingFlow(true);
runFlow();
}
@Test
void testNotLoggedIn() {
sessionMetadata.setRegistrarId(null);
EppException thrown = assertThrows(NotLoggedInException.class, this::runFlow);
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
void testDryRun() throws Exception {
setEppInput("contact_transfer_reject.xml");
dryRunFlowAssertResponse(loadFile("contact_transfer_reject_response.xml"));
}
@Test
void testSuccess() throws Exception {
doSuccessfulTest("contact_transfer_reject.xml", "contact_transfer_reject_response.xml");
}
@Test
void testSuccess_domainAuthInfo() throws Exception {
doSuccessfulTest("contact_transfer_reject_with_authinfo.xml",
"contact_transfer_reject_response.xml");
}
@Test
void testFailure_badPassword() {
// Change the contact's password so it does not match the password in the file.
contact =
persistResource(
contact
.asBuilder()
.setAuthInfo(ContactAuthInfo.create(PasswordAuth.create("badpassword")))
.build());
EppException thrown =
assertThrows(
BadAuthInfoForResourceException.class,
() -> doFailingTest("contact_transfer_reject_with_authinfo.xml"));
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
void testFailure_neverBeenTransferred() {
changeTransferStatus(null);
EppException thrown =
assertThrows(
NotPendingTransferException.class, () -> doFailingTest("contact_transfer_reject.xml"));
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
void testFailure_clientApproved() {
changeTransferStatus(TransferStatus.CLIENT_APPROVED);
EppException thrown =
assertThrows(
NotPendingTransferException.class, () -> doFailingTest("contact_transfer_reject.xml"));
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
void testFailure_clientRejected() {
changeTransferStatus(TransferStatus.CLIENT_REJECTED);
EppException thrown =
assertThrows(
NotPendingTransferException.class, () -> doFailingTest("contact_transfer_reject.xml"));
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
void testFailure_clientCancelled() {
changeTransferStatus(TransferStatus.CLIENT_CANCELLED);
EppException thrown =
assertThrows(
NotPendingTransferException.class, () -> doFailingTest("contact_transfer_reject.xml"));
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
void testFailure_serverApproved() {
changeTransferStatus(TransferStatus.SERVER_APPROVED);
EppException thrown =
assertThrows(
NotPendingTransferException.class, () -> doFailingTest("contact_transfer_reject.xml"));
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
void testFailure_serverCancelled() {
changeTransferStatus(TransferStatus.SERVER_CANCELLED);
EppException thrown =
assertThrows(
NotPendingTransferException.class, () -> doFailingTest("contact_transfer_reject.xml"));
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
void testFailure_gainingClient() {
setRegistrarIdForFlow("NewRegistrar");
EppException thrown =
assertThrows(
ResourceNotOwnedException.class, () -> doFailingTest("contact_transfer_reject.xml"));
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
void testFailure_unrelatedClient() {
setRegistrarIdForFlow("ClientZ");
EppException thrown =
assertThrows(
ResourceNotOwnedException.class, () -> doFailingTest("contact_transfer_reject.xml"));
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
void testFailure_deletedContact() throws Exception {
contact =
persistResource(contact.asBuilder().setDeletionTime(clock.nowUtc().minusDays(1)).build());
ResourceDoesNotExistException thrown =
assertThrows(
ResourceDoesNotExistException.class,
() -> doFailingTest("contact_transfer_reject.xml"));
assertThat(thrown).hasMessageThat().contains(String.format("(%s)", getUniqueIdFromCommand()));
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
void testFailure_nonexistentContact() throws Exception {
persistResource(contact.asBuilder().setDeletionTime(clock.nowUtc().minusDays(1)).build());
ResourceDoesNotExistException thrown =
assertThrows(
ResourceDoesNotExistException.class,
() -> doFailingTest("contact_transfer_reject.xml"));
assertThat(thrown).hasMessageThat().contains(String.format("(%s)", getUniqueIdFromCommand()));
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
void testIcannActivityReportField_getsLogged() throws Exception {
runFlow();
assertIcannReportingActivityFieldLogged("srs-cont-transfer-reject");
void testThrowsException() {
assertAboutEppExceptions()
.that(assertThrows(ContactsProhibitedException.class, this::runFlow))
.marshalsToXml();
}
}

View File

@@ -14,304 +14,24 @@
package google.registry.flows.contact;
import static com.google.common.base.Predicates.equalTo;
import static com.google.common.base.Predicates.not;
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.testing.ContactSubject.assertAboutContacts;
import static google.registry.testing.DatabaseHelper.assertNoBillingEvents;
import static google.registry.testing.DatabaseHelper.assertPollMessagesEqual;
import static google.registry.testing.DatabaseHelper.deleteResource;
import static google.registry.testing.DatabaseHelper.getPollMessages;
import static google.registry.testing.DatabaseHelper.loadByKeys;
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 google.registry.flows.EppException;
import google.registry.flows.FlowUtils.NotLoggedInException;
import google.registry.flows.ResourceFlowUtils.BadAuthInfoForResourceException;
import google.registry.flows.ResourceFlowUtils.ResourceDoesNotExistException;
import google.registry.flows.exceptions.AlreadyPendingTransferException;
import google.registry.flows.exceptions.MissingTransferRequestAuthInfoException;
import google.registry.flows.exceptions.ObjectAlreadySponsoredException;
import google.registry.flows.exceptions.ResourceStatusProhibitsOperationException;
import google.registry.model.contact.Contact;
import google.registry.model.contact.ContactAuthInfo;
import google.registry.model.eppcommon.AuthInfo.PasswordAuth;
import google.registry.model.eppcommon.StatusValue;
import google.registry.model.eppcommon.Trid;
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 org.joda.time.DateTime;
import org.junit.jupiter.api.BeforeEach;
import google.registry.flows.FlowTestCase;
import google.registry.flows.exceptions.ContactsProhibitedException;
import org.junit.jupiter.api.Test;
/** Unit tests for {@link ContactTransferRequestFlow}. */
class ContactTransferRequestFlowTest
extends ContactTransferFlowTestCase<ContactTransferRequestFlow, Contact> {
class ContactTransferRequestFlowTest extends FlowTestCase<ContactTransferRequestFlow> {
ContactTransferRequestFlowTest() {
// We need the transfer to happen at exactly this time in order for the response to match up.
clock.setTo(DateTime.parse("2000-06-08T22:00:00.0Z"));
}
@BeforeEach
void beforeEach() {
setEppInput("contact_transfer_request.xml");
setRegistrarIdForFlow("NewRegistrar");
contact = persistActiveContact("sh8013");
clock.advanceOneMilli();
}
private void doSuccessfulTest(String commandFilename, String expectedXmlFilename)
throws Exception {
setEppInput(commandFilename);
DateTime afterTransfer = clock.nowUtc().plus(getContactAutomaticTransferLength());
// Setup done; run the test.
assertMutatingFlow(true);
runFlowAssertResponse(loadFile(expectedXmlFilename));
// Transfer should have been requested. Verify correct fields were set.
contact = reloadResourceByForeignKey();
assertAboutContacts()
.that(contact)
.hasCurrentSponsorRegistrarId("TheRegistrar")
.and()
.hasOnlyOneHistoryEntryWhich()
.hasType(HistoryEntry.Type.CONTACT_TRANSFER_REQUEST);
Trid expectedTrid =
Trid.create(
getClientTrid(),
contact.getTransferData().getTransferRequestTrid().getServerTransactionId());
assertThat(contact.getTransferData())
.isEqualTo(
new ContactTransferData.Builder()
.setTransferRequestTrid(expectedTrid)
.setTransferRequestTime(clock.nowUtc())
.setGainingRegistrarId("NewRegistrar")
.setLosingRegistrarId("TheRegistrar")
.setTransferStatus(TransferStatus.PENDING)
.setPendingTransferExpirationTime(afterTransfer)
// Make the server-approve entities field a no-op comparison; it's easier to
// do this comparison separately below.
.setServerApproveEntities(
contact.getRepoId(),
contact.getTransferData().getHistoryEntryId(),
forceEmptyToNull(contact.getTransferData().getServerApproveEntities()))
.build());
assertNoBillingEvents();
assertThat(getPollMessages("TheRegistrar", clock.nowUtc())).hasSize(1);
PollMessage losingRequestMessage =
getOnlyElement(getPollMessages("TheRegistrar", clock.nowUtc()));
// If we fast forward AUTOMATIC_TRANSFER_DAYS the transfer should have happened.
assertAboutContacts()
.that(contact.cloneProjectedAtTime(afterTransfer))
.hasCurrentSponsorRegistrarId("NewRegistrar");
assertThat(getPollMessages("NewRegistrar", afterTransfer)).hasSize(1);
assertThat(getPollMessages("TheRegistrar", afterTransfer)).hasSize(2);
PollMessage gainingApproveMessage =
getOnlyElement(getPollMessages("NewRegistrar", afterTransfer));
PollMessage losingApproveMessage =
getPollMessages("TheRegistrar", afterTransfer)
.stream()
.filter(not(equalTo(losingRequestMessage)))
.collect(onlyElement());
// Check for TransferData server-approve entities containing what we expect: only
// poll messages, the approval notice ones for gaining and losing registrars.
assertPollMessagesEqual(
Iterables.filter(
loadByKeys(contact.getTransferData().getServerApproveEntities()), PollMessage.class),
ImmutableList.of(gainingApproveMessage, losingApproveMessage));
assertLastHistoryContainsResource(contact);
}
private void doFailingTest(String commandFilename) throws Exception {
setEppInput(commandFilename);
// Setup done; run the test.
assertMutatingFlow(true);
runFlow();
}
@Test
void testNotLoggedIn() {
sessionMetadata.setRegistrarId(null);
EppException thrown = assertThrows(NotLoggedInException.class, this::runFlow);
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
void testDryRun() throws Exception {
setEppInput("contact_transfer_request.xml");
dryRunFlowAssertResponse(loadFile("contact_transfer_request_response.xml"));
}
@Test
void testSuccess() throws Exception {
doSuccessfulTest("contact_transfer_request.xml", "contact_transfer_request_response.xml");
}
@Test
void testFailure_noAuthInfo() {
EppException thrown =
assertThrows(
MissingTransferRequestAuthInfoException.class,
() -> doFailingTest("contact_transfer_request_no_authinfo.xml"));
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
void testFailure_badPassword() {
// Change the contact's password so it does not match the password in the file.
contact =
persistResource(
contact
.asBuilder()
.setAuthInfo(ContactAuthInfo.create(PasswordAuth.create("badpassword")))
.build());
EppException thrown =
assertThrows(
BadAuthInfoForResourceException.class,
() -> doFailingTest("contact_transfer_request.xml"));
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
void testSuccess_clientApproved() throws Exception {
changeTransferStatus(TransferStatus.CLIENT_APPROVED);
doSuccessfulTest("contact_transfer_request.xml", "contact_transfer_request_response.xml");
}
@Test
void testSuccess_clientRejected() throws Exception {
changeTransferStatus(TransferStatus.CLIENT_REJECTED);
doSuccessfulTest("contact_transfer_request.xml", "contact_transfer_request_response.xml");
}
@Test
void testSuccess_clientCancelled() throws Exception {
changeTransferStatus(TransferStatus.CLIENT_CANCELLED);
doSuccessfulTest("contact_transfer_request.xml", "contact_transfer_request_response.xml");
}
@Test
void testSuccess_serverApproved() throws Exception {
changeTransferStatus(TransferStatus.SERVER_APPROVED);
doSuccessfulTest("contact_transfer_request.xml", "contact_transfer_request_response.xml");
}
@Test
void testSuccess_serverCancelled() throws Exception {
changeTransferStatus(TransferStatus.SERVER_CANCELLED);
doSuccessfulTest("contact_transfer_request.xml", "contact_transfer_request_response.xml");
}
@Test
void testFailure_pending() {
contact =
persistResource(
contact
.asBuilder()
.setTransferData(
contact
.getTransferData()
.asBuilder()
.setTransferStatus(TransferStatus.PENDING)
.setPendingTransferExpirationTime(clock.nowUtc().plusDays(1))
.build())
.build());
EppException thrown =
assertThrows(
AlreadyPendingTransferException.class,
() -> doFailingTest("contact_transfer_request.xml"));
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
void testFailure_sponsoringClient() {
setRegistrarIdForFlow("TheRegistrar");
EppException thrown =
assertThrows(
ObjectAlreadySponsoredException.class,
() -> doFailingTest("contact_transfer_request.xml"));
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
void testFailure_deletedContact() throws Exception {
contact =
persistResource(contact.asBuilder().setDeletionTime(clock.nowUtc().minusDays(1)).build());
ResourceDoesNotExistException thrown =
assertThrows(
ResourceDoesNotExistException.class,
() -> doFailingTest("contact_transfer_request.xml"));
assertThat(thrown).hasMessageThat().contains(String.format("(%s)", getUniqueIdFromCommand()));
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
void testFailure_nonexistentContact() throws Exception {
deleteResource(contact);
ResourceDoesNotExistException thrown =
assertThrows(
ResourceDoesNotExistException.class,
() -> doFailingTest("contact_transfer_request.xml"));
assertThat(thrown).hasMessageThat().contains(String.format("(%s)", getUniqueIdFromCommand()));
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
void testFailure_clientTransferProhibited() {
contact =
persistResource(
contact.asBuilder().addStatusValue(StatusValue.CLIENT_TRANSFER_PROHIBITED).build());
ResourceStatusProhibitsOperationException thrown =
assertThrows(
ResourceStatusProhibitsOperationException.class,
() -> doFailingTest("contact_transfer_request.xml"));
assertThat(thrown).hasMessageThat().contains("clientTransferProhibited");
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
void testFailure_serverTransferProhibited() {
contact =
persistResource(
contact.asBuilder().addStatusValue(StatusValue.SERVER_TRANSFER_PROHIBITED).build());
ResourceStatusProhibitsOperationException thrown =
assertThrows(
ResourceStatusProhibitsOperationException.class,
() -> doFailingTest("contact_transfer_request.xml"));
assertThat(thrown).hasMessageThat().contains("serverTransferProhibited");
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
void testFailure_pendingDelete() {
contact =
persistResource(contact.asBuilder().addStatusValue(StatusValue.PENDING_DELETE).build());
ResourceStatusProhibitsOperationException thrown =
assertThrows(
ResourceStatusProhibitsOperationException.class,
() -> doFailingTest("contact_transfer_request.xml"));
assertThat(thrown).hasMessageThat().contains("pendingDelete");
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
void testIcannActivityReportField_getsLogged() throws Exception {
runFlow();
assertIcannReportingActivityFieldLogged("srs-cont-transfer-request");
void testThrowsException() {
assertAboutEppExceptions()
.that(assertThrows(ContactsProhibitedException.class, this::runFlow))
.marshalsToXml();
}
}

View File

@@ -14,449 +14,24 @@
package google.registry.flows.contact;
import static com.google.common.truth.Truth.assertThat;
import static google.registry.model.common.FeatureFlag.FeatureName.MINIMUM_DATASET_CONTACTS_PROHIBITED;
import static google.registry.model.common.FeatureFlag.FeatureStatus.ACTIVE;
import static google.registry.model.common.FeatureFlag.FeatureStatus.INACTIVE;
import static google.registry.testing.ContactSubject.assertAboutContacts;
import static google.registry.testing.DatabaseHelper.assertNoBillingEvents;
import static google.registry.testing.DatabaseHelper.newContact;
import static google.registry.testing.DatabaseHelper.persistActiveContact;
import static google.registry.testing.DatabaseHelper.persistDeletedContact;
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.junit.jupiter.api.Assertions.assertThrows;
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.FlowUtils.NotLoggedInException;
import google.registry.flows.ResourceFlowTestCase;
import google.registry.flows.ResourceFlowUtils.AddRemoveSameValueException;
import google.registry.flows.ResourceFlowUtils.ResourceDoesNotExistException;
import google.registry.flows.ResourceFlowUtils.ResourceNotOwnedException;
import google.registry.flows.ResourceFlowUtils.StatusNotClientSettableException;
import google.registry.flows.contact.ContactFlowUtils.BadInternationalizedPostalInfoException;
import google.registry.flows.contact.ContactFlowUtils.DeclineContactDisclosureFieldDisallowedPolicyException;
import google.registry.flows.FlowTestCase;
import google.registry.flows.exceptions.ContactsProhibitedException;
import google.registry.flows.exceptions.ResourceHasClientUpdateProhibitedException;
import google.registry.flows.exceptions.ResourceStatusProhibitsOperationException;
import google.registry.model.common.FeatureFlag;
import google.registry.model.contact.Contact;
import google.registry.model.contact.ContactAddress;
import google.registry.model.contact.PostalInfo;
import google.registry.model.contact.PostalInfo.Type;
import google.registry.model.eppcommon.StatusValue;
import org.junit.jupiter.api.Test;
/** Unit tests for {@link ContactUpdateFlow}. */
class ContactUpdateFlowTest extends ResourceFlowTestCase<ContactUpdateFlow, Contact> {
class ContactUpdateFlowTest extends FlowTestCase<ContactUpdateFlow> {
ContactUpdateFlowTest() {
setEppInput("contact_update.xml");
}
private void doSuccessfulTest() throws Exception {
clock.advanceOneMilli();
assertMutatingFlow(true);
runFlowAssertResponse(loadFile("generic_success_response.xml"));
Contact contact = reloadResourceByForeignKey();
// Check that the contact was updated. This value came from the xml.
assertAboutContacts()
.that(contact)
.hasAuthInfoPwd("2fooBAR")
.and()
.hasOnlyOneHistoryEntryWhich()
.hasNoXml();
assertNoBillingEvents();
assertLastHistoryContainsResource(contact);
}
@Test
void testNotLoggedIn() {
sessionMetadata.setRegistrarId(null);
EppException thrown = assertThrows(NotLoggedInException.class, this::runFlow);
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
void testDryRun() throws Exception {
persistActiveContact(getUniqueIdFromCommand());
dryRunFlowAssertResponse(loadFile("generic_success_response.xml"));
}
@Test
void testSuccess() throws Exception {
persistActiveContact(getUniqueIdFromCommand());
doSuccessfulTest();
}
@Test
void testFailure_minimumDatasetPhase2_cannotUpdateContacts() throws Exception {
persistResource(
new FeatureFlag.Builder()
.setFeatureName(MINIMUM_DATASET_CONTACTS_PROHIBITED)
.setStatusMap(
ImmutableSortedMap.of(START_OF_TIME, INACTIVE, clock.nowUtc().minusDays(5), ACTIVE))
.build());
EppException thrown = assertThrows(ContactsProhibitedException.class, this::runFlow);
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
void testSuccess_updatingInternationalizedPostalInfoDeletesLocalized() throws Exception {
Contact contact =
persistResource(
newContact(getUniqueIdFromCommand())
.asBuilder()
.setLocalizedPostalInfo(
new PostalInfo.Builder()
.setType(Type.LOCALIZED)
.setAddress(
new ContactAddress.Builder()
.setStreet(ImmutableList.of("111 8th Ave", "4th Floor"))
.setCity("New York")
.setState("NY")
.setZip("10011")
.setCountryCode("US")
.build())
.build())
.build());
clock.advanceOneMilli();
// The test xml updates the internationalized postal info and should therefore implicitly delete
// the localized one since they are treated as a pair for update purposes.
assertAboutContacts().that(contact)
.hasNonNullLocalizedPostalInfo().and()
.hasNullInternationalizedPostalInfo();
runFlowAssertResponse(loadFile("generic_success_response.xml"));
assertAboutContacts().that(reloadResourceByForeignKey())
.hasNullLocalizedPostalInfo().and()
.hasInternationalizedPostalInfo(new PostalInfo.Builder()
.setType(Type.INTERNATIONALIZED)
.setAddress(new ContactAddress.Builder()
.setStreet(ImmutableList.of("124 Example Dr.", "Suite 200"))
.setCity("Dulles")
.setState("VA")
.setZip("20166-6503")
.setCountryCode("US")
.build())
.build());
}
@Test
void testSuccess_updatingLocalizedPostalInfoDeletesInternationalized() throws Exception {
setEppInput("contact_update_localized.xml");
Contact contact =
persistResource(
newContact(getUniqueIdFromCommand())
.asBuilder()
.setInternationalizedPostalInfo(
new PostalInfo.Builder()
.setType(Type.INTERNATIONALIZED)
.setAddress(
new ContactAddress.Builder()
.setStreet(ImmutableList.of("111 8th Ave", "4th Floor"))
.setCity("New York")
.setState("NY")
.setZip("10011")
.setCountryCode("US")
.build())
.build())
.build());
clock.advanceOneMilli();
// The test xml updates the localized postal info and should therefore implicitly delete
// the internationalized one since they are treated as a pair for update purposes.
assertAboutContacts().that(contact)
.hasNonNullInternationalizedPostalInfo().and()
.hasNullLocalizedPostalInfo();
runFlowAssertResponse(loadFile("generic_success_response.xml"));
assertAboutContacts().that(reloadResourceByForeignKey())
.hasNullInternationalizedPostalInfo().and()
.hasLocalizedPostalInfo(new PostalInfo.Builder()
.setType(Type.LOCALIZED)
.setAddress(new ContactAddress.Builder()
.setStreet(ImmutableList.of("124 Example Dr.", "Suite 200"))
.setCity("Dulles")
.setState("VA")
.setZip("20166-6503")
.setCountryCode("US")
.build())
.build());
}
@Test
void testSuccess_partialPostalInfoUpdate() throws Exception {
setEppInput("contact_update_partial_postalinfo.xml");
persistResource(
newContact(getUniqueIdFromCommand())
.asBuilder()
.setLocalizedPostalInfo(
new PostalInfo.Builder()
.setType(Type.LOCALIZED)
.setName("A. Person")
.setOrg("Company Inc.")
.setAddress(
new ContactAddress.Builder()
.setStreet(ImmutableList.of("123 4th st", "5th Floor"))
.setCity("City")
.setState("AB")
.setZip("12345")
.setCountryCode("US")
.build())
.build())
.build());
clock.advanceOneMilli();
// The test xml updates the address of the postal info and should leave the name untouched.
runFlowAssertResponse(loadFile("generic_success_response.xml"));
assertAboutContacts().that(reloadResourceByForeignKey()).hasLocalizedPostalInfo(
new PostalInfo.Builder()
.setType(Type.LOCALIZED)
.setName("A. Person")
.setOrg("Company Inc.")
.setAddress(new ContactAddress.Builder()
.setStreet(ImmutableList.of("456 5th st"))
.setCity("Place")
.setState("CD")
.setZip("54321")
.setCountryCode("US")
.build())
.build());
}
@Test
void testSuccess_updateOnePostalInfo_touchOtherPostalInfoPreservesIt() throws Exception {
setEppInput("contact_update_partial_postalinfo_preserve_int.xml");
persistResource(
newContact(getUniqueIdFromCommand())
.asBuilder()
.setLocalizedPostalInfo(
new PostalInfo.Builder()
.setType(Type.LOCALIZED)
.setName("A. Person")
.setOrg("Company Inc.")
.setAddress(
new ContactAddress.Builder()
.setStreet(ImmutableList.of("123 4th st", "5th Floor"))
.setCity("City")
.setState("AB")
.setZip("12345")
.setCountryCode("US")
.build())
.build())
.setInternationalizedPostalInfo(
new PostalInfo.Builder()
.setType(Type.INTERNATIONALIZED)
.setName("B. Person")
.setOrg("Company Co.")
.setAddress(
new ContactAddress.Builder()
.setStreet(ImmutableList.of("100 200th Dr.", "6th Floor"))
.setCity("Town")
.setState("CD")
.setZip("67890")
.setCountryCode("US")
.build())
.build())
.build());
clock.advanceOneMilli();
// The test xml updates the address of the localized postal info. It also sets the name of the
// internationalized postal info to the same value it previously had, which causes it to be
// preserved. If the xml had not mentioned the internationalized one at all it would have been
// deleted.
runFlowAssertResponse(loadFile("generic_success_response.xml"));
assertAboutContacts().that(reloadResourceByForeignKey())
.hasLocalizedPostalInfo(
new PostalInfo.Builder()
.setType(Type.LOCALIZED)
.setName("A. Person")
.setOrg("Company Inc.")
.setAddress(new ContactAddress.Builder()
.setStreet(ImmutableList.of("456 5th st"))
.setCity("Place")
.setState("CD")
.setZip("54321")
.setCountryCode("US")
.build())
.build())
.and()
.hasInternationalizedPostalInfo(
new PostalInfo.Builder()
.setType(Type.INTERNATIONALIZED)
.setName("B. Person")
.setOrg("Company Co.")
.setAddress(new ContactAddress.Builder()
.setStreet(ImmutableList.of("100 200th Dr.", "6th Floor"))
.setCity("Town")
.setState("CD")
.setZip("67890")
.setCountryCode("US")
.build())
.build());
}
@Test
void testFailure_neverExisted() throws Exception {
ResourceDoesNotExistException thrown =
assertThrows(ResourceDoesNotExistException.class, this::runFlow);
assertThat(thrown).hasMessageThat().contains(String.format("(%s)", getUniqueIdFromCommand()));
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
void testFailure_existedButWasDeleted() throws Exception {
persistDeletedContact(getUniqueIdFromCommand(), clock.nowUtc().minusDays(1));
ResourceDoesNotExistException thrown =
assertThrows(ResourceDoesNotExistException.class, this::runFlow);
assertThat(thrown).hasMessageThat().contains(String.format("(%s)", getUniqueIdFromCommand()));
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
void testFailure_statusValueNotClientSettable() throws Exception {
setEppInput("contact_update_prohibited_status.xml");
persistActiveContact(getUniqueIdFromCommand());
EppException thrown = assertThrows(StatusNotClientSettableException.class, this::runFlow);
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
void testSuccess_superuserStatusValueNotClientSettable() throws Exception {
setEppInput("contact_update_prohibited_status.xml");
persistActiveContact(getUniqueIdFromCommand());
clock.advanceOneMilli();
runFlowAssertResponse(
CommitMode.LIVE, UserPrivileges.SUPERUSER, loadFile("generic_success_response.xml"));
}
@Test
void testFailure_unauthorizedClient() throws Exception {
sessionMetadata.setRegistrarId("NewRegistrar");
persistActiveContact(getUniqueIdFromCommand());
EppException thrown = assertThrows(ResourceNotOwnedException.class, this::runFlow);
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
void testSuccess_superuserUnauthorizedClient() throws Exception {
sessionMetadata.setRegistrarId("NewRegistrar");
persistActiveContact(getUniqueIdFromCommand());
clock.advanceOneMilli();
runFlowAssertResponse(
CommitMode.LIVE, UserPrivileges.SUPERUSER, loadFile("generic_success_response.xml"));
}
@Test
void testSuccess_clientUpdateProhibited_removed() throws Exception {
setEppInput("contact_update_remove_client_update_prohibited.xml");
persistResource(
newContact(getUniqueIdFromCommand())
.asBuilder()
.setStatusValues(ImmutableSet.of(StatusValue.CLIENT_UPDATE_PROHIBITED))
.build());
doSuccessfulTest();
assertAboutContacts()
.that(reloadResourceByForeignKey())
.doesNotHaveStatusValue(StatusValue.CLIENT_UPDATE_PROHIBITED);
}
@Test
void testSuccess_superuserClientUpdateProhibited_notRemoved() throws Exception {
setEppInput("contact_update_prohibited_status.xml");
persistResource(
newContact(getUniqueIdFromCommand())
.asBuilder()
.setStatusValues(ImmutableSet.of(StatusValue.CLIENT_UPDATE_PROHIBITED))
.build());
clock.advanceOneMilli();
runFlowAssertResponse(
CommitMode.LIVE, UserPrivileges.SUPERUSER, loadFile("generic_success_response.xml"));
assertAboutContacts()
.that(reloadResourceByForeignKey())
.hasStatusValue(StatusValue.CLIENT_UPDATE_PROHIBITED)
.and()
.hasStatusValue(StatusValue.SERVER_DELETE_PROHIBITED);
}
@Test
void testFailure_clientUpdateProhibited_notRemoved() throws Exception {
persistResource(
newContact(getUniqueIdFromCommand())
.asBuilder()
.setStatusValues(ImmutableSet.of(StatusValue.CLIENT_UPDATE_PROHIBITED))
.build());
EppException thrown =
assertThrows(ResourceHasClientUpdateProhibitedException.class, this::runFlow);
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
void testFailure_serverUpdateProhibited() throws Exception {
persistResource(
newContact(getUniqueIdFromCommand())
.asBuilder()
.setStatusValues(ImmutableSet.of(StatusValue.SERVER_UPDATE_PROHIBITED))
.build());
ResourceStatusProhibitsOperationException thrown =
assertThrows(ResourceStatusProhibitsOperationException.class, this::runFlow);
assertThat(thrown).hasMessageThat().contains("serverUpdateProhibited");
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
void testFailure_pendingDeleteProhibited() throws Exception {
persistResource(
newContact(getUniqueIdFromCommand())
.asBuilder()
.setStatusValues(ImmutableSet.of(StatusValue.PENDING_DELETE))
.build());
ResourceStatusProhibitsOperationException thrown =
assertThrows(ResourceStatusProhibitsOperationException.class, this::runFlow);
assertThat(thrown).hasMessageThat().contains("pendingDelete");
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
void testSuccess_nonAsciiInLocAddress() throws Exception {
setEppInput("contact_update_hebrew_loc.xml");
persistActiveContact(getUniqueIdFromCommand());
doSuccessfulTest();
}
@Test
void testFailure_nonAsciiInIntAddress() throws Exception {
setEppInput("contact_update_hebrew_int.xml");
persistActiveContact(getUniqueIdFromCommand());
EppException thrown =
assertThrows(BadInternationalizedPostalInfoException.class, this::runFlow);
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
void testFailure_declineDisclosure() throws Exception {
setEppInput("contact_update_decline_disclosure.xml");
persistActiveContact(getUniqueIdFromCommand());
EppException thrown =
assertThrows(DeclineContactDisclosureFieldDisallowedPolicyException.class, this::runFlow);
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
void testFailure_addRemoveSameValue() throws Exception {
setEppInput("contact_update_add_remove_same.xml");
persistActiveContact(getUniqueIdFromCommand());
EppException thrown = assertThrows(AddRemoveSameValueException.class, this::runFlow);
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
void testIcannActivityReportField_getsLogged() throws Exception {
persistActiveContact(getUniqueIdFromCommand());
clock.advanceOneMilli();
runFlow();
assertIcannReportingActivityFieldLogged("srs-cont-update");
void testThrowsException() {
assertAboutEppExceptions()
.that(assertThrows(ContactsProhibitedException.class, this::runFlow))
.marshalsToXml();
}
}

View File

@@ -24,10 +24,6 @@ import static google.registry.model.billing.BillingBase.Flag.RESERVED;
import static google.registry.model.billing.BillingBase.Flag.SUNRISE;
import static google.registry.model.billing.BillingBase.RenewalPriceBehavior.NONPREMIUM;
import static google.registry.model.billing.BillingBase.RenewalPriceBehavior.SPECIFIED;
import static google.registry.model.common.FeatureFlag.FeatureName.MINIMUM_DATASET_CONTACTS_OPTIONAL;
import static google.registry.model.common.FeatureFlag.FeatureName.MINIMUM_DATASET_CONTACTS_PROHIBITED;
import static google.registry.model.common.FeatureFlag.FeatureStatus.ACTIVE;
import static google.registry.model.common.FeatureFlag.FeatureStatus.INACTIVE;
import static google.registry.model.domain.fee.Fee.FEE_EXTENSION_URIS;
import static google.registry.model.domain.token.AllocationToken.TokenType.BULK_PRICING;
import static google.registry.model.domain.token.AllocationToken.TokenType.DEFAULT_PROMO;
@@ -53,7 +49,6 @@ import static google.registry.testing.DatabaseHelper.deleteTld;
import static google.registry.testing.DatabaseHelper.getHistoryEntries;
import static google.registry.testing.DatabaseHelper.loadAllOf;
import static google.registry.testing.DatabaseHelper.loadRegistrar;
import static google.registry.testing.DatabaseHelper.newContact;
import static google.registry.testing.DatabaseHelper.newHost;
import static google.registry.testing.DatabaseHelper.persistActiveContact;
import static google.registry.testing.DatabaseHelper.persistActiveDomain;
@@ -104,7 +99,6 @@ import google.registry.flows.domain.DomainFlowUtils.DomainLabelBlockedByBsaExcep
import google.registry.flows.domain.DomainFlowUtils.DomainLabelTooLongException;
import google.registry.flows.domain.DomainFlowUtils.DomainNameExistsAsTldException;
import google.registry.flows.domain.DomainFlowUtils.DomainReservedException;
import google.registry.flows.domain.DomainFlowUtils.DuplicateContactForRoleException;
import google.registry.flows.domain.DomainFlowUtils.EmptyDomainNamePartException;
import google.registry.flows.domain.DomainFlowUtils.ExceedsMaxRegistrationYearsException;
import google.registry.flows.domain.DomainFlowUtils.ExpiredClaimException;
@@ -123,12 +117,9 @@ import google.registry.flows.domain.DomainFlowUtils.LinkedResourceInPendingDelet
import google.registry.flows.domain.DomainFlowUtils.LinkedResourcesDoNotExistException;
import google.registry.flows.domain.DomainFlowUtils.MalformedTcnIdException;
import google.registry.flows.domain.DomainFlowUtils.MaxSigLifeNotSupportedException;
import google.registry.flows.domain.DomainFlowUtils.MissingAdminContactException;
import google.registry.flows.domain.DomainFlowUtils.MissingBillingAccountMapException;
import google.registry.flows.domain.DomainFlowUtils.MissingClaimsNoticeException;
import google.registry.flows.domain.DomainFlowUtils.MissingContactTypeException;
import google.registry.flows.domain.DomainFlowUtils.MissingRegistrantException;
import google.registry.flows.domain.DomainFlowUtils.MissingTechnicalContactException;
import google.registry.flows.domain.DomainFlowUtils.NameserversNotAllowedForTldException;
import google.registry.flows.domain.DomainFlowUtils.NameserversNotSpecifiedForTldWithNameserverAllowListException;
import google.registry.flows.domain.DomainFlowUtils.NotAuthorizedForTldException;
@@ -155,7 +146,6 @@ import google.registry.model.billing.BillingBase.Reason;
import google.registry.model.billing.BillingBase.RenewalPriceBehavior;
import google.registry.model.billing.BillingEvent;
import google.registry.model.billing.BillingRecurrence;
import google.registry.model.common.FeatureFlag;
import google.registry.model.domain.Domain;
import google.registry.model.domain.DomainHistory;
import google.registry.model.domain.GracePeriod;
@@ -1945,28 +1935,6 @@ class DomainCreateFlowTest extends ResourceFlowTestCase<DomainCreateFlow, Domain
assertThat(thrown).hasMessageThat().contains("ns2.example.net");
}
@Test
void testFailure_missingContact() {
persistActiveHost("ns1.example.net");
persistActiveHost("ns2.example.net");
persistActiveContact("jd1234");
LinkedResourcesDoNotExistException thrown =
assertThrows(LinkedResourcesDoNotExistException.class, this::runFlow);
assertThat(thrown).hasMessageThat().contains("(sh8013)");
}
@Test
void testFailure_pendingDeleteContact() {
persistActiveHost("ns1.example.net");
persistActiveHost("ns2.example.net");
persistActiveContact("sh8013");
persistResource(newContact("jd1234").asBuilder().addStatusValue(PENDING_DELETE).build());
clock.advanceOneMilli();
LinkedResourceInPendingDeleteProhibitsOperationException thrown =
assertThrows(LinkedResourceInPendingDeleteProhibitsOperationException.class, this::runFlow);
assertThat(thrown).hasMessageThat().contains("jd1234");
}
@Test
void testFailure_wrongTld() {
persistContactsAndHosts("net");
@@ -2072,14 +2040,6 @@ class DomainCreateFlowTest extends ResourceFlowTestCase<DomainCreateFlow, Domain
.build());
}
@Test
void testFailure_duplicateContact() {
setEppInput("domain_create_duplicate_contact.xml");
persistContactsAndHosts();
EppException thrown = assertThrows(DuplicateContactForRoleException.class, this::runFlow);
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
void testFailure_missingContactType() {
// We need to test for missing type, but not for invalid - the schema enforces that for us.
@@ -2090,149 +2050,21 @@ class DomainCreateFlowTest extends ResourceFlowTestCase<DomainCreateFlow, Domain
}
@Test
void testFailure_missingRegistrant() {
setEppInput("domain_create_missing_registrant.xml");
persistContactsAndHosts();
EppException thrown = assertThrows(MissingRegistrantException.class, this::runFlow);
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
void testSuccess_minimumDatasetPhase1_missingRegistrant() throws Exception {
persistResource(
new FeatureFlag.Builder()
.setFeatureName(MINIMUM_DATASET_CONTACTS_OPTIONAL)
.setStatusMap(
ImmutableSortedMap.of(START_OF_TIME, INACTIVE, clock.nowUtc().minusDays(5), ACTIVE))
.build());
setEppInput("domain_create_missing_registrant.xml");
persistContactsAndHosts();
runFlowAssertResponse(
loadFile("domain_create_response.xml", ImmutableMap.of("DOMAIN", "example.tld")));
}
@Test
void testFailure_minimumDatasetPhase2_noRegistrantButSomeOtherContactTypes() throws Exception {
persistResource(
new FeatureFlag.Builder()
.setFeatureName(MINIMUM_DATASET_CONTACTS_PROHIBITED)
.setStatusMap(
ImmutableSortedMap.of(START_OF_TIME, INACTIVE, clock.nowUtc().minusDays(5), ACTIVE))
.build());
setEppInput("domain_create_missing_registrant.xml");
void testFailure_minimumDataset_noRegistrantButSomeOtherContactTypes() throws Exception {
setEppInput("domain_create_other_contact_types.xml");
persistContactsAndHosts();
EppException thrown = assertThrows(ContactsProhibitedException.class, this::runFlow);
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
void testFailure_missingAdmin() {
setEppInput("domain_create_missing_admin.xml");
persistContactsAndHosts();
EppException thrown = assertThrows(MissingAdminContactException.class, this::runFlow);
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
void testSuccess_minimumDatasetPhase1_missingAdmin() throws Exception {
persistResource(
new FeatureFlag.Builder()
.setFeatureName(MINIMUM_DATASET_CONTACTS_OPTIONAL)
.setStatusMap(
ImmutableSortedMap.of(START_OF_TIME, INACTIVE, clock.nowUtc().minusDays(5), ACTIVE))
.build());
setEppInput("domain_create_missing_admin.xml");
persistContactsAndHosts();
runFlowAssertResponse(
loadFile("domain_create_response.xml", ImmutableMap.of("DOMAIN", "example.tld")));
}
@Test
void testFailure_minimumDatasetPhase2_registrantAndOtherContactsSent() throws Exception {
persistResource(
new FeatureFlag.Builder()
.setFeatureName(MINIMUM_DATASET_CONTACTS_PROHIBITED)
.setStatusMap(
ImmutableSortedMap.of(START_OF_TIME, INACTIVE, clock.nowUtc().minusDays(5), ACTIVE))
.build());
setEppInput("domain_create_missing_admin.xml");
void testFailure_minimumDataset_registrantNotPermitted() throws Exception {
setEppInput("domain_create_has_registrant_contact.xml");
persistContactsAndHosts();
EppException thrown = assertThrows(RegistrantProhibitedException.class, this::runFlow);
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
void testFailure_missingTech() {
setEppInput("domain_create_missing_tech.xml");
persistContactsAndHosts();
EppException thrown = assertThrows(MissingTechnicalContactException.class, this::runFlow);
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
void testSuccess_minimumDatasetPhase1_missingTech() throws Exception {
persistResource(
new FeatureFlag.Builder()
.setFeatureName(MINIMUM_DATASET_CONTACTS_OPTIONAL)
.setStatusMap(
ImmutableSortedMap.of(START_OF_TIME, INACTIVE, clock.nowUtc().minusDays(5), ACTIVE))
.build());
setEppInput("domain_create_missing_tech.xml");
persistContactsAndHosts();
runFlowAssertResponse(
loadFile("domain_create_response.xml", ImmutableMap.of("DOMAIN", "example.tld")));
}
@Test
void testFailure_missingNonRegistrantContacts() {
setEppInput("domain_create_missing_non_registrant_contacts.xml");
persistContactsAndHosts();
EppException thrown = assertThrows(MissingAdminContactException.class, this::runFlow);
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
void testSuccess_minimumDatasetPhase1_missingNonRegistrantContacts() throws Exception {
persistResource(
new FeatureFlag.Builder()
.setFeatureName(MINIMUM_DATASET_CONTACTS_OPTIONAL)
.setStatusMap(
ImmutableSortedMap.of(START_OF_TIME, INACTIVE, clock.nowUtc().minusDays(5), ACTIVE))
.build());
setEppInput("domain_create_missing_non_registrant_contacts.xml");
persistContactsAndHosts();
runFlowAssertResponse(
loadFile("domain_create_response.xml", ImmutableMap.of("DOMAIN", "example.tld")));
}
@Test
void testFailure_minimumDatasetPhase2_registrantNotPermitted() throws Exception {
persistResource(
new FeatureFlag.Builder()
.setFeatureName(MINIMUM_DATASET_CONTACTS_PROHIBITED)
.setStatusMap(
ImmutableSortedMap.of(START_OF_TIME, INACTIVE, clock.nowUtc().minusDays(5), ACTIVE))
.build());
setEppInput("domain_create_missing_non_registrant_contacts.xml");
persistContactsAndHosts();
EppException thrown = assertThrows(RegistrantProhibitedException.class, this::runFlow);
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
void testSuccess_minimumDatasetPhase2_noContactsWhatsoever() throws Exception {
persistResource(
new FeatureFlag.Builder()
.setFeatureName(MINIMUM_DATASET_CONTACTS_PROHIBITED)
.setStatusMap(
ImmutableSortedMap.of(START_OF_TIME, INACTIVE, clock.nowUtc().minusDays(5), ACTIVE))
.build());
setEppInput("domain_create_no_contacts.xml");
persistContactsAndHosts();
runFlowAssertResponse(
loadFile("domain_create_response.xml", ImmutableMap.of("DOMAIN", "example.tld")));
}
@Test
void testFailure_badIdn() {
createTld("xn--q9jyb4c");

View File

@@ -23,7 +23,6 @@ import static google.registry.model.eppcommon.EppXmlTransformer.marshal;
import static google.registry.model.tld.Tld.TldState.QUIET_PERIOD;
import static google.registry.testing.DatabaseHelper.assertNoBillingEvents;
import static google.registry.testing.DatabaseHelper.createTld;
import static google.registry.testing.DatabaseHelper.persistActiveContact;
import static google.registry.testing.DatabaseHelper.persistActiveHost;
import static google.registry.testing.DatabaseHelper.persistBillingRecurrenceForDomain;
import static google.registry.testing.DatabaseHelper.persistPremiumList;
@@ -55,10 +54,6 @@ import google.registry.model.billing.BillingBase.Flag;
import google.registry.model.billing.BillingBase.Reason;
import google.registry.model.billing.BillingBase.RenewalPriceBehavior;
import google.registry.model.billing.BillingRecurrence;
import google.registry.model.contact.Contact;
import google.registry.model.contact.ContactAuthInfo;
import google.registry.model.domain.DesignatedContact;
import google.registry.model.domain.DesignatedContact.Type;
import google.registry.model.domain.Domain;
import google.registry.model.domain.DomainAuthInfo;
import google.registry.model.domain.DomainHistory;
@@ -103,8 +98,6 @@ class DomainInfoFlowTest extends ResourceFlowTestCase<DomainInfoFlow, Domain> {
private static final Pattern OK_PATTERN = Pattern.compile("\"ok\"");
private Contact registrant;
private Contact contact;
private Host host1;
private Host host2;
private Host host3;
@@ -124,8 +117,6 @@ class DomainInfoFlowTest extends ResourceFlowTestCase<DomainInfoFlow, Domain> {
}
private void persistTestEntities(String domainName, boolean inactive) {
registrant = persistActiveContact("jd1234");
contact = persistActiveContact("sh8013");
host1 = persistActiveHost("ns1.example.tld");
host2 = persistActiveHost("ns1.example.net");
domain =
@@ -140,11 +131,6 @@ class DomainInfoFlowTest extends ResourceFlowTestCase<DomainInfoFlow, Domain> {
.setLastEppUpdateTime(DateTime.parse("1999-12-03T09:00:00.0Z"))
.setLastTransferTime(DateTime.parse("2000-04-08T09:00:00.0Z"))
.setRegistrationExpirationTime(DateTime.parse("2005-04-03T22:00:00.0Z"))
.setRegistrant(Optional.of(registrant.createVKey()))
.setContacts(
ImmutableSet.of(
DesignatedContact.create(Type.ADMIN, contact.createVKey()),
DesignatedContact.create(Type.TECH, contact.createVKey())))
.setNameservers(
inactive ? null : ImmutableSet.of(host1.createVKey(), host2.createVKey()))
.setAuthInfo(DomainAuthInfo.create(PasswordAuth.create("2fooBAR")))
@@ -323,24 +309,6 @@ class DomainInfoFlowTest extends ResourceFlowTestCase<DomainInfoFlow, Domain> {
doSuccessfulTest("domain_info_response.xml");
}
@Test
void testSuccess_differentRegistrarWithRegistrantAuthInfo() throws Exception {
persistTestEntities(false);
setEppInput("domain_info_with_contact_auth.xml");
eppLoader.replaceAll("JD1234-REP", registrant.getRepoId());
sessionMetadata.setRegistrarId("ClientZ");
doSuccessfulTest("domain_info_response.xml", false);
}
@Test
void testSuccess_differentRegistrarWithContactAuthInfo() throws Exception {
persistTestEntities(false);
setEppInput("domain_info_with_contact_auth.xml");
eppLoader.replaceAll("JD1234-REP", registrant.getRepoId());
sessionMetadata.setRegistrarId("ClientZ");
doSuccessfulTest("domain_info_response.xml", false);
}
@Test
void testSuccess_inQuietPeriod() throws Exception {
persistResource(
@@ -618,99 +586,6 @@ class DomainInfoFlowTest extends ResourceFlowTestCase<DomainInfoFlow, Domain> {
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
void testFailure_differentRegistrarWrongRegistrantAuthInfo() {
persistTestEntities(false);
// Change the password of the registrant so that it does not match the file.
registrant =
persistResource(
registrant
.asBuilder()
.setAuthInfo(ContactAuthInfo.create(PasswordAuth.create("diffpw")))
.build());
sessionMetadata.setRegistrarId("ClientZ");
setEppInput("domain_info_with_contact_auth.xml");
// Replace the ROID in the xml file with the one for our registrant.
eppLoader.replaceAll("JD1234-REP", registrant.getRepoId());
EppException thrown = assertThrows(BadAuthInfoForResourceException.class, this::runFlow);
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
void testFailure_wrongRegistrantAuthInfo() {
persistTestEntities(false);
// Change the password of the registrant so that it does not match the file.
registrant =
persistResource(
registrant
.asBuilder()
.setAuthInfo(ContactAuthInfo.create(PasswordAuth.create("diffpw")))
.build());
setEppInput("domain_info_with_contact_auth.xml");
// Replace the ROID in the xml file with the one for our registrant.
eppLoader.replaceAll("JD1234-REP", registrant.getRepoId());
EppException thrown = assertThrows(BadAuthInfoForResourceException.class, this::runFlow);
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
void testFailure_differentRegistrarWrongContactAuthInfo() {
persistTestEntities(false);
// Change the password of the contact so that it does not match the file.
contact =
persistResource(
contact
.asBuilder()
.setAuthInfo(ContactAuthInfo.create(PasswordAuth.create("diffpw")))
.build());
sessionMetadata.setRegistrarId("ClientZ");
setEppInput("domain_info_with_contact_auth.xml");
// Replace the ROID in the xml file with the one for our contact.
eppLoader.replaceAll("JD1234-REP", contact.getRepoId());
EppException thrown = assertThrows(BadAuthInfoForResourceException.class, this::runFlow);
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
void testFailure_wrongContactAuthInfo() {
persistTestEntities(false);
// Change the password of the contact so that it does not match the file.
contact =
persistResource(
contact
.asBuilder()
.setAuthInfo(ContactAuthInfo.create(PasswordAuth.create("diffpw")))
.build());
setEppInput("domain_info_with_contact_auth.xml");
// Replace the ROID in the xml file with the one for our contact.
eppLoader.replaceAll("JD1234-REP", contact.getRepoId());
EppException thrown = assertThrows(BadAuthInfoForResourceException.class, this::runFlow);
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
void testFailure_differentRegistrarUnrelatedContactAuthInfo() {
persistTestEntities(false);
Contact unrelatedContact = persistActiveContact("foo1234");
sessionMetadata.setRegistrarId("ClientZ");
setEppInput("domain_info_with_contact_auth.xml");
// Replace the ROID in the xml file with the one for our unrelated contact.
eppLoader.replaceAll("JD1234-REP", unrelatedContact.getRepoId());
EppException thrown = assertThrows(BadAuthInfoForResourceException.class, this::runFlow);
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
void testFailure_unrelatedContactAuthInfo() {
persistTestEntities(false);
Contact unrelatedContact = persistActiveContact("foo1234");
setEppInput("domain_info_with_contact_auth.xml");
// Replace the ROID in the xml file with the one for our unrelated contact.
eppLoader.replaceAll("JD1234-REP", unrelatedContact.getRepoId());
EppException thrown = assertThrows(BadAuthInfoForResourceException.class, this::runFlow);
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
/**
* Test create command. Fee extension version 6 is the only one which supports fee extensions on
* info commands and responses, so we don't need to test the other versions.
@@ -719,10 +594,7 @@ class DomainInfoFlowTest extends ResourceFlowTestCase<DomainInfoFlow, Domain> {
void testFeeExtension_createCommand() throws Exception {
setEppInput(
"domain_info_fee.xml",
updateSubstitutions(
SUBSTITUTION_BASE,
"COMMAND", "create",
"PERIOD", "2"));
updateSubstitutions(SUBSTITUTION_BASE, "COMMAND", "create", "PERIOD", "2"));
persistTestEntities(false);
setUpBillingEventForExistingDomain();
doSuccessfulTest(
@@ -741,10 +613,7 @@ class DomainInfoFlowTest extends ResourceFlowTestCase<DomainInfoFlow, Domain> {
void testFeeExtension_renewCommand() throws Exception {
setEppInput(
"domain_info_fee.xml",
updateSubstitutions(
SUBSTITUTION_BASE,
"COMMAND", "renew",
"PERIOD", "2"));
updateSubstitutions(SUBSTITUTION_BASE, "COMMAND", "renew", "PERIOD", "2"));
persistTestEntities(false);
setUpBillingEventForExistingDomain();
doSuccessfulTest(
@@ -763,10 +632,7 @@ class DomainInfoFlowTest extends ResourceFlowTestCase<DomainInfoFlow, Domain> {
void testFeeExtension_transferCommand() throws Exception {
setEppInput(
"domain_info_fee.xml",
updateSubstitutions(
SUBSTITUTION_BASE,
"COMMAND", "transfer",
"PERIOD", "1"));
updateSubstitutions(SUBSTITUTION_BASE, "COMMAND", "transfer", "PERIOD", "1"));
persistTestEntities(false);
setUpBillingEventForExistingDomain();
doSuccessfulTest(
@@ -785,10 +651,7 @@ class DomainInfoFlowTest extends ResourceFlowTestCase<DomainInfoFlow, Domain> {
void testFeeExtension_restoreCommand() throws Exception {
setEppInput(
"domain_info_fee.xml",
updateSubstitutions(
SUBSTITUTION_BASE,
"COMMAND", "restore",
"PERIOD", "1"));
updateSubstitutions(SUBSTITUTION_BASE, "COMMAND", "restore", "PERIOD", "1"));
persistTestEntities(false);
setUpBillingEventForExistingDomain();
doSuccessfulTest("domain_info_fee_restore_response.xml", false, ImmutableMap.of(), true);
@@ -838,10 +701,7 @@ class DomainInfoFlowTest extends ResourceFlowTestCase<DomainInfoFlow, Domain> {
setEppInput(
"domain_info_fee.xml",
updateSubstitutions(
SUBSTITUTION_BASE,
"NAME", "rich.example",
"COMMAND", "create",
"PERIOD", "1"));
SUBSTITUTION_BASE, "NAME", "rich.example", "COMMAND", "create", "PERIOD", "1"));
persistTestEntities("rich.example", false);
setUpBillingEventForExistingDomain();
doSuccessfulTest(
@@ -858,10 +718,7 @@ class DomainInfoFlowTest extends ResourceFlowTestCase<DomainInfoFlow, Domain> {
setEppInput(
"domain_info_fee.xml",
updateSubstitutions(
SUBSTITUTION_BASE,
"NAME", "rich.example",
"COMMAND", "renew",
"PERIOD", "1"));
SUBSTITUTION_BASE, "NAME", "rich.example", "COMMAND", "renew", "PERIOD", "1"));
persistTestEntities("rich.example", false);
setUpBillingEventForExistingDomain();
doSuccessfulTest(
@@ -973,10 +830,7 @@ class DomainInfoFlowTest extends ResourceFlowTestCase<DomainInfoFlow, Domain> {
setEppInput(
"domain_info_fee.xml",
updateSubstitutions(
SUBSTITUTION_BASE,
"NAME", "rich.example",
"COMMAND", "transfer",
"PERIOD", "1"));
SUBSTITUTION_BASE, "NAME", "rich.example", "COMMAND", "transfer", "PERIOD", "1"));
persistTestEntities("rich.example", false);
setUpBillingEventForExistingDomain();
doSuccessfulTest(
@@ -993,10 +847,7 @@ class DomainInfoFlowTest extends ResourceFlowTestCase<DomainInfoFlow, Domain> {
setEppInput(
"domain_info_fee.xml",
updateSubstitutions(
SUBSTITUTION_BASE,
"NAME", "rich.example",
"COMMAND", "restore",
"PERIOD", "1"));
SUBSTITUTION_BASE, "NAME", "rich.example", "COMMAND", "restore", "PERIOD", "1"));
persistTestEntities("rich.example", false);
setUpBillingEventForExistingDomain();
doSuccessfulTest(
@@ -1009,10 +860,7 @@ class DomainInfoFlowTest extends ResourceFlowTestCase<DomainInfoFlow, Domain> {
setEppInput(
"domain_info_fee.xml",
updateSubstitutions(
SUBSTITUTION_BASE,
"COMMAND", "create",
"CURRENCY", "EUR",
"PERIOD", "1"));
SUBSTITUTION_BASE, "COMMAND", "create", "CURRENCY", "EUR", "PERIOD", "1"));
persistTestEntities(false);
setUpBillingEventForExistingDomain();
EppException thrown = assertThrows(CurrencyUnitMismatchException.class, this::runFlow);
@@ -1024,10 +872,7 @@ class DomainInfoFlowTest extends ResourceFlowTestCase<DomainInfoFlow, Domain> {
setEppInput(
"domain_info_fee.xml",
updateSubstitutions(
SUBSTITUTION_BASE,
"COMMAND", "create",
"CURRENCY", "BAD",
"PERIOD", "1"));
SUBSTITUTION_BASE, "COMMAND", "create", "CURRENCY", "BAD", "PERIOD", "1"));
EppException thrown = assertThrows(UnknownCurrencyEppException.class, this::runFlow);
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@@ -1037,11 +882,7 @@ class DomainInfoFlowTest extends ResourceFlowTestCase<DomainInfoFlow, Domain> {
void testFeeExtension_periodNotInYears() {
setEppInput(
"domain_info_fee.xml",
updateSubstitutions(
SUBSTITUTION_BASE,
"COMMAND", "create",
"PERIOD", "2",
"UNIT", "m"));
updateSubstitutions(SUBSTITUTION_BASE, "COMMAND", "create", "PERIOD", "2", "UNIT", "m"));
persistTestEntities(false);
setUpBillingEventForExistingDomain();
EppException thrown = assertThrows(BadPeriodUnitException.class, this::runFlow);
@@ -1073,10 +914,7 @@ class DomainInfoFlowTest extends ResourceFlowTestCase<DomainInfoFlow, Domain> {
void testFeeExtension_multiyearRestore() {
setEppInput(
"domain_info_fee.xml",
updateSubstitutions(
SUBSTITUTION_BASE,
"COMMAND", "restore",
"PERIOD", "2"));
updateSubstitutions(SUBSTITUTION_BASE, "COMMAND", "restore", "PERIOD", "2"));
persistTestEntities(false);
setUpBillingEventForExistingDomain();
EppException thrown = assertThrows(RestoresAreAlwaysForOneYearException.class, this::runFlow);
@@ -1088,10 +926,7 @@ class DomainInfoFlowTest extends ResourceFlowTestCase<DomainInfoFlow, Domain> {
void testFeeExtension_multiyearTransfer() {
setEppInput(
"domain_info_fee.xml",
updateSubstitutions(
SUBSTITUTION_BASE,
"COMMAND", "transfer",
"PERIOD", "2"));
updateSubstitutions(SUBSTITUTION_BASE, "COMMAND", "transfer", "PERIOD", "2"));
persistTestEntities(false);
setUpBillingEventForExistingDomain();
EppException thrown = assertThrows(TransfersAreAlwaysForOneYearException.class, this::runFlow);

View File

@@ -1004,6 +1004,40 @@ class DomainTransferRequestFlowTest
ImmutableMap.of("PERIOD", "0", "AUTOMATIC_TRANSFER_LENGTH", "5")));
}
@Test
void testSuccess_superuserExtension_clientTransferProhibited() throws Exception {
setupDomain("example", "tld");
eppRequestSource = EppRequestSource.TOOL;
domain =
persistResource(
domain.asBuilder().addStatusValue(StatusValue.CLIENT_TRANSFER_PROHIBITED).build());
doSuccessfulSuperuserExtensionTest(
"domain_transfer_request_superuser_extension.xml",
"domain_transfer_request_response_su_ext_zero_period_zero_transfer_length.xml",
domain.getRegistrationExpirationTime().plusYears(0),
ImmutableMap.of("PERIOD", "0", "AUTOMATIC_TRANSFER_LENGTH", "0"),
Optional.empty(),
Period.create(0, Unit.YEARS),
Duration.ZERO);
}
@Test
void testSuccess_superuserExtension_serverTransferProhibited() throws Exception {
setupDomain("example", "tld");
eppRequestSource = EppRequestSource.TOOL;
domain =
persistResource(
domain.asBuilder().addStatusValue(StatusValue.SERVER_TRANSFER_PROHIBITED).build());
doSuccessfulSuperuserExtensionTest(
"domain_transfer_request_superuser_extension.xml",
"domain_transfer_request_response_su_ext_zero_period_zero_transfer_length.xml",
domain.getRegistrationExpirationTime().plusYears(0),
ImmutableMap.of("PERIOD", "0", "AUTOMATIC_TRANSFER_LENGTH", "0"),
Optional.empty(),
Period.create(0, Unit.YEARS),
Duration.ZERO);
}
@Test
void testSuccess_cappedExpiration() throws Exception {
setupDomain("example", "tld");
@@ -1809,6 +1843,22 @@ class DomainTransferRequestFlowTest
assertThat(thrown).hasMessageThat().contains("pendingDelete");
}
@Test
void testFailure_pendingDelete_evenWhenSuperuser() {
setupDomain("example", "tld");
eppRequestSource = EppRequestSource.TOOL;
domain = persistResource(domain.asBuilder().addStatusValue(StatusValue.PENDING_DELETE).build());
ResourceStatusProhibitsOperationException thrown =
assertThrows(
ResourceStatusProhibitsOperationException.class,
() ->
runTest(
"domain_transfer_request_superuser_extension.xml",
UserPrivileges.SUPERUSER,
ImmutableMap.of("PERIOD", "0", "AUTOMATIC_TRANSFER_LENGTH", "0")));
assertThat(thrown).hasMessageThat().contains("pendingDelete");
}
@Test
void testIcannActivityReportField_getsLogged() throws Exception {
setupDomain("example", "tld");

View File

@@ -19,10 +19,6 @@ import static com.google.common.collect.Sets.union;
import static com.google.common.io.BaseEncoding.base16;
import static com.google.common.truth.Truth.assertThat;
import static google.registry.model.ForeignKeyUtils.loadResource;
import static google.registry.model.common.FeatureFlag.FeatureName.MINIMUM_DATASET_CONTACTS_OPTIONAL;
import static google.registry.model.common.FeatureFlag.FeatureName.MINIMUM_DATASET_CONTACTS_PROHIBITED;
import static google.registry.model.common.FeatureFlag.FeatureStatus.ACTIVE;
import static google.registry.model.common.FeatureFlag.FeatureStatus.INACTIVE;
import static google.registry.model.eppcommon.StatusValue.CLIENT_DELETE_PROHIBITED;
import static google.registry.model.eppcommon.StatusValue.CLIENT_HOLD;
import static google.registry.model.eppcommon.StatusValue.CLIENT_RENEW_PROHIBITED;
@@ -45,7 +41,6 @@ import static google.registry.testing.DatabaseHelper.assertPollMessagesForResour
import static google.registry.testing.DatabaseHelper.createTld;
import static google.registry.testing.DatabaseHelper.getOnlyHistoryEntryOfType;
import static google.registry.testing.DatabaseHelper.getPollMessages;
import static google.registry.testing.DatabaseHelper.loadByKey;
import static google.registry.testing.DatabaseHelper.loadRegistrar;
import static google.registry.testing.DatabaseHelper.persistActiveContact;
import static google.registry.testing.DatabaseHelper.persistActiveDomain;
@@ -68,13 +63,14 @@ import google.registry.config.RegistryConfig;
import google.registry.flows.EppException;
import google.registry.flows.EppException.UnimplementedExtensionException;
import google.registry.flows.EppRequestSource;
import google.registry.flows.FlowTestCase.CommitMode;
import google.registry.flows.FlowTestCase.UserPrivileges;
import google.registry.flows.FlowUtils.NotLoggedInException;
import google.registry.flows.ResourceFlowTestCase;
import google.registry.flows.ResourceFlowUtils.AddRemoveSameValueException;
import google.registry.flows.ResourceFlowUtils.ResourceDoesNotExistException;
import google.registry.flows.ResourceFlowUtils.ResourceNotOwnedException;
import google.registry.flows.ResourceFlowUtils.StatusNotClientSettableException;
import google.registry.flows.domain.DomainFlowUtils.DuplicateContactForRoleException;
import google.registry.flows.domain.DomainFlowUtils.EmptySecDnsUpdateException;
import google.registry.flows.domain.DomainFlowUtils.FeesMismatchException;
import google.registry.flows.domain.DomainFlowUtils.FeesRequiredForNonFreeOperationException;
@@ -82,10 +78,6 @@ import google.registry.flows.domain.DomainFlowUtils.InvalidDsRecordException;
import google.registry.flows.domain.DomainFlowUtils.LinkedResourceInPendingDeleteProhibitsOperationException;
import google.registry.flows.domain.DomainFlowUtils.LinkedResourcesDoNotExistException;
import google.registry.flows.domain.DomainFlowUtils.MaxSigLifeChangeNotSupportedException;
import google.registry.flows.domain.DomainFlowUtils.MissingAdminContactException;
import google.registry.flows.domain.DomainFlowUtils.MissingContactTypeException;
import google.registry.flows.domain.DomainFlowUtils.MissingRegistrantException;
import google.registry.flows.domain.DomainFlowUtils.MissingTechnicalContactException;
import google.registry.flows.domain.DomainFlowUtils.NameserversNotAllowedForTldException;
import google.registry.flows.domain.DomainFlowUtils.NameserversNotSpecifiedForTldWithNameserverAllowListException;
import google.registry.flows.domain.DomainFlowUtils.NotAuthorizedForTldException;
@@ -101,7 +93,6 @@ import google.registry.flows.exceptions.ResourceStatusProhibitsOperationExceptio
import google.registry.model.ImmutableObject;
import google.registry.model.billing.BillingBase.Reason;
import google.registry.model.billing.BillingEvent;
import google.registry.model.common.FeatureFlag;
import google.registry.model.contact.Contact;
import google.registry.model.domain.DesignatedContact;
import google.registry.model.domain.DesignatedContact.Type;
@@ -320,38 +311,7 @@ class DomainUpdateFlowTest extends ResourceFlowTestCase<DomainUpdateFlow, Domain
}
@Test
void testFailure_emptyRegistrant() throws Exception {
setEppInput("domain_update_empty_registrant.xml");
persistReferencedEntities();
persistDomain();
MissingRegistrantException thrown =
assertThrows(MissingRegistrantException.class, this::runFlow);
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
void testSuccess_minimumDatasetPhase1_emptyRegistrant() throws Exception {
persistResource(
new FeatureFlag.Builder()
.setFeatureName(MINIMUM_DATASET_CONTACTS_OPTIONAL)
.setStatusMap(
ImmutableSortedMap.of(START_OF_TIME, INACTIVE, clock.nowUtc().minusDays(5), ACTIVE))
.build());
setEppInput("domain_update_empty_registrant.xml");
persistReferencedEntities();
persistDomain();
runFlowAssertResponse(loadFile("generic_success_response.xml"));
assertThat(reloadResourceByForeignKey().getRegistrant()).isEmpty();
}
@Test
void testFailure_minimumDatasetPhase2_whenAddingNewContacts() throws Exception {
persistResource(
new FeatureFlag.Builder()
.setFeatureName(MINIMUM_DATASET_CONTACTS_PROHIBITED)
.setStatusMap(
ImmutableSortedMap.of(START_OF_TIME, INACTIVE, clock.nowUtc().minusDays(5), ACTIVE))
.build());
void testFailure_minimumDataset_whenAddingNewContacts() throws Exception {
// This EPP adds a new technical contact mak21 that wasn't already present.
setEppInput("domain_update_empty_registrant.xml");
persistReferencedEntities();
@@ -386,7 +346,7 @@ class DomainUpdateFlowTest extends ResourceFlowTestCase<DomainUpdateFlow, Domain
}
@Test
void testSuccess_addAndRemoveLargeNumberOfNameserversAndContacts() throws Exception {
void testSuccess_addAndRemoveLargeNumberOfNameservers() throws Exception {
persistReferencedEntities();
persistDomain();
setEppInput("domain_update_max_everything.xml");
@@ -398,20 +358,10 @@ class DomainUpdateFlowTest extends ResourceFlowTestCase<DomainUpdateFlow, Domain
nameservers.add(host.createVKey());
}
}
ImmutableList.Builder<DesignatedContact> contactsBuilder = new ImmutableList.Builder<>();
for (int i = 0; i < 8; i++) {
contactsBuilder.add(
DesignatedContact.create(
DesignatedContact.Type.values()[i % 4],
persistActiveContact(String.format("max_test_%d", i)).createVKey()));
}
ImmutableList<DesignatedContact> contacts = contactsBuilder.build();
persistResource(
reloadResourceByForeignKey()
.asBuilder()
.setNameservers(nameservers.build())
.setContacts(ImmutableSet.copyOf(contacts.subList(0, 3)))
.setRegistrant(Optional.of(contacts.get(3).getContactKey()))
.build());
clock.advanceOneMilli();
assertMutatingFlow(true);
@@ -419,9 +369,6 @@ class DomainUpdateFlowTest extends ResourceFlowTestCase<DomainUpdateFlow, Domain
Domain domain = reloadResourceByForeignKey();
assertAboutDomains().that(domain).hasOneHistoryEntryEachOfTypes(DOMAIN_CREATE, DOMAIN_UPDATE);
assertThat(domain.getNameservers()).hasSize(13);
// getContacts does not return contacts of type REGISTRANT, so check these separately.
assertThat(domain.getContacts()).hasSize(3);
assertThat(loadByKey(domain.getRegistrant().get()).getContactId()).isEqualTo("max_test_7");
assertNoBillingEvents();
assertDomainDnsRequests("example.tld");
}
@@ -503,26 +450,6 @@ class DomainUpdateFlowTest extends ResourceFlowTestCase<DomainUpdateFlow, Domain
runFlowAssertResponse(loadFile("generic_success_response.xml"));
}
@Test
void testSuccess_multipleReferencesToSameContactRemoved() throws Exception {
setEppInput("domain_update_remove_multiple_contacts.xml");
persistReferencedEntities();
Contact sh8013 = loadResource(Contact.class, "sh8013", clock.nowUtc()).get();
VKey<Contact> sh8013Key = sh8013.createVKey();
persistResource(
DatabaseHelper.newDomain(getUniqueIdFromCommand())
.asBuilder()
.setRegistrant(Optional.of(sh8013Key))
.setContacts(
ImmutableSet.of(
DesignatedContact.create(Type.ADMIN, sh8013Key),
DesignatedContact.create(Type.BILLING, sh8013Key),
DesignatedContact.create(Type.TECH, sh8013Key)))
.build());
clock.advanceOneMilli();
runFlowAssertResponse(loadFile("generic_success_response.xml"));
}
@Test
void testSuccess_removeClientUpdateProhibited() throws Exception {
persistReferencedEntities();
@@ -1180,40 +1107,6 @@ class DomainUpdateFlowTest extends ResourceFlowTestCase<DomainUpdateFlow, Domain
assertThat(thrown).hasMessageThat().contains("(ns2.example.foo)");
}
@Test
void testFailure_missingContact() throws Exception {
persistActiveHost("ns1.example.foo");
persistActiveHost("ns2.example.foo");
persistActiveContact("mak21");
persistActiveDomain(getUniqueIdFromCommand());
LinkedResourcesDoNotExistException thrown =
assertThrows(LinkedResourcesDoNotExistException.class, this::runFlow);
assertThat(thrown).hasMessageThat().contains("(sh8013)");
}
@Test
void testFailure_addingDuplicateContact() throws Exception {
persistReferencedEntities();
persistActiveContact("foo");
persistDomain();
// Add a tech contact to the persisted entity, which should cause the flow to fail when it tries
// to add "mak21" as a second tech contact.
persistResource(
reloadResourceByForeignKey()
.asBuilder()
.setContacts(
DesignatedContact.create(
Type.TECH,
loadResource(Contact.class, "foo", clock.nowUtc()).get().createVKey()))
.build());
EppException thrown = assertThrows(DuplicateContactForRoleException.class, this::runFlow);
assertAboutEppExceptions().that(thrown).marshalsToXml();
assertThat(thrown.getResult().getMsg())
.isEqualTo(
"More than one contact for a given role is not allowed: "
+ "role [tech] has contacts [foo, mak21]");
}
@Test
void testFailure_statusValueNotClientSettable() throws Exception {
setEppInput("domain_update_prohibited_status.xml");
@@ -1425,40 +1318,6 @@ class DomainUpdateFlowTest extends ResourceFlowTestCase<DomainUpdateFlow, Domain
assertThat(thrown).hasMessageThat().contains("pendingDelete");
}
@Test
void testFailure_duplicateContactInCommand() throws Exception {
setEppInput("domain_update_duplicate_contact.xml");
persistReferencedEntities();
persistDomain();
EppException thrown = assertThrows(DuplicateContactForRoleException.class, this::runFlow);
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
void testFailure_multipleDuplicateContactInCommand() throws Exception {
setEppInput("domain_update_multiple_duplicate_contacts.xml");
persistReferencedEntities();
persistDomain();
EppException thrown = assertThrows(DuplicateContactForRoleException.class, this::runFlow);
assertThat(thrown)
.hasMessageThat()
.isEqualTo(
"More than one contact for a given role is not allowed: "
+ "role [billing] has contacts [mak21, sh8013], "
+ "role [tech] has contacts [mak21, sh8013]");
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
void testFailure_missingContactType() throws Exception {
// We need to test for missing type, but not for invalid - the schema enforces that for us.
setEppInput("domain_update_missing_contact_type.xml");
persistReferencedEntities();
persistDomain();
EppException thrown = assertThrows(MissingContactTypeException.class, this::runFlow);
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
void testFailure_unauthorizedClient() throws Exception {
sessionMetadata.setRegistrarId("NewRegistrar");
@@ -1514,68 +1373,8 @@ class DomainUpdateFlowTest extends ResourceFlowTestCase<DomainUpdateFlow, Domain
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
// Contacts mismatch.
@Test
void testFailure_sameContactAddedAndRemoved() throws Exception {
setEppInput("domain_update_add_remove_same_contact.xml");
persistReferencedEntities();
persistResource(
DatabaseHelper.newDomain(getUniqueIdFromCommand())
.asBuilder()
.setContacts(
DesignatedContact.create(
Type.TECH,
loadResource(Contact.class, "sh8013", clock.nowUtc()).get().createVKey()))
.build());
EppException thrown = assertThrows(AddRemoveSameValueException.class, this::runFlow);
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
void testFailure_removeAdmin() throws Exception {
setEppInput("domain_update_remove_admin.xml");
persistReferencedEntities();
persistResource(
DatabaseHelper.newDomain(getUniqueIdFromCommand())
.asBuilder()
.setContacts(
ImmutableSet.of(
DesignatedContact.create(Type.ADMIN, sh8013Contact.createVKey()),
DesignatedContact.create(Type.TECH, sh8013Contact.createVKey())))
.build());
EppException thrown = assertThrows(MissingAdminContactException.class, this::runFlow);
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
void testSuccess_minimumDatasetPhase1_removeAdmin() throws Exception {
persistResource(
new FeatureFlag.Builder()
.setFeatureName(MINIMUM_DATASET_CONTACTS_OPTIONAL)
.setStatusMap(
ImmutableSortedMap.of(START_OF_TIME, INACTIVE, clock.nowUtc().minusDays(5), ACTIVE))
.build());
setEppInput("domain_update_remove_admin.xml");
persistReferencedEntities();
persistResource(
DatabaseHelper.newDomain(getUniqueIdFromCommand())
.asBuilder()
.setContacts(
ImmutableSet.of(
DesignatedContact.create(Type.ADMIN, sh8013Contact.createVKey()),
DesignatedContact.create(Type.TECH, sh8013Contact.createVKey())))
.build());
runFlowAssertResponse(loadFile("generic_success_response.xml"));
}
@Test
void testFailure_minimumDatasetPhase2_addingNewRegistrantFails() throws Exception {
persistResource(
new FeatureFlag.Builder()
.setFeatureName(MINIMUM_DATASET_CONTACTS_PROHIBITED)
.setStatusMap(
ImmutableSortedMap.of(START_OF_TIME, INACTIVE, clock.nowUtc().minusDays(5), ACTIVE))
.build());
void testFailure_minimumDataset_addingNewRegistrantFails() throws Exception {
persistReferencedEntities();
persistResource(
DatabaseHelper.newDomain(getUniqueIdFromCommand())
@@ -1593,109 +1392,6 @@ class DomainUpdateFlowTest extends ResourceFlowTestCase<DomainUpdateFlow, Domain
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
void testFailure_removeTech() throws Exception {
setEppInput("domain_update_remove_tech.xml");
persistReferencedEntities();
persistResource(
DatabaseHelper.newDomain(getUniqueIdFromCommand())
.asBuilder()
.setContacts(
ImmutableSet.of(
DesignatedContact.create(Type.ADMIN, sh8013Contact.createVKey()),
DesignatedContact.create(Type.TECH, sh8013Contact.createVKey())))
.build());
EppException thrown = assertThrows(MissingTechnicalContactException.class, this::runFlow);
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
void testSuccess_minimumDatasetPhase1_removeTech() throws Exception {
persistResource(
new FeatureFlag.Builder()
.setFeatureName(MINIMUM_DATASET_CONTACTS_OPTIONAL)
.setStatusMap(
ImmutableSortedMap.of(START_OF_TIME, INACTIVE, clock.nowUtc().minusDays(5), ACTIVE))
.build());
setEppInput("domain_update_remove_tech.xml");
persistReferencedEntities();
persistResource(
DatabaseHelper.newDomain(getUniqueIdFromCommand())
.asBuilder()
.setContacts(
ImmutableSet.of(
DesignatedContact.create(Type.ADMIN, sh8013Contact.createVKey()),
DesignatedContact.create(Type.TECH, sh8013Contact.createVKey())))
.build());
runFlowAssertResponse(loadFile("generic_success_response.xml"));
}
@Test
void testSuccess_minimumDatasetPhase2_removeAllContacts() throws Exception {
persistResource(
new FeatureFlag.Builder()
.setFeatureName(MINIMUM_DATASET_CONTACTS_PROHIBITED)
.setStatusMap(
ImmutableSortedMap.of(START_OF_TIME, INACTIVE, clock.nowUtc().minusDays(5), ACTIVE))
.build());
setEppInput("domain_update_remove_all_contacts.xml");
persistReferencedEntities();
persistResource(
DatabaseHelper.newDomain(getUniqueIdFromCommand())
.asBuilder()
.setContacts(
ImmutableSet.of(
DesignatedContact.create(Type.ADMIN, sh8013Contact.createVKey()),
DesignatedContact.create(Type.TECH, sh8013Contact.createVKey())))
.build());
runFlowAssertResponse(loadFile("generic_success_response.xml"));
Domain updatedDomain = reloadResourceByForeignKey();
assertThat(updatedDomain.getRegistrant()).isEmpty();
assertThat(updatedDomain.getContacts()).isEmpty();
}
@Test
void testSuccess_minimumDatasetPhase2_removeOneContact() throws Exception {
persistResource(
new FeatureFlag.Builder()
.setFeatureName(MINIMUM_DATASET_CONTACTS_PROHIBITED)
.setStatusMap(
ImmutableSortedMap.of(START_OF_TIME, INACTIVE, clock.nowUtc().minusDays(5), ACTIVE))
.build());
setEppInput("domain_update_remove_admin.xml");
persistReferencedEntities();
persistResource(
DatabaseHelper.newDomain(getUniqueIdFromCommand())
.asBuilder()
.setContacts(
ImmutableSet.of(
DesignatedContact.create(Type.ADMIN, sh8013Contact.createVKey()),
DesignatedContact.create(Type.TECH, sh8013Contact.createVKey())))
.build());
assertThat(reloadResourceByForeignKey().getRegistrant()).isPresent();
assertThat(reloadResourceByForeignKey().getContacts()).hasSize(2);
runFlowAssertResponse(loadFile("generic_success_response.xml"));
Domain updatedDomain = reloadResourceByForeignKey();
assertThat(updatedDomain.getRegistrant()).isPresent();
assertThat(updatedDomain.getContacts()).hasSize(1);
}
@Test
void testFailure_addPendingDeleteContact() throws Exception {
persistReferencedEntities();
persistDomain();
persistResource(
loadResource(Contact.class, "mak21", clock.nowUtc())
.get()
.asBuilder()
.addStatusValue(PENDING_DELETE)
.build());
clock.advanceOneMilli();
LinkedResourceInPendingDeleteProhibitsOperationException thrown =
assertThrows(LinkedResourceInPendingDeleteProhibitsOperationException.class, this::runFlow);
assertThat(thrown).hasMessageThat().contains("mak21");
}
@Test
void testFailure_addPendingDeleteHost() throws Exception {
persistReferencedEntities();
@@ -1727,31 +1423,6 @@ class DomainUpdateFlowTest extends ResourceFlowTestCase<DomainUpdateFlow, Domain
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
void testSuccess_changeContactsAndRegistrant() throws Exception {
setEppInput("domain_update_contacts_and_registrant.xml");
persistReferencedEntities();
persistDomainWithRegistrant();
reloadResourceByForeignKey()
.getContacts()
.forEach(
contact ->
assertThat(loadByKey(contact.getContactKey()).getContactId()).isEqualTo("mak21"));
assertThat(loadByKey(reloadResourceByForeignKey().getRegistrant().get()).getContactId())
.isEqualTo("mak21");
runFlow();
reloadResourceByForeignKey()
.getContacts()
.forEach(
contact ->
assertThat(loadByKey(contact.getContactKey()).getContactId()).isEqualTo("sh8013"));
assertThat(loadByKey(reloadResourceByForeignKey().getRegistrant().get()).getContactId())
.isEqualTo("sh8013");
}
@Test
void testSuccess_tldWithNameserverAllowList_removeNameserver() throws Exception {
setEppInput("domain_update_remove_nameserver.xml");

View File

@@ -751,7 +751,9 @@ public final class TldTest extends EntityTestCase {
assertThrows(
IllegalArgumentException.class,
() -> Tld.get("tld").asBuilder().setRoidSuffix("123456789"));
assertThat(e).hasMessageThat().isEqualTo("ROID suffix must be in format ^[A-Z\\d_]{1,8}$");
assertThat(e)
.hasMessageThat()
.isEqualTo("ROID suffix 123456789 must be in format ^[A-Z\\d]{1,8}$");
}
@Test
@@ -766,6 +768,12 @@ public final class TldTest extends EntityTestCase {
IllegalArgumentException.class, () -> Tld.get("tld").asBuilder().setRoidSuffix("ABC-DEF"));
}
@Test
void testFailure_roidSuffixContainsUnderscores() {
assertThrows(
IllegalArgumentException.class, () -> Tld.get("tld").asBuilder().setRoidSuffix("ABC_DEF"));
}
@Test
void testSuccess_setDefaultPromoTokens() {
Tld registry = Tld.get("tld");

View File

@@ -34,6 +34,7 @@ import google.registry.keyring.KeyringModule;
import google.registry.keyring.api.KeyModule;
import google.registry.module.TestRequestComponent.TestRequestComponentModule;
import google.registry.monitoring.whitebox.StackdriverModule;
import google.registry.mosapi.module.MosApiModule;
import google.registry.persistence.PersistenceModule;
import google.registry.privileges.secretmanager.SecretManagerModule;
import google.registry.rde.JSchModule;
@@ -61,6 +62,7 @@ import jakarta.inject.Singleton;
GroupsModule.class,
GroupssettingsModule.class,
GsonModule.class,
MosApiModule.class,
JSchModule.class,
KeyModule.class,
KeyringModule.class,

View File

@@ -0,0 +1,203 @@
// Copyright 2025 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.mosapi;
import static com.google.common.truth.Truth.assertThat;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import com.google.common.collect.ImmutableMap;
import google.registry.mosapi.MosApiException.MosApiAuthorizationException;
import java.io.IOException;
import java.util.Map;
import okhttp3.Call;
import okhttp3.MediaType;
import okhttp3.OkHttpClient;
import okhttp3.Protocol;
import okhttp3.Request;
import okhttp3.Response;
import okhttp3.ResponseBody;
import okio.Buffer;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;
public class MosApiClientTest {
private static final String SERVICE_URL = "https://mosapi.example.com/v1";
private static final String ENTITY_TYPE = "registries";
// Mocks
private OkHttpClient mockHttpClient;
private Call mockCall;
private MosApiClient mosApiClient;
@BeforeEach
void setUp() {
mockHttpClient = mock(OkHttpClient.class);
mockCall = mock(Call.class);
when(mockHttpClient.newCall(any(Request.class))).thenReturn(mockCall);
mosApiClient = new MosApiClient(mockHttpClient, SERVICE_URL, ENTITY_TYPE);
}
@Test
void testConstructor_throwsOnInvalidUrl() {
IllegalArgumentException thrown =
assertThrows(
IllegalArgumentException.class,
() -> new MosApiClient(mockHttpClient, "ht tp://bad-url", ENTITY_TYPE));
assertThat(thrown).hasMessageThat().contains("Invalid MoSAPI Service URL");
}
// --- GET Request Tests ---
@Test
void testSendGetRequest_success() throws Exception {
// 1. Prepare Success Response
Response successResponse = createResponse(200, "{\"status\":\"ok\"}");
when(mockCall.execute()).thenReturn(successResponse);
Map<String, String> params = ImmutableMap.of("since", "2024-01-01");
Map<String, String> headers = ImmutableMap.of("Authorization", "Bearer token123");
// 2. Execute
try (Response response =
mosApiClient.sendGetRequest("tld-1", "monitoring/state", params, headers)) {
// Verify Response
assertThat(response.isSuccessful()).isTrue();
assertThat(response.body().string()).isEqualTo("{\"status\":\"ok\"}");
// 3. Verify Request Construction
ArgumentCaptor<Request> requestCaptor = ArgumentCaptor.forClass(Request.class);
verify(mockHttpClient).newCall(requestCaptor.capture());
Request capturedRequest = requestCaptor.getValue();
// Check URL:
assertThat(capturedRequest.method()).isEqualTo("GET");
assertThat(capturedRequest.url().encodedPath())
.isEqualTo("/v1/registries/tld-1/monitoring/state");
assertThat(capturedRequest.url().queryParameter("since")).isEqualTo("2024-01-01");
// Check Headers
assertThat(capturedRequest.header("Authorization")).isEqualTo("Bearer token123");
}
}
@Test
void testSendGetRequest_throwsOn401() throws IOException {
// Prepare 401 Response
Response unauthorizedResponse = createResponse(401, "Unauthorized");
when(mockCall.execute()).thenReturn(unauthorizedResponse);
MosApiAuthorizationException thrown =
assertThrows(
MosApiAuthorizationException.class,
() ->
mosApiClient.sendGetRequest("tld-1", "path", ImmutableMap.of(), ImmutableMap.of()));
assertThat(thrown).hasMessageThat().contains("Authorization failed");
}
@Test
void testSendGetRequest_wrapsIoException() throws IOException {
// Simulate Network Failure
when(mockCall.execute()).thenThrow(new IOException("Network error"));
// Execute & Assert
MosApiException thrown =
assertThrows(
MosApiException.class,
() ->
mosApiClient.sendGetRequest("tld-1", "path", ImmutableMap.of(), ImmutableMap.of()));
assertThat(thrown).hasMessageThat().contains("Error during GET request to");
assertThat(thrown).hasCauseThat().isInstanceOf(IOException.class);
}
// --- POST Request Tests ---
@Test
void testSendPostRequest_success() throws Exception {
// 1. Prepare Response
Response successResponse = createResponse(200, "{\"updated\":true}");
when(mockCall.execute()).thenReturn(successResponse);
String requestBody = "{\"data\":\"update\"}";
Map<String, String> headers = ImmutableMap.of("Content-Type", "application/json");
try (Response response =
mosApiClient.sendPostRequest("tld-1", "update", null, headers, requestBody)) {
assertThat(response.isSuccessful()).isTrue();
ArgumentCaptor<Request> requestCaptor = ArgumentCaptor.forClass(Request.class);
verify(mockHttpClient).newCall(requestCaptor.capture());
Request capturedRequest = requestCaptor.getValue();
assertThat(capturedRequest.method()).isEqualTo("POST");
assertThat(capturedRequest.url().encodedPath()).isEqualTo("/v1/registries/tld-1/update");
// Verify Body content
Buffer buffer = new Buffer();
capturedRequest.body().writeTo(buffer);
assertThat(buffer.readUtf8()).isEqualTo(requestBody);
}
}
@Test
void testSendPostRequest_throwsOn401() throws IOException {
// Prepare 401 Response
Response unauthorizedResponse = createResponse(401, "Unauthorized");
when(mockCall.execute()).thenReturn(unauthorizedResponse);
MosApiAuthorizationException thrown =
assertThrows(
MosApiAuthorizationException.class,
() ->
mosApiClient.sendPostRequest(
"tld-1", "path", ImmutableMap.of(), ImmutableMap.of(), "{}"));
assertThat(thrown).hasMessageThat().contains("Authorization failed");
}
@Test
void testSendPostRequest_wrapsIoException() throws IOException {
// Simulate Network Failure
when(mockCall.execute()).thenThrow(new IOException("Network error"));
MosApiException thrown =
assertThrows(
MosApiException.class,
() ->
mosApiClient.sendPostRequest(
"tld-1", "path", ImmutableMap.of(), ImmutableMap.of(), "{}"));
assertThat(thrown).hasMessageThat().contains("Error during POST request to");
assertThat(thrown).hasCauseThat().isInstanceOf(IOException.class);
}
/** Helper to build a real OkHttp Response object manually. */
private Response createResponse(int code, String bodyContent) {
return new Response.Builder()
.request(new Request.Builder().url("http://localhost/").build())
.protocol(Protocol.HTTP_1_1)
.code(code)
.message("Msg")
.body(ResponseBody.create(bodyContent, MediaType.parse("application/json")))
.build();
}
}

Some files were not shown because too many files have changed in this diff Show More