1
0
mirror of https://github.com/google/nomulus synced 2026-01-19 12:13:04 +00:00

Compare commits

...

40 Commits

Author SHA1 Message Date
Ben McIlwain
a7387e975b Add RDAP nameserver tests for .zz-- TLD hostnames (#2936)
The actual error is fixed as a side effect of PR #2935, but this adds tests
verifying the intended behavior.

BUG= http://b/476144993
2026-01-16 17:55:41 +00:00
Ben McIlwain
5c6667507b Remove contacts from RDE (#2934)
This is necessary to pass RST, as we cannot have any mention of contacts in our
escrow files as we are a thin registry.

BUG= http://b/474636582
2026-01-16 15:25:33 +00:00
Ben McIlwain
c187c92ae4 Allow creation of hostnames on .zz-- style TLDs for RST (#2935)
This is a follow-on to PR #2909, which fixed the issue for domains, but
apparently not fully for hostnames.

BUG= http://b/476144993
2026-01-15 20:37:32 +00:00
gbrodman
22ca4e3f2b Disable old fee extensions in non-prod envs (#2933)
The primary annoyance with this is that it means we need (or at least,
should) split all tests that use the fee extension into two separate
tests -- one that simulates non-prod environments, and one that
simulates prod environments. This leads to duplication of many tests but
that's fine since this is theoretically temporary.
2026-01-14 19:04:22 +00:00
Nilay Shah
f27136458a Configure cloud scheduler to trigger MoSAPI SLA status to cloud monitoring (#2926)
* Configure cloud scheduler to trigger MoSAPI SLA status to cloud monitoring in production

- We have kept this job to trigger for every 3 minutes so that we get near to real time update for our task.
- This will not trigger metrics for now as we have not written Metrics triggering logic yet
- Logs are added

* Change Trigger scheduling from 3 minutes to 5 minutes
2026-01-13 18:48:43 +00:00
Ben McIlwain
d8e647316e Remove contact as a supported object type in EPP (#2932)
This primarily affects the EPP greeting. We already were erroring out when any
contact flows attempted to be run; this should just prevent registrars from even
trying them at all.

This PR is designed to be minimally invasive, and does not remove any of the
contact flows or Jakarta XML/XJC objects/files themselves. That can be done
later as a follow-up.

Also note that the contact namespace urn:ietf:params:xml:ns:contact-1.0 is still
present for now in RDE exports, but I'll remove that subsequently as well.

BUG= http://b/475506288
2026-01-13 17:21:03 +00:00
Ben McIlwain
d6e0a7b979 Change domain update commands to be varipotent by status (#2930)
This means that attempting to add a status that is already present will now
fail, and attempting to remove a status that is not present will also now fail.

This also refactors the existing checks into a single verify method, rather than
having to call three separate methods from every callsite.

BUG= http://b/474645068
2026-01-12 22:12:08 +00:00
Juan Celhay
5725eb95e0 Add Cloud java profiler to nomulus docker images (#2919)
* add cloud profiler to dockerfile and start script

* add apt-get update

* change in cb machine type for nomulus

* fix typo

* add max worker limit to gradle tests

* Switch to root before doing apt-get

* correct dockerfile

* jetty/Dockerfile

* profiler service conditional to kubernetes container name
2026-01-12 15:19:05 +00:00
Pavlo Tkach
aa12998276 Increase console workload memory allocation (#2929) 2026-01-09 19:27:07 +00:00
gbrodman
d415416bc5 Update the fee extension 1.0 and add some tests (#2925)
Many of the actual fee extension changes are based off Weimin's PR
https://github.com/google/nomulus/pull/2912, though this makes some
additional changes based on the XML schema and description from RFC 8748.

This adds tests for the DomainCheckFlow which is the most complex and
thorough user of the fee extension, but we'll want to add further tests
to the other domain flows to make sure they're handled correctly.
2026-01-09 18:09:17 +00:00
gbrodman
3a1068f313 Add indexes on current_package_token in Domain* (#2916)
It just makes it possible to delete allocation tokens, otherwise we need
to do a linear search over the entire Domain and DomainHistory tables if
we ever want to delete something.
2026-01-09 17:55:37 +00:00
gbrodman
69e5d40f04 Forbid no-op domain-NS and host-IP adds/removes (#2928)
The RST testing expects us to fail if they try to remove an IP from a
host that already doesn't that have that IP, or to add one that already
exists (ditto on both for a domain's nameservers). I don't really see an
issue with our previous no-op implementation, but we need to do this to
pass the tests.
2026-01-09 17:55:12 +00:00
gbrodman
64f6cd9af4 Only include fee 1.0 extension in nonprod envs (#2927)
We need to have this enabled in sandbox, but we wish to wait to enable
it for production to make sure that the implementation is correct and
that clients can use it.

Soon we'll want to do something similar (but the opposite) with the old
fee extensions, where we **only** serve them in production (or maybe
unit test as well). That will allow us to pass the RST tests that depend
on only having the fee extension 1.0.
2026-01-08 22:00:39 +00:00
gbrodman
40184689ca Allow for a currency unit in fee:check responses (#2922)
This is / will be required in https://datatracker.ietf.org/doc/rfc8748/.
I split this out from the rest of the fee-extension testing so that it
can be easily visible.
2026-01-07 21:12:20 +00:00
Nilay Shah
826ad85d20 Add endpoint to trigger MoSAPI metrics export (#2923)
This commit introduces a new backend endpoint at `/_dr/task/triggerMosApiServiceState` that initiates the process of fetching the latest service states for all TLDs from the MoSAPI endpoint and exporting them as metrics to Cloud Monitoring.

  The key changes include:
   - A new `TriggerServiceStateAction` class that handles the GET request to the new endpoint.
     - Logic within `MosApiStateService` to concurrently fetch states for all configured TLDs.
     - A new `MosApiMetrics` class (currently a placeholder) responsible for sending the collected states to the monitoring service.
     - Unit tests for the new action and the updated service logic.

This endpoint will be called periodically to ensure that the MosApi service health metrics are kept up-to-date.
2026-01-07 19:13:19 +00:00
gbrodman
2b47bc9b0a Move fee class from extension to item (#2924)
this is coming from the schema https://datatracker.ietf.org/doc/rfc8748/
section 6.1. The class, that we use for "premium" notes, moved from the
command to the object itself.
2026-01-06 19:00:19 +00:00
gbrodman
9555dca8c6 Don't allow loopback IP addresses for hosts (#2920)
I don't know where in the spec these are explicitly disallowed, but it
seems like good practice and we'll fail the RST tests if we don't
disallow them.
2026-01-05 21:29:15 +00:00
Ben McIlwain
49484c06d3 Filter out registrars of type OT&E from RDE escrow deposits (#2921)
The RDE XML schema (which is verified by ICANN's RST) requires the presence of a
numeric IANA identifier, which is always null for OT&E registrars. This change
synchronizes the three types of registrars that must have a null IANA identifier
(see
https://cs.opensource.google/nomulus/nomulus/+/master:core/src/main/java/google/registry/model/registrar/Registrar.java;l=109-142;drc=b1266c95e8d9f8206415d2821929d4161869b699
) with the registrars that are excluded from the RDE deposit. Note that there
are no registrars of type OT&E in prod and I can't think of a reason they would
need to be included in escrow deposits on sandbox.
2026-01-05 21:20:11 +00:00
Nilay Shah
81d222e7d6 Add GetServiceState action for MoSAPI service monitoring (#2906)
* Add GetServiceState action for MoSAPI service monitoring

Implements the `/api/mosapi/getServiceState` endpoint to retrieve service health summaries for TLDs from the MoSAPI system.

- Introduces `GetServiceStateAction` to fetch TLD service status.
- Implements `MosApiStateService` to transform raw MoSAPI responses into a curated `ServiceStateSummary`.
- Uses concurrent processing with a fixed thread pool to fetch states for all configured TLDs efficiently while respecting MoSAPI rate limits.

junit test added

* Refactor MoSAPI models to records and address review nits

- Convert model classes to Java records for conciseness and immutability.
- Update unit tests to use Java text blocks for improved JSON readability.
- Simplify service and action layers by removing redundant logic and logging.
- Fix configuration nits regarding primitive types and comment formatting.

* Consolidate MoSAPI models and enhance null-safety

- Moves model records into a single MosApiModels.java file.
- Switches to ImmutableList/ImmutableMap with non-null defaults in constructors.
- Removes redundant pass-through methods in MosApiStateService.
- Updates tests to use Java Text Blocks and non-null collection assertions.

* Improve MoSAPI client error handling and clean up data models

Refactors the MoSAPI monitoring client to be more robust against
infrastructure failures

* Refactor: use nullToEmptyImmutableCopy() for MoSAPI models

Standardize null-handling in model classes by using the Nomulus
`nullToEmptyImmutableCopy()` utility. This ensures consistent API
responses with empty lists instead of omitted fields.
2026-01-05 15:44:01 +00:00
Weimin Yu
7e9d4c27d1 Use downloaded Gradle distribution on Cloud Build (#2918)
This way we get around the http url and no longer needs public access on
the GCS bucket.
2025-12-30 21:08:04 +00:00
Weimin Yu
f9c22ff1c5 Add RST support in Sandbox (#2917)
* Add RST support in Sandbox

Added RST test label files as resources.

Added a RstTmchUtils class that loads appropriate labels according to
TLD pattern.

Temporarily changed label fetching in production to include the TLD
string, so that the new class may know which set of labels to use.

* Addressing comments

* Addressing comments
2025-12-30 20:59:28 +00:00
gbrodman
2562d582f3 Add more strict hostname validation on host:check flows (#2915)
We do most of these on host create already so we should also do them on
host checks. The only added change is the character validation (our
existing hostnames all match these).
2025-12-30 16:41:56 +00:00
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
505 changed files with 13720 additions and 9323 deletions

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

@@ -98,8 +98,8 @@ PRESUBMITS = {
"File did not include the license header.",
# Files must end in a newline
PresubmitCheck(r".*\n$", ("java", "js", "soy", "sql", "py", "sh", "gradle", "ts"),
{"node_modules/"}, REQUIRED):
PresubmitCheck(r".*\n$", ("java", "js", "soy", "sql", "py", "sh", "gradle", "ts", "xml"),
{"node_modules/", ".idea"}, REQUIRED):
"Source files must end in a newline.",
# System.(out|err).println should only appear in tools/ or load-testing/

View File

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

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

@@ -50,8 +50,6 @@ import google.registry.config.CredentialModule;
import google.registry.config.RegistryConfig.ConfigModule;
import google.registry.gcs.GcsUtils;
import google.registry.model.EppResource;
import google.registry.model.contact.Contact;
import google.registry.model.contact.ContactHistory;
import google.registry.model.domain.Domain;
import google.registry.model.domain.DomainHistory;
import google.registry.model.host.Host;
@@ -73,7 +71,6 @@ import jakarta.inject.Inject;
import jakarta.inject.Singleton;
import java.io.IOException;
import java.io.Serializable;
import java.util.HashSet;
import org.apache.beam.sdk.Pipeline;
import org.apache.beam.sdk.PipelineResult;
import org.apache.beam.sdk.coders.KvCoder;
@@ -138,25 +135,21 @@ import org.joda.time.DateTime;
* pairs of (contact/host repo ID: pending deposit) for all RDE pending deposits for further
* processing.
*
* <h3>{@link Contact}</h3>
*
* We first join most recent contact histories, represented by (contact repo ID: contact history
* revision ID) pairs, with referenced contacts, represented by (contact repo ID: pending deposit)
* pairs, on the contact repo ID, to remove unreferenced contact histories. Contact resources are
* then loaded from the remaining referenced contact histories, and marshalled into (pending
* deposit: deposit fragment) pairs.
*
* <h3>{@link Host}</h3>
*
* Similar to {@link Contact}, we join the most recent host history with referenced hosts to find
* most recent referenced hosts. For external hosts we do the same treatment as we did on contacts
* and obtain the (pending deposit: deposit fragment) pairs. For subordinate hosts, we need to find
* the superordinate domain in order to properly handle pending transfer in the deposit as well. So
* we first find the superordinate domain repo ID from the host and join the (superordinate domain
* repo ID: (subordinate host repo ID: (pending deposit: revision ID))) pair with the (domain repo
* ID: revision ID) pair obtained from the domain history query in order to map the host at
* watermark to the domain at watermark. We then proceed to create the (pending deposit: deposit
* fragment) pair for subordinate hosts using the added domain information.
* <p>We first join most recent host histories, represented by (host repo ID: host history revision
* ID) pairs, with referenced hosts, represented by (host repo ID: pending deposit) pairs, on the
* host repo ID, to remove unreferenced host histories. Host resources are then loaded from the
* remaining referenced host histories, and marshalled into (pending deposit: deposit fragment)
* pairs.
*
* <p>For subordinate hosts, we need to find the superordinate domain in order to properly handle
* pending transfer in the deposit as well. So we first find the superordinate domain repo ID from
* the host and join the (superordinate domain repo ID: (subordinate host repo ID: (pending deposit:
* revision ID))) pair with the (domain repo ID: revision ID) pair obtained from the domain history
* query in order to map the host at watermark to the domain at watermark. We then proceed to create
* the (pending deposit: deposit fragment) pair for subordinate hosts using the added domain
* information.
*
* <h2>Processing {@link DepositFragment}</h2>
*
@@ -184,10 +177,10 @@ public class RdePipeline implements Serializable {
private final CloudTasksUtils cloudTasksUtils;
private final RdeMarshaller marshaller;
// Registrars to be excluded from data escrow. Not including the sandbox-only OTE type so that
// if sneaks into production we would get an extra signal.
// Registrars to be excluded from data escrow (i.e. all registrar types that have a null IANA
// identifier and thus would not be valid according to the RDE schema).
private static final ImmutableSet<Type> IGNORED_REGISTRAR_TYPES =
Sets.immutableEnumSet(Registrar.Type.MONITORING, Registrar.Type.TEST);
Sets.immutableEnumSet(Registrar.Type.MONITORING, Registrar.Type.OTE, Registrar.Type.TEST);
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
@@ -230,9 +223,6 @@ public class RdePipeline implements Serializable {
PCollection<KV<String, Long>> domainHistories =
getMostRecentHistoryEntries(pipeline, DomainHistory.class);
PCollection<KV<String, Long>> contactHistories =
getMostRecentHistoryEntries(pipeline, ContactHistory.class);
PCollection<KV<String, Long>> hostHistories =
getMostRecentHistoryEntries(pipeline, HostHistory.class);
@@ -241,10 +231,6 @@ public class RdePipeline implements Serializable {
PCollection<KV<PendingDeposit, DepositFragment>> domainFragments =
processedDomainHistories.get(DOMAIN_FRAGMENTS);
PCollection<KV<PendingDeposit, DepositFragment>> contactFragments =
processContactHistories(
processedDomainHistories.get(REFERENCED_CONTACTS), contactHistories);
PCollectionTuple processedHosts =
processHostHistories(processedDomainHistories.get(REFERENCED_HOSTS), hostHistories);
@@ -256,7 +242,6 @@ public class RdePipeline implements Serializable {
return PCollectionList.of(registrarFragments)
.and(domainFragments)
.and(contactFragments)
.and(externalHostFragments)
.and(subordinateHostFragments)
.apply(
@@ -437,7 +422,6 @@ public class RdePipeline implements Serializable {
private PCollectionTuple processDomainHistories(PCollection<KV<String, Long>> domainHistories) {
Counter activeDomainCounter = Metrics.counter("RDE", "ActiveDomainBase");
Counter domainFragmentCounter = Metrics.counter("RDE", "DomainFragment");
Counter referencedContactCounter = Metrics.counter("RDE", "ReferencedContact");
Counter referencedHostCounter = Metrics.counter("RDE", "ReferencedHost");
return domainHistories.apply(
"Map DomainHistory to DepositFragment " + "and emit referenced Contact and Host",
@@ -463,19 +447,8 @@ public class RdePipeline implements Serializable {
KV.of(
pendingDeposit,
marshaller.marshalDomain(domain, pendingDeposit.mode())));
// Contacts and hosts are only deposited in RDE, not BRDA.
// Hosts are only deposited in RDE, not BRDA.
if (pendingDeposit.mode() == RdeMode.FULL) {
HashSet<Serializable> contacts = new HashSet<>();
domain.getAdminContact().ifPresent(c -> contacts.add(c.getKey()));
domain.getTechContact().ifPresent(c -> contacts.add(c.getKey()));
domain.getRegistrant().ifPresent(c -> contacts.add(c.getKey()));
domain.getBillingContact().ifPresent(c -> contacts.add(c.getKey()));
referencedContactCounter.inc(contacts.size());
contacts.forEach(
contactRepoId ->
receiver
.get(REFERENCED_CONTACTS)
.output(KV.of((String) contactRepoId, pendingDeposit)));
if (domain.getNsHosts() != null) {
referencedHostCounter.inc(domain.getNsHosts().size());
domain
@@ -497,38 +470,6 @@ public class RdePipeline implements Serializable {
DOMAIN_FRAGMENTS, TupleTagList.of(REFERENCED_CONTACTS).and(REFERENCED_HOSTS)));
}
private PCollection<KV<PendingDeposit, DepositFragment>> processContactHistories(
PCollection<KV<String, PendingDeposit>> referencedContacts,
PCollection<KV<String, Long>> contactHistories) {
Counter contactFragmentCounter = Metrics.counter("RDE", "ContactFragment");
return removeUnreferencedResource(referencedContacts, contactHistories, Contact.class)
.apply(
"Map Contact to DepositFragment",
FlatMapElements.into(
kvs(
TypeDescriptor.of(PendingDeposit.class),
TypeDescriptor.of(DepositFragment.class)))
.via(
(KV<String, CoGbkResult> kv) -> {
Contact contact =
(Contact)
loadResourceByHistoryEntryId(
ContactHistory.class,
kv.getKey(),
kv.getValue().getAll(REVISION_ID));
DepositFragment fragment = marshaller.marshalContact(contact);
ImmutableSet<KV<PendingDeposit, DepositFragment>> fragments =
Streams.stream(kv.getValue().getAll(PENDING_DEPOSIT))
// The same contact could be used by multiple domains, therefore
// matched to the same pending deposit multiple times.
.distinct()
.map(pendingDeposit -> KV.of(pendingDeposit, fragment))
.collect(toImmutableSet());
contactFragmentCounter.inc(fragments.size());
return fragments;
}));
}
private PCollectionTuple processHostHistories(
PCollection<KV<String, PendingDeposit>> referencedHosts,
PCollection<KV<String, Long>> hostHistories) {

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;
@@ -1415,6 +1416,58 @@ 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);
}
@Provides
@Config("mosapiTldThreadCnt")
public static int provideMosapiTldThreads(RegistryConfigSettings config) {
return config.mosapi.tldThreadCnt;
}
private static String formatComments(String text) {
return Splitter.on('\n').omitEmptyStrings().trimResults().splitToList(text).stream()
.map(s -> "# " + s)

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 {
@@ -262,4 +263,15 @@ 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;
public int tldThreadCnt;
}
}

View File

@@ -616,3 +616,34 @@ 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"
# Provides a fixed thread pool for parallel TLD processing.
# @see <a href="https://www.icann.org/mosapi-specification.pdf">
# ICANN MoSAPI Specification, Section 12.3</a>
tldThreadCnt: 4

View File

@@ -322,4 +322,15 @@
<service>bsa</service>
<schedule>23 8,20 * * *</schedule>
</task>
<task>
<url><![CDATA[/_dr/task/triggerMosApiServiceState]]></url>
<name>triggerMosApiServiceState</name>
<description>
Fetches the service state from MosAPI and triggers the metrics status for all TLDs.
</description>
<!-- Runs every 5 minutes. -->
<schedule>*/5 * * * *</schedule>
</task>
</entries>

View File

@@ -194,13 +194,27 @@ public final class ResourceFlowUtils {
}
}
/** Check that the same values aren't being added and removed in an update command. */
public static void checkSameValuesNotAddedAndRemoved(
ImmutableSet<?> fieldsToAdd, ImmutableSet<?> fieldsToRemove)
throws AddRemoveSameValueException {
/**
* Verifies the adds and removes on a resource.
*
* <p>This throws an exception in three different situations: if the same value is being both
* added and removed, if a value is being added that is already present, or if a value is being
* removed that isn't present.
*/
public static <T> void verifyAddsAndRemoves(
ImmutableSet<T> existingFields, ImmutableSet<T> fieldsToAdd, ImmutableSet<T> fieldsToRemove)
throws AddRemoveSameValueException,
AddExistingValueException,
RemoveNonexistentValueException {
if (!intersection(fieldsToAdd, fieldsToRemove).isEmpty()) {
throw new AddRemoveSameValueException();
}
if (!intersection(fieldsToAdd, existingFields).isEmpty()) {
throw new AddExistingValueException();
}
if (intersection(fieldsToRemove, existingFields).size() != fieldsToRemove.size()) {
throw new RemoveNonexistentValueException();
}
}
/** Check that all {@link StatusValue} objects in a set are client-settable. */
@@ -266,6 +280,20 @@ public final class ResourceFlowUtils {
}
}
/** Cannot add a value that is already present. */
public static class AddExistingValueException extends ParameterValuePolicyErrorException {
public AddExistingValueException() {
super("Cannot add a value that is already present");
}
}
/** Cannot remove a value that does not exist. */
public static class RemoveNonexistentValueException extends ParameterValuePolicyErrorException {
public RemoveNonexistentValueException() {
super("Cannot remove a value that does not exist");
}
}
/** The specified status value cannot be set by clients. */
public static class StatusNotClientSettableException extends ParameterValueRangeErrorException {
public StatusNotClientSettableException(String statusValue) {

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

@@ -88,6 +88,7 @@ import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
import org.joda.money.CurrencyUnit;
import org.joda.time.DateTime;
/**
@@ -298,11 +299,13 @@ public final class DomainCheckFlow implements TransactionalFlow {
boolean shouldUseTieredPricingPromotion =
RegistryConfig.getTieredPricingPromotionRegistrarIds().contains(registrarId);
ImmutableSet.Builder<CurrencyUnit> currenciesBuilder = new ImmutableSet.Builder<>();
for (FeeCheckCommandExtensionItem feeCheckItem : feeCheck.getItems()) {
for (String domainName : getDomainNamesToCheckForFee(feeCheckItem, domainNames.keySet())) {
FeeCheckResponseExtensionItem.Builder<?> builder = feeCheckItem.createResponseBuilder();
Optional<Domain> domain = Optional.ofNullable(domainObjs.get(domainName));
Tld tld = Tld.get(domainNames.get(domainName).parent().toString());
currenciesBuilder.add(tld.getCurrency());
Optional<AllocationToken> token;
try {
// The precise token to use for this fee request may vary based on the domain or even the
@@ -385,7 +388,8 @@ public final class DomainCheckFlow implements TransactionalFlow {
responseItems.add(builder.setDomainNameIfSupported(domainName).build());
}
}
return ImmutableList.of(feeCheck.createResponse(responseItems.build()));
return ImmutableList.of(
feeCheck.createResponse(responseItems.build(), currenciesBuilder.build()));
}
/**

View File

@@ -108,7 +108,8 @@ public final class DomainClaimsCheckFlow implements TransactionalFlow {
verifyClaimsPeriodNotEnded(tld, now);
}
}
Optional<String> claimKey = ClaimsListDao.get().getClaimKey(parsedDomain.parts().get(0));
Optional<String> claimKey =
ClaimsListDao.get(tldStr).getClaimKey(parsedDomain.parts().get(0));
launchChecksBuilder.add(
LaunchCheck.create(
LaunchCheckName.create(claimKey.isPresent(), domainName), claimKey.orElse(null)));

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}
@@ -283,7 +280,7 @@ public final class DomainCreateFlow implements MutatingFlow {
checkAllowedAccessToTld(registrarId, tld.getTldStr());
checkHasBillingAccount(registrarId, tld.getTldStr());
boolean isValidReservedCreate = isValidReservedCreate(domainName, allocationToken);
ClaimsList claimsList = ClaimsListDao.get();
ClaimsList claimsList = ClaimsListDao.get(tld.getTldStr());
verifyIsGaOrSpecialCase(
tld,
claimsList,
@@ -315,7 +312,8 @@ public final class DomainCreateFlow implements MutatingFlow {
// at this point so that we can verify it before the "after validation" extension point.
signedMarkId =
tmchUtils
.verifySignedMarks(launchCreate.get().getSignedMarks(), domainLabel, now)
.verifySignedMarks(
tld.getTldStr(), launchCreate.get().getSignedMarks(), domainLabel, now)
.getId();
}
verifyNotBlockedByBsa(domainName, tld, now, allocationToken);

View File

@@ -55,7 +55,7 @@ public final class DomainFlowTmchUtils {
}
public SignedMark verifySignedMarks(
ImmutableList<AbstractSignedMark> signedMarks, String domainLabel, DateTime now)
String tld, ImmutableList<AbstractSignedMark> signedMarks, String domainLabel, DateTime now)
throws EppException {
if (signedMarks.size() > 1) {
throw new TooManySignedMarksException();
@@ -64,7 +64,7 @@ public final class DomainFlowTmchUtils {
throw new SignedMarksMustBeEncodedException();
}
SignedMark signedMark =
verifyEncodedSignedMark((EncodedSignedMark) signedMarks.get(0), now);
verifyEncodedSignedMark(tld, (EncodedSignedMark) signedMarks.get(0), now);
return verifySignedMarkValidForDomainLabel(signedMark, domainLabel);
}
@@ -76,8 +76,9 @@ public final class DomainFlowTmchUtils {
return signedMark;
}
public SignedMark verifyEncodedSignedMark(EncodedSignedMark encodedSignedMark, DateTime now)
throws EppException {
// TODO(b/412715713): remove the tld parameter when RST completes.
public SignedMark verifyEncodedSignedMark(
String tld, EncodedSignedMark encodedSignedMark, DateTime now) throws EppException {
if (!encodedSignedMark.getEncoding().equals("base64")) {
throw new Base64RequiredForEncodedSignedMarksException();
}
@@ -95,7 +96,7 @@ public final class DomainFlowTmchUtils {
throw new SignedMarkParsingErrorException();
}
if (SignedMarkRevocationList.get().isSmdRevoked(signedMark.getId(), now)) {
if (SignedMarkRevocationList.get(tld).isSmdRevoked(signedMark.getId(), now)) {
throw new SignedMarkRevokedErrorException();
}

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;
@@ -222,7 +218,7 @@ public class DomainFlowUtils {
return domainName;
}
private static void validateFirstLabel(String firstLabel) throws EppException {
public static void validateFirstLabel(String firstLabel) throws EppException {
if (firstLabel.length() > MAX_LABEL_SIZE) {
throw new DomainLabelTooLongException();
}
@@ -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();
}
}
@@ -1018,23 +976,21 @@ public class DomainFlowUtils {
throw new UrgentAttributeNotSupportedException();
}
// There must be at least one of add/rem/chg, and chg isn't actually supported.
if (secDnsUpdate.getChange() != null) {
if (secDnsUpdate.getChange().isPresent()) {
// The only thing you can change is maxSigLife, and we don't support that at all.
throw new MaxSigLifeChangeNotSupportedException();
}
Add add = secDnsUpdate.getAdd();
Remove remove = secDnsUpdate.getRemove();
if (add == null && remove == null) {
Optional<Add> add = secDnsUpdate.getAdd();
Optional<Remove> remove = secDnsUpdate.getRemove();
if (add.isEmpty() && remove.isEmpty()) {
throw new EmptySecDnsUpdateException();
}
if (remove != null && Boolean.FALSE.equals(remove.getAll())) {
if (remove.isPresent() && Boolean.FALSE.equals(remove.get().getAll())) {
throw new SecDnsAllUsageException(); // Explicit all=false is meaningless.
}
Set<DomainDsData> toAdd = (add == null) ? ImmutableSet.of() : add.getDsData();
Set<DomainDsData> toAdd = add.map(Add::getDsData).orElse(ImmutableSet.of());
Set<DomainDsData> toRemove =
(remove == null)
? ImmutableSet.of()
: (remove.getAll() == null) ? remove.getDsData() : oldDsData;
remove.map(r -> (r.getAll() == null) ? r.getDsData() : oldDsData).orElse(ImmutableSet.of());
// RFC 5910 specifies that removes are processed before adds.
return ImmutableSet.copyOf(union(difference(oldDsData, toRemove), toAdd));
}
@@ -1282,49 +1238,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 +1354,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 +1361,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

@@ -21,8 +21,8 @@ import static com.google.common.collect.Sets.union;
import static google.registry.dns.DnsUtils.requestDomainDnsRefresh;
import static google.registry.flows.FlowUtils.persistEntityChanges;
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.verifyAddsAndRemoves;
import static google.registry.flows.ResourceFlowUtils.verifyAllStatusesAreClientSettable;
import static google.registry.flows.ResourceFlowUtils.verifyNoDisallowedStatuses;
import static google.registry.flows.ResourceFlowUtils.verifyOptionalAuthInfo;
@@ -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;
@@ -79,6 +75,8 @@ import google.registry.model.domain.fee.FeeUpdateCommandExtension;
import google.registry.model.domain.metadata.MetadataExtension;
import google.registry.model.domain.secdns.DomainDsData;
import google.registry.model.domain.secdns.SecDnsUpdateExtension;
import google.registry.model.domain.secdns.SecDnsUpdateExtension.Add;
import google.registry.model.domain.secdns.SecDnsUpdateExtension.Remove;
import google.registry.model.domain.superuser.DomainUpdateSuperuserExtension;
import google.registry.model.eppcommon.AuthInfo;
import google.registry.model.eppcommon.StatusValue;
@@ -123,10 +121,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}
@@ -252,12 +247,19 @@ public final class DomainUpdateFlow implements MutatingFlow {
private Domain performUpdate(Update command, Domain domain, DateTime now) throws EppException {
AddRemove add = command.getInnerAdd();
AddRemove remove = command.getInnerRemove();
checkSameValuesNotAddedAndRemoved(add.getNameservers(), remove.getNameservers());
checkSameValuesNotAddedAndRemoved(add.getContacts(), remove.getContacts());
checkSameValuesNotAddedAndRemoved(add.getStatusValues(), remove.getStatusValues());
Change change = command.getInnerChange();
Optional<SecDnsUpdateExtension> secDnsUpdate =
eppInput.getSingleExtension(SecDnsUpdateExtension.class);
verifyAddsAndRemoves(domain.getNameservers(), add.getNameservers(), remove.getNameservers());
verifyAddsAndRemoves(domain.getContacts(), add.getContacts(), remove.getContacts());
verifyAddsAndRemoves(domain.getStatusValues(), add.getStatusValues(), remove.getStatusValues());
if (secDnsUpdate.isPresent()) {
SecDnsUpdateExtension ext = secDnsUpdate.get();
verifyAddsAndRemoves(
domain.getDsData(),
ext.getAdd().map(Add::getDsData).orElse(ImmutableSet.of()),
ext.getRemove().map(Remove::getDsData).orElse(ImmutableSet.of()));
}
Change change = command.getInnerChange();
// We have to verify no duplicate contacts _before_ constructing the domain because it is
// illegal to construct a domain with duplicate contacts.
@@ -307,18 +309,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

@@ -65,6 +65,7 @@ public final class HostCheckFlow implements TransactionalFlow {
ForeignKeyUtils.loadKeys(Host.class, hostnames, clock.nowUtc()).keySet();
ImmutableList.Builder<HostCheck> checks = new ImmutableList.Builder<>();
for (String hostname : hostnames) {
HostFlowUtils.validateHostName(hostname);
boolean unused = !existingIds.contains(hostname);
checks.add(HostCheck.create(unused, hostname, unused ? null : "In use"));
}

View File

@@ -116,6 +116,7 @@ public final class HostCreateFlow implements MutatingFlow {
? new SubordinateHostMustHaveIpException()
: new UnexpectedExternalHostIpException();
}
HostFlowUtils.validateInetAddresses(command.getInetAddresses());
Host newHost =
new Host.Builder()
.setCreationRegistrarId(registrarId)

View File

@@ -14,12 +14,16 @@
package google.registry.flows.host;
import static google.registry.flows.domain.DomainFlowUtils.validateFirstLabel;
import static google.registry.model.EppResourceUtils.isActive;
import static google.registry.model.tld.Tlds.findTldForName;
import static google.registry.util.DomainNameUtils.canonicalizeHostname;
import static google.registry.util.PreconditionsUtils.checkArgumentNotNull;
import static java.util.stream.Collectors.joining;
import com.google.common.base.Ascii;
import com.google.common.base.CharMatcher;
import com.google.common.collect.ImmutableSet;
import com.google.common.net.InternetDomainName;
import google.registry.flows.EppException;
import google.registry.flows.EppException.AuthorizationErrorException;
@@ -31,13 +35,17 @@ import google.registry.flows.EppException.StatusProhibitsOperationException;
import google.registry.model.ForeignKeyUtils;
import google.registry.model.domain.Domain;
import google.registry.model.eppcommon.StatusValue;
import google.registry.util.Idn;
import java.net.InetAddress;
import java.util.Optional;
import org.joda.time.DateTime;
/** Static utility functions for host flows. */
public class HostFlowUtils {
/** Validator for ASCII lowercase letters, digits, and "-_", allowing "." as a separator */
private static final CharMatcher HOST_NAME_ALLOWED_CHARS =
CharMatcher.inRange('a', 'z').or(CharMatcher.inRange('0', '9').or(CharMatcher.anyOf("-._")));
/** Checks that a host name is valid. */
public static InternetDomainName validateHostName(String name) throws EppException {
checkArgumentNotNull(name, "Must specify host name to validate");
@@ -49,10 +57,13 @@ public class HostFlowUtils {
throw new HostNameNotLowerCaseException(hostNameLowerCase);
}
try {
String hostNamePunyCoded = Idn.toASCII(name);
String hostNamePunyCoded = canonicalizeHostname(name);
if (!name.equals(hostNamePunyCoded)) {
throw new HostNameNotPunyCodedException(hostNamePunyCoded);
}
if (!HOST_NAME_ALLOWED_CHARS.matchesAllOf(name)) {
throw new BadHostNameCharacterException();
}
InternetDomainName hostName = InternetDomainName.from(name);
if (!name.equals(hostName.toString())) {
throw new HostNameNotNormalizedException(hostName.toString());
@@ -71,6 +82,7 @@ public class HostFlowUtils {
if (hostName.parts().size() < effectiveTld.parts().size() + 2) {
throw new HostNameTooShallowException();
}
validateFirstLabel(hostName.parts().getFirst());
return hostName;
} catch (IllegalArgumentException e) {
throw new InvalidHostNameException();
@@ -98,6 +110,24 @@ public class HostFlowUtils {
return superordinateDomain;
}
/** Makes sure that no provided IP addresses are local / loopback addresses. */
public static void validateInetAddresses(ImmutableSet<InetAddress> inetAddresses)
throws EppException {
if (inetAddresses == null) {
return;
}
if (inetAddresses.stream().anyMatch(InetAddress::isLoopbackAddress)) {
throw new LoopbackIpNotValidForHostException();
}
}
/** Loopback IPs are not valid for hosts. */
static class LoopbackIpNotValidForHostException extends ParameterValuePolicyErrorException {
public LoopbackIpNotValidForHostException() {
super("Loopback IPs are not valid for hosts");
}
}
/** Superordinate domain for this hostname does not exist. */
static class SuperordinateDomainDoesNotExistException extends ObjectDoesNotExistException {
public SuperordinateDomainDoesNotExistException(String domainName) {
@@ -180,4 +210,11 @@ public class HostFlowUtils {
String.format("Host names must be in normalized format; expected %s", expectedHostName));
}
}
/** Host names can only contain a-z, 0-9, '.', '_', and '-'. */
static class BadHostNameCharacterException extends ParameterValueSyntaxErrorException {
public BadHostNameCharacterException() {
super("Host names can only contain a-z, 0-9, '.', '_', and '-'");
}
}
}

View File

@@ -20,8 +20,8 @@ import static google.registry.dns.DnsUtils.requestHostDnsRefresh;
import static google.registry.dns.RefreshDnsOnHostRenameAction.PARAM_HOST_KEY;
import static google.registry.dns.RefreshDnsOnHostRenameAction.QUEUE_HOST_RENAME;
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.verifyAddsAndRemoves;
import static google.registry.flows.ResourceFlowUtils.verifyAllStatusesAreClientSettable;
import static google.registry.flows.ResourceFlowUtils.verifyNoDisallowedStatuses;
import static google.registry.flows.ResourceFlowUtils.verifyResourceOwnership;
@@ -159,8 +159,11 @@ public final class HostUpdateFlow implements MutatingFlow {
}
AddRemove add = command.getInnerAdd();
AddRemove remove = command.getInnerRemove();
checkSameValuesNotAddedAndRemoved(add.getStatusValues(), remove.getStatusValues());
checkSameValuesNotAddedAndRemoved(add.getInetAddresses(), remove.getInetAddresses());
verifyAddsAndRemoves(
existingHost.getStatusValues(), add.getStatusValues(), remove.getStatusValues());
verifyAddsAndRemoves(
existingHost.getInetAddresses(), add.getInetAddresses(), remove.getInetAddresses());
HostFlowUtils.validateInetAddresses(add.getInetAddresses());
VKey<Domain> newSuperordinateDomainKey =
newSuperordinateDomain.map(Domain::createVKey).orElse(null);
// If the superordinateDomain field is changing, set the lastSuperordinateChange to now.

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

@@ -15,6 +15,8 @@
package google.registry.model.domain.fee;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import google.registry.flows.EppException;
import google.registry.model.eppinput.EppInput.CommandExtension;
import org.joda.money.CurrencyUnit;
@@ -42,4 +44,11 @@ public interface FeeCheckCommandExtension<
ImmutableList<C> getItems();
R createResponse(ImmutableList<? extends FeeCheckResponseExtensionItem> items);
default R createResponse(
ImmutableList<? extends FeeCheckResponseExtensionItem> items,
ImmutableSet<CurrencyUnit> currenciesSeen)
throws EppException {
return createResponse(items);
}
}

View File

@@ -18,7 +18,6 @@ import com.google.common.base.Ascii;
import google.registry.model.domain.Period;
import google.registry.model.domain.fee.FeeCheckCommandExtensionItem;
import jakarta.xml.bind.annotation.XmlAttribute;
import jakarta.xml.bind.annotation.XmlElement;
import jakarta.xml.bind.annotation.XmlType;
import java.util.Locale;
import java.util.Optional;
@@ -32,12 +31,13 @@ import org.joda.time.DateTime;
* <pre>{@code
* <fee:command name="renew" phase="sunrise" subphase="hello">
* <fee:period unit="y">1</fee:period>
* <fee:class>premium</fee:class>
* <fee:date>2017-05-17T13:22:21.0Z</fee:date>
* </fee:command>
* }</pre>
*
* <p>The `feeClass` and `feeDate` attributes that are present in version 0.12 are removed from this
* version.
*/
@XmlType(propOrder = {"period", "feeClass", "feeDate"})
@XmlType(propOrder = {"period"})
public class FeeCheckCommandExtensionItemStdV1 extends FeeCheckCommandExtensionItem {
/** The default validity period (if not specified) is 1 year for all operations. */
@@ -50,12 +50,6 @@ public class FeeCheckCommandExtensionItemStdV1 extends FeeCheckCommandExtensionI
@XmlAttribute String subphase;
@XmlElement(name = "class")
String feeClass;
@XmlElement(name = "date")
DateTime feeDate;
/** Version 1.0 does not support domain name or currency in fee extension items. */
@Override
public boolean isDomainNameSupported() {
@@ -107,6 +101,6 @@ public class FeeCheckCommandExtensionItemStdV1 extends FeeCheckCommandExtensionI
@Override
public Optional<DateTime> getEffectiveDate() {
return Optional.ofNullable(feeDate);
return Optional.empty();
}
}

View File

@@ -17,6 +17,9 @@ package google.registry.model.domain.feestdv1;
import static google.registry.util.CollectionUtils.nullToEmptyImmutableCopy;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import google.registry.flows.EppException;
import google.registry.flows.EppException.ParameterValuePolicyErrorException;
import google.registry.model.ImmutableObject;
import google.registry.model.domain.fee.FeeCheckCommandExtension;
import google.registry.model.domain.fee.FeeCheckResponseExtensionItem;
@@ -51,13 +54,33 @@ public class FeeCheckCommandExtensionStdV1 extends ImmutableObject
@Override
public FeeCheckResponseExtensionStdV1 createResponse(
ImmutableList<? extends FeeCheckResponseExtensionItem> items) {
throw new UnsupportedOperationException("FeeCheckCommandExtensionStdV1 requires a currency");
}
@Override
public FeeCheckResponseExtensionStdV1 createResponse(
ImmutableList<? extends FeeCheckResponseExtensionItem> items,
ImmutableSet<CurrencyUnit> currenciesSeen)
throws EppException {
ImmutableList.Builder<FeeCheckResponseExtensionItemStdV1> builder =
new ImmutableList.Builder<>();
for (FeeCheckResponseExtensionItem item : items) {
if (item instanceof FeeCheckResponseExtensionItemStdV1) {
builder.add((FeeCheckResponseExtensionItemStdV1) item);
if (item instanceof FeeCheckResponseExtensionItemStdV1 stdv1Item) {
builder.add(stdv1Item);
}
}
return FeeCheckResponseExtensionStdV1.create(currency, builder.build());
if (currenciesSeen.size() > 1) {
throw new MultipleCurrenciesCannotBeCheckedException();
}
return FeeCheckResponseExtensionStdV1.create(currenciesSeen.iterator().next(), builder.build());
}
/** Domains across multiple currencies cannot be checked simultaneously. */
public static class MultipleCurrenciesCannotBeCheckedException
extends ParameterValuePolicyErrorException {
public MultipleCurrenciesCannotBeCheckedException() {
// The fee extension 1.0 only supports one currency shared across all results
super("Domains across multiple currencies cannot be checked simultaneously");
}
}
}

View File

@@ -24,13 +24,11 @@ import google.registry.model.domain.Period;
import google.registry.model.domain.fee.Fee;
import google.registry.model.domain.fee.FeeQueryCommandExtensionItem.CommandName;
import jakarta.xml.bind.annotation.XmlAttribute;
import jakarta.xml.bind.annotation.XmlElement;
import jakarta.xml.bind.annotation.XmlType;
import java.util.List;
import org.joda.time.DateTime;
/** The version 1.0 response command entity for a domain check on a single resource. */
@XmlType(propOrder = {"period", "fee", "feeClass", "effectiveDate", "notAfterDate"})
@XmlType(propOrder = {"period", "fee"})
public class FeeCheckResponseExtensionItemCommandStdV1 extends ImmutableObject {
/** The command that was checked. */
@@ -53,26 +51,6 @@ public class FeeCheckResponseExtensionItemCommandStdV1 extends ImmutableObject {
*/
List<Fee> fee;
/**
* The type of the fee.
*
* <p>We will use "premium" for fees on premium names, and omit the field otherwise.
*/
@XmlElement(name = "class")
String feeClass;
/** The effective date that the check is to be performed on (if specified in the query). */
@XmlElement(name = "date")
DateTime effectiveDate;
/** The date after which the quoted fee is no longer valid (if applicable). */
@XmlElement(name = "notAfter")
DateTime notAfterDate;
public String getFeeClass() {
return feeClass;
}
/** Builder for {@link FeeCheckResponseExtensionItemCommandStdV1}. */
public static class Builder extends Buildable.Builder<FeeCheckResponseExtensionItemCommandStdV1> {
@@ -96,24 +74,9 @@ public class FeeCheckResponseExtensionItemCommandStdV1 extends ImmutableObject {
return this;
}
public Builder setEffectiveDate(DateTime effectiveDate) {
getInstance().effectiveDate = effectiveDate;
return this;
}
public Builder setNotAfterDate(DateTime notAfterDate) {
getInstance().notAfterDate = notAfterDate;
return this;
}
public Builder setFee(List<Fee> fees) {
getInstance().fee = forceEmptyToNull(ImmutableList.copyOf(fees));
return this;
}
public Builder setClass(String feeClass) {
getInstance().feeClass = feeClass;
return this;
}
}
}

View File

@@ -17,20 +17,18 @@ package google.registry.model.domain.feestdv1;
import static google.registry.util.CollectionUtils.forceEmptyToNull;
import com.google.common.collect.ImmutableList;
import google.registry.model.domain.DomainObjectSpec;
import google.registry.model.domain.Period;
import google.registry.model.domain.fee.Fee;
import google.registry.model.domain.fee.FeeCheckResponseExtensionItem;
import google.registry.model.domain.fee.FeeQueryCommandExtensionItem.CommandName;
import jakarta.xml.bind.annotation.XmlType;
import org.joda.time.DateTime;
/** The version 1.0 response for a domain check on a single resource. */
@XmlType(propOrder = {"object", "command"})
@XmlType(propOrder = {"objID", "feeClass", "command"})
public class FeeCheckResponseExtensionItemStdV1 extends FeeCheckResponseExtensionItem {
/** The domain that was checked. */
DomainObjectSpec object;
String objID;
/** The command that was checked. */
FeeCheckResponseExtensionItemCommandStdV1 command;
@@ -53,15 +51,6 @@ public class FeeCheckResponseExtensionItemStdV1 extends FeeCheckResponseExtensio
return super.getFees();
}
/**
* This method is not annotated for JAXB because this version of the extension doesn't support
* "feeClass" and because the data comes off of the command object rather than a field.
*/
@Override
public String getFeeClass() {
return command.getFeeClass();
}
/** Builder for {@link FeeCheckResponseExtensionItemStdV1}. */
public static class Builder
extends FeeCheckResponseExtensionItem.Builder<FeeCheckResponseExtensionItemStdV1> {
@@ -91,13 +80,13 @@ public class FeeCheckResponseExtensionItemStdV1 extends FeeCheckResponseExtensio
@Override
public Builder setClass(String feeClass) {
commandBuilder.setClass(feeClass);
super.setClass(feeClass);
return this;
}
@Override
public Builder setDomainNameIfSupported(String name) {
getInstance().object = new DomainObjectSpec(name);
getInstance().objID = name;
return this;
}
@@ -106,17 +95,5 @@ public class FeeCheckResponseExtensionItemStdV1 extends FeeCheckResponseExtensio
getInstance().command = commandBuilder.build();
return super.build();
}
@Override
public Builder setEffectiveDateIfSupported(DateTime effectiveDate) {
commandBuilder.setEffectiveDate(effectiveDate);
return this;
}
@Override
public Builder setNotAfterDateIfSupported(DateTime notAfterDate) {
commandBuilder.setNotAfterDate(notAfterDate);
return this;
}
}
}

View File

@@ -31,7 +31,7 @@ import jakarta.xml.bind.annotation.adapters.XmlJavaTypeAdapter;
@Access(AccessType.FIELD)
public abstract class DomainDsDataBase extends ImmutableObject implements UnsafeSerializable {
@XmlTransient @Transient String domainRepoId;
@XmlTransient @Transient @Insignificant String domainRepoId;
/** The identifier for this particular key in the domain. */
@Transient int keyTag;

View File

@@ -24,6 +24,7 @@ import jakarta.xml.bind.annotation.XmlElement;
import jakarta.xml.bind.annotation.XmlRootElement;
import jakarta.xml.bind.annotation.XmlTransient;
import jakarta.xml.bind.annotation.XmlType;
import java.util.Optional;
import java.util.Set;
/** The EPP secDNS extension that may be present on domain update commands. */
@@ -55,16 +56,16 @@ public class SecDnsUpdateExtension extends ImmutableObject implements CommandExt
return urgent;
}
public Remove getRemove() {
return remove;
public Optional<Remove> getRemove() {
return Optional.ofNullable(remove);
}
public Add getAdd() {
return add;
public Optional<Add> getAdd() {
return Optional.ofNullable(add);
}
public Change getChange() {
return change;
public Optional<Change> getChange() {
return Optional.ofNullable(change);
}
@XmlTransient

View File

@@ -14,13 +14,17 @@
package google.registry.model.eppcommon;
import static com.google.common.collect.ImmutableList.toImmutableList;
import static java.nio.charset.StandardCharsets.UTF_8;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import google.registry.model.ImmutableObject;
import google.registry.model.eppinput.EppInput;
import google.registry.model.eppoutput.EppOutput;
import google.registry.util.NonFinalForTesting;
import google.registry.util.RegistryEnvironment;
import google.registry.xml.ValidationMode;
import google.registry.xml.XmlException;
import google.registry.xml.XmlTransformer;
@@ -31,7 +35,7 @@ import java.io.ByteArrayOutputStream;
public class EppXmlTransformer {
// Hardcoded XML schemas, ordered with respect to dependency.
private static final ImmutableList<String> SCHEMAS =
private static final ImmutableList<String> ALL_SCHEMAS =
ImmutableList.of(
"eppcom.xsd",
"epp.xsd",
@@ -54,11 +58,39 @@ public class EppXmlTransformer {
"allocationToken-1.0.xsd",
"bulkToken.xsd");
private static final XmlTransformer INPUT_TRANSFORMER =
new XmlTransformer(SCHEMAS, EppInput.class);
// XML schemas that should not be used in production (yet)
private static final ImmutableSet<String> NON_PROD_SCHEMAS = ImmutableSet.of("fee-std-v1.xsd");
private static final XmlTransformer OUTPUT_TRANSFORMER =
new XmlTransformer(SCHEMAS, EppOutput.class);
// XML schemas that should only be used in production (for backcompat)
private static final ImmutableSet<String> ONLY_PROD_SCHEMAS =
ImmutableSet.of("fee06.xsd", "fee11.xsd", "fee12.xsd");
// TODO(gbrodman): make this final when we can actually remove the old fee extensions and aren't
// relying on switching by environment
@NonFinalForTesting
private static XmlTransformer INPUT_TRANSFORMER =
new XmlTransformer(getSchemas(), EppInput.class);
// TODO(gbrodman): make this final when we can actually remove the old fee extensions and aren't
// relying on switching by environment
@NonFinalForTesting
private static XmlTransformer OUTPUT_TRANSFORMER =
new XmlTransformer(getSchemas(), EppOutput.class);
@VisibleForTesting
public static ImmutableList<String> getSchemas() {
ImmutableSet<String> schemasToSkip =
RegistryEnvironment.get().equals(RegistryEnvironment.PRODUCTION)
? NON_PROD_SCHEMAS
: ONLY_PROD_SCHEMAS;
return ALL_SCHEMAS.stream().filter(s -> !schemasToSkip.contains(s)).collect(toImmutableList());
}
@VisibleForTesting
public static void reloadTransformers() {
INPUT_TRANSFORMER = new XmlTransformer(getSchemas(), EppInput.class);
OUTPUT_TRANSFORMER = new XmlTransformer(getSchemas(), EppOutput.class);
}
public static void validateOutput(String xml) throws XmlException {
OUTPUT_TRANSFORMER.validate(xml);

View File

@@ -17,6 +17,7 @@ package google.registry.model.eppcommon;
import static com.google.common.collect.ImmutableSet.toImmutableSet;
import static com.google.common.collect.Maps.uniqueIndex;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import google.registry.model.domain.fee06.FeeCheckCommandExtensionV06;
@@ -33,6 +34,8 @@ import google.registry.model.domain.rgp.RgpUpdateExtension;
import google.registry.model.domain.secdns.SecDnsCreateExtension;
import google.registry.model.eppinput.EppInput.CommandExtension;
import google.registry.model.eppoutput.EppResponse.ResponseExtension;
import google.registry.util.NonFinalForTesting;
import google.registry.util.RegistryEnvironment;
import jakarta.xml.bind.annotation.XmlSchema;
import java.util.EnumSet;
@@ -43,35 +46,52 @@ public class ProtocolDefinition {
public static final String LANGUAGE = "en";
public static final ImmutableSet<String> SUPPORTED_OBJECT_SERVICES =
ImmutableSet.of(
"urn:ietf:params:xml:ns:host-1.0",
"urn:ietf:params:xml:ns:domain-1.0",
"urn:ietf:params:xml:ns:contact-1.0");
ImmutableSet.of("urn:ietf:params:xml:ns:host-1.0", "urn:ietf:params:xml:ns:domain-1.0");
/** Enums repesenting valid service extensions that are recognized by the server. */
/** Enum representing which environments should have which service extensions enabled. */
private enum ServiceExtensionVisibility {
ALL,
ONLY_IN_PRODUCTION,
ONLY_IN_NON_PRODUCTION,
NONE
}
/** Enum representing valid service extensions that are recognized by the server. */
public enum ServiceExtension {
LAUNCH_EXTENSION_1_0(LaunchCreateExtension.class, null, true),
REDEMPTION_GRACE_PERIOD_1_0(RgpUpdateExtension.class, null, true),
SECURE_DNS_1_1(SecDnsCreateExtension.class, null, true),
FEE_0_6(FeeCheckCommandExtensionV06.class, FeeCheckResponseExtensionV06.class, true),
FEE_0_11(FeeCheckCommandExtensionV11.class, FeeCheckResponseExtensionV11.class, true),
FEE_0_12(FeeCheckCommandExtensionV12.class, FeeCheckResponseExtensionV12.class, true),
FEE_1_00(FeeCheckCommandExtensionStdV1.class, FeeCheckResponseExtensionStdV1.class, false),
METADATA_1_0(MetadataExtension.class, null, false);
LAUNCH_EXTENSION_1_0(LaunchCreateExtension.class, null, ServiceExtensionVisibility.ALL),
REDEMPTION_GRACE_PERIOD_1_0(RgpUpdateExtension.class, null, ServiceExtensionVisibility.ALL),
SECURE_DNS_1_1(SecDnsCreateExtension.class, null, ServiceExtensionVisibility.ALL),
FEE_0_6(
FeeCheckCommandExtensionV06.class,
FeeCheckResponseExtensionV06.class,
ServiceExtensionVisibility.ONLY_IN_PRODUCTION),
FEE_0_11(
FeeCheckCommandExtensionV11.class,
FeeCheckResponseExtensionV11.class,
ServiceExtensionVisibility.ONLY_IN_PRODUCTION),
FEE_0_12(
FeeCheckCommandExtensionV12.class,
FeeCheckResponseExtensionV12.class,
ServiceExtensionVisibility.ONLY_IN_PRODUCTION),
FEE_1_00(
FeeCheckCommandExtensionStdV1.class,
FeeCheckResponseExtensionStdV1.class,
ServiceExtensionVisibility.ONLY_IN_NON_PRODUCTION),
METADATA_1_0(MetadataExtension.class, null, ServiceExtensionVisibility.NONE);
private final Class<? extends CommandExtension> commandExtensionClass;
private final Class<? extends ResponseExtension> responseExtensionClass;
private final String uri;
private final boolean visible;
private final ServiceExtensionVisibility visibility;
ServiceExtension(
Class<? extends CommandExtension> commandExtensionClass,
Class<? extends ResponseExtension> responseExtensionClass,
boolean visible) {
ServiceExtensionVisibility visibility) {
this.commandExtensionClass = commandExtensionClass;
this.responseExtensionClass = responseExtensionClass;
this.uri = getCommandExtensionUri(commandExtensionClass);
this.visible = visible;
this.visibility = visibility;
}
public Class<? extends CommandExtension> getCommandExtensionClass() {
@@ -86,14 +106,20 @@ public class ProtocolDefinition {
return uri;
}
public boolean getVisible() {
return visible;
}
/** Returns the namespace URI of the command extension class. */
public static String getCommandExtensionUri(Class<? extends CommandExtension> clazz) {
return clazz.getPackage().getAnnotation(XmlSchema.class).namespace();
}
public boolean isVisible() {
return switch (visibility) {
case ALL -> true;
case ONLY_IN_PRODUCTION -> RegistryEnvironment.get().equals(RegistryEnvironment.PRODUCTION);
case ONLY_IN_NON_PRODUCTION ->
!RegistryEnvironment.get().equals(RegistryEnvironment.PRODUCTION);
case NONE -> false;
};
}
}
/**
@@ -110,15 +136,25 @@ public class ProtocolDefinition {
}
/** A set of all the visible extension URIs. */
private static final ImmutableSet<String> visibleServiceExtensionUris =
EnumSet.allOf(ServiceExtension.class)
.stream()
.filter(ServiceExtension::getVisible)
.map(ServiceExtension::getUri)
.collect(toImmutableSet());
// TODO(gbrodman): make this final when we can actually remove the old fee extensions and aren't
// relying on switching by environment
@NonFinalForTesting private static ImmutableSet<String> visibleServiceExtensionUris;
static {
reloadServiceExtensionUris();
}
/** Return the set of all visible service extension URIs. */
public static ImmutableSet<String> getVisibleServiceExtensionUris() {
return visibleServiceExtensionUris;
}
@VisibleForTesting
public static void reloadServiceExtensionUris() {
visibleServiceExtensionUris =
EnumSet.allOf(ServiceExtension.class).stream()
.filter(ServiceExtension::isVisible)
.map(ServiceExtension::getUri)
.collect(toImmutableSet());
}
}

View File

@@ -21,6 +21,7 @@ import static google.registry.util.DateTimeUtils.isBeforeOrAt;
import com.google.common.base.Supplier;
import com.google.common.collect.ImmutableMap;
import google.registry.model.ImmutableObject;
import google.registry.tmch.RstTmchUtils;
import jakarta.persistence.CollectionTable;
import jakarta.persistence.Column;
import jakarta.persistence.ElementCollection;
@@ -71,6 +72,11 @@ public class SignedMarkRevocationList extends ImmutableObject {
return CACHE.get();
}
// TODO(b/412715713): remove the tld parameter when RST completes.
public static SignedMarkRevocationList get(String tld) {
return RstTmchUtils.getSmdrList(tld).orElseGet(SignedMarkRevocationList::get);
}
/** Create a new {@link SignedMarkRevocationList} without saving it. */
public static SignedMarkRevocationList create(
DateTime creationTime, ImmutableMap<String, DateTime> revokes) {

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

@@ -22,6 +22,7 @@ import com.github.benmanes.caffeine.cache.LoadingCache;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.ImmutableMap;
import google.registry.model.CacheUtils;
import google.registry.tmch.RstTmchUtils;
import java.time.Duration;
import java.util.Optional;
@@ -72,6 +73,11 @@ public class ClaimsListDao {
return CACHE.get(ClaimsListDao.class);
}
// TODO(b/412715713): remove the tld parameter when RST completes.
public static ClaimsList get(String tld) {
return RstTmchUtils.getClaimsList(tld).orElseGet(ClaimsListDao::get);
}
/**
* Returns the most recent revision of the {@link ClaimsList} in SQL or an empty list if it
* doesn't exist.

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;
@@ -61,6 +62,9 @@ import google.registry.module.ReadinessProbeAction.ReadinessProbeActionFrontend;
import google.registry.module.ReadinessProbeAction.ReadinessProbeActionPubApi;
import google.registry.module.ReadinessProbeAction.ReadinessProbeConsoleAction;
import google.registry.monitoring.whitebox.WhiteboxModule;
import google.registry.mosapi.GetServiceStateAction;
import google.registry.mosapi.TriggerServiceStateAction;
import google.registry.mosapi.module.MosApiRequestModule;
import google.registry.rdap.RdapAutnumAction;
import google.registry.rdap.RdapDomainAction;
import google.registry.rdap.RdapDomainSearchAction;
@@ -150,6 +154,7 @@ import google.registry.ui.server.console.settings.SecurityAction;
EppToolModule.class,
IcannReportingModule.class,
LoadTestModule.class,
MosApiRequestModule.class,
RdapModule.class,
RdeModule.class,
ReportingModule.class,
@@ -171,6 +176,8 @@ interface RequestComponent {
BsaValidateAction bsaValidateAction();
BulkDomainTransferAction bulkDomainTransferAction();
CannedScriptExecutionAction cannedScriptExecutionAction();
CheckApiAction checkApiAction();
@@ -229,6 +236,8 @@ interface RequestComponent {
GenerateZoneFilesAction generateZoneFilesAction();
GetServiceStateAction getServiceStateAction();
IcannReportingStagingAction icannReportingStagingAction();
IcannReportingUploadAction icannReportingUploadAction();
@@ -331,6 +340,8 @@ interface RequestComponent {
TmchSmdrlAction tmchSmdrlAction();
TriggerServiceStateAction triggerServiceStateAction();
UpdateRegistrarRdapBaseUrlsAction updateRegistrarRdapBaseUrlsAction();
UpdateUserGroupAction updateUserGroupAction();

View File

@@ -0,0 +1,68 @@
// 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 com.google.common.net.MediaType;
import com.google.gson.Gson;
import google.registry.request.Action;
import google.registry.request.HttpException.ServiceUnavailableException;
import google.registry.request.Parameter;
import google.registry.request.Response;
import google.registry.request.auth.Auth;
import jakarta.inject.Inject;
import java.util.Optional;
/** An action that returns the current MoSAPI service state for a given TLD or all TLDs. */
@Action(
service = Action.Service.BACKEND,
path = GetServiceStateAction.PATH,
method = Action.Method.GET,
auth = Auth.AUTH_ADMIN)
public class GetServiceStateAction implements Runnable {
public static final String PATH = "/_dr/mosapi/getServiceState";
public static final String TLD_PARAM = "tld";
private final MosApiStateService stateService;
private final Response response;
private final Gson gson;
private final Optional<String> tld;
@Inject
public GetServiceStateAction(
MosApiStateService stateService,
Response response,
Gson gson,
@Parameter(TLD_PARAM) Optional<String> tld) {
this.stateService = stateService;
this.response = response;
this.gson = gson;
this.tld = tld;
}
@Override
public void run() {
response.setContentType(MediaType.JSON_UTF_8);
try {
if (tld.isPresent()) {
response.setPayload(gson.toJson(stateService.getServiceStateSummary(tld.get())));
} else {
response.setPayload(gson.toJson(stateService.getAllServiceStateSummaries()));
}
} catch (MosApiException e) {
throw new ServiceUnavailableException("Error fetching MoSAPI service state.");
}
}
}

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,26 @@
// 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 com.google.gson.annotations.Expose;
/**
* 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(
@Expose String resultCode, @Expose String message, @Expose String description) {}

View File

@@ -0,0 +1,114 @@
// 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 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 MosApiException(String message) {
super(message);
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,34 @@
// Copyright 2026 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 com.google.common.flogger.FluentLogger;
import google.registry.mosapi.MosApiModels.TldServiceState;
import jakarta.inject.Inject;
import java.util.List;
/** Metrics Exporter for MoSAPI. */
public class MosApiMetrics {
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
@Inject
public MosApiMetrics() {}
public void recordStates(List<TldServiceState> states) {
// b/467541269: Logic to push status to Cloud Monitoring goes here
logger.atInfo().log("MoSAPI record metrics logic will be implemented from here");
}
}

View File

@@ -0,0 +1,122 @@
// 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 google.registry.util.CollectionUtils.nullToEmptyImmutableCopy;
import com.google.gson.annotations.Expose;
import com.google.gson.annotations.SerializedName;
import java.util.List;
import java.util.Map;
import javax.annotation.Nullable;
/** Data models for ICANN MoSAPI. */
public final class MosApiModels {
private MosApiModels() {}
/**
* A wrapper response containing the state summaries of all monitored services.
*
* <p>This corresponds to the collection of service statuses returned when monitoring the state of
* a TLD
*
* @see <a href="https://www.icann.org/mosapi-specification.pdf">ICANN MoSAPI Specification,
* Section 5.1</a>
*/
public record AllServicesStateResponse(
// A list of state summaries for each monitored service (e.g. DNS, RDDS, etc.)
@Expose List<ServiceStateSummary> serviceStates) {
public AllServicesStateResponse {
serviceStates = nullToEmptyImmutableCopy(serviceStates);
}
}
/**
* A summary of a service incident.
*
* @see <a href="https://www.icann.org/mosapi-specification.pdf">ICANN MoSAPI Specification,
* Section 5.1</a>
*/
public record IncidentSummary(
@Expose String incidentID,
@Expose long startTime,
@Expose boolean falsePositive,
@Expose String state,
@Expose @Nullable Long endTime) {}
/**
* A curated summary of the service state for a TLD.
*
* <p>This class aggregates the high-level status of a TLD and details of any active incidents
* affecting specific services (like DNS or RDDS), based on the data structures defined in the
* MoSAPI specification.
*
* @see <a href="https://www.icann.org/mosapi-specification.pdf">ICANN MoSAPI Specification,
* Section 5.1</a>
*/
public record ServiceStateSummary(
@Expose String tld,
@Expose String overallStatus,
@Expose List<ServiceStatus> activeIncidents) {
public ServiceStateSummary {
activeIncidents = nullToEmptyImmutableCopy(activeIncidents);
}
}
/** Represents the status of a single monitored service. */
public record ServiceStatus(
/**
* A JSON string that contains the status of the Service as seen from the monitoring system.
* Possible values include "Up", "Down", "Disabled", "UP-inconclusive-no-data", etc.
*/
@Expose String status,
// A JSON number that contains the current percentage of the Emergency Threshold
// of the Service. A value of "0" specifies that there are no Incidents
// affecting the threshold.
@Expose double emergencyThreshold,
@Expose List<IncidentSummary> incidents) {
public ServiceStatus {
incidents = nullToEmptyImmutableCopy(incidents);
}
}
/**
* Represents the overall health of all monitored services for a TLD.
*
* @see <a href="https://www.icann.org/mosapi-specification.pdf">ICANN MoSAPI Specification,
* Section 5.1</a>
*/
public record TldServiceState(
@Expose String tld,
long lastUpdateApiDatabase,
// A JSON string that contains the status of the TLD as seen from the monitoring system
@Expose String status,
// A JSON object containing detailed information for each potential monitored service (i.e.,
// DNS,
// RDDS, EPP, DNSSEC, RDAP).
@Expose @SerializedName("testedServices") Map<String, ServiceStatus> serviceStatuses) {
public TldServiceState {
serviceStatuses = nullToEmptyImmutableCopy(serviceStatuses);
}
}
}

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,155 @@
// 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.collect.ImmutableList.toImmutableList;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.common.flogger.FluentLogger;
import google.registry.config.RegistryConfig.Config;
import google.registry.mosapi.MosApiModels.AllServicesStateResponse;
import google.registry.mosapi.MosApiModels.ServiceStateSummary;
import google.registry.mosapi.MosApiModels.ServiceStatus;
import google.registry.mosapi.MosApiModels.TldServiceState;
import jakarta.inject.Inject;
import jakarta.inject.Named;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutorService;
import java.util.stream.Collectors;
/** A service that provides business logic for interacting with MoSAPI Service State. */
public class MosApiStateService {
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
private final ServiceMonitoringClient serviceMonitoringClient;
private final ExecutorService tldExecutor;
private final ImmutableSet<String> tlds;
private final MosApiMetrics mosApiMetrics;
private static final String DOWN_STATUS = "Down";
private static final String FETCH_ERROR_STATUS = "ERROR";
@Inject
public MosApiStateService(
ServiceMonitoringClient serviceMonitoringClient,
MosApiMetrics mosApiMetrics,
@Config("mosapiTlds") ImmutableSet<String> tlds,
@Named("mosapiTldExecutor") ExecutorService tldExecutor) {
this.serviceMonitoringClient = serviceMonitoringClient;
this.mosApiMetrics = mosApiMetrics;
this.tlds = tlds;
this.tldExecutor = tldExecutor;
}
/** Fetches and transforms the service state for a given TLD into a summary. */
public ServiceStateSummary getServiceStateSummary(String tld) throws MosApiException {
TldServiceState rawState = serviceMonitoringClient.getTldServiceState(tld);
return transformToSummary(rawState);
}
/** Fetches and transforms the service state for all configured TLDs. */
public AllServicesStateResponse getAllServiceStateSummaries() {
ImmutableList<CompletableFuture<ServiceStateSummary>> futures =
tlds.stream()
.map(
tld ->
CompletableFuture.supplyAsync(
() -> {
try {
return getServiceStateSummary(tld);
} catch (MosApiException e) {
logger.atWarning().withCause(e).log(
"Failed to get service state for TLD %s.", tld);
// we don't want to throw exception if fetch failed
return new ServiceStateSummary(tld, FETCH_ERROR_STATUS, null);
}
},
tldExecutor))
.collect(ImmutableList.toImmutableList());
ImmutableList<ServiceStateSummary> summaries =
futures.stream()
.map(CompletableFuture::join) // Waits for all tasks to complete
.collect(toImmutableList());
return new AllServicesStateResponse(summaries);
}
private ServiceStateSummary transformToSummary(TldServiceState rawState) {
ImmutableList<ServiceStatus> activeIncidents = ImmutableList.of();
if (DOWN_STATUS.equalsIgnoreCase(rawState.status())) {
activeIncidents =
rawState.serviceStatuses().entrySet().stream()
.filter(
entry -> {
ServiceStatus serviceStatus = entry.getValue();
return serviceStatus.incidents() != null
&& !serviceStatus.incidents().isEmpty();
})
.map(
entry ->
new ServiceStatus(
// key is the service name
entry.getKey(),
entry.getValue().emergencyThreshold(),
entry.getValue().incidents()))
.collect(toImmutableList());
}
return new ServiceStateSummary(rawState.tld(), rawState.status(), activeIncidents);
}
/** Triggers monitoring exposure for all configured TLDs. */
public void triggerMetricsForAllServiceStateSummaries() {
ImmutableList<CompletableFuture<TldServiceState>> futures =
tlds.stream()
.map(
tld ->
CompletableFuture.supplyAsync(
() -> {
try {
return serviceMonitoringClient.getTldServiceState(tld);
} catch (MosApiException e) {
// Log the error but don't rethrow as RuntimeException
logger.atWarning().withCause(e).log(
"Failed to fetch state for TLD: %s", tld);
return null; // Return null so the stream keeps moving
}
},
tldExecutor))
.collect(toImmutableList());
List<TldServiceState> allStates =
futures.stream()
.map(CompletableFuture::join)
.filter(Objects::nonNull)
.collect(Collectors.toList());
if (!allStates.isEmpty()) {
try {
logger.atInfo().log("Triggering MoSAPI status to cloud monitoring for all TLDs.");
mosApiMetrics.recordStates(allStates);
} catch (Exception e) {
logger.atSevere().withCause(e).log("Failed to submit MoSAPI metrics batch.");
}
} else {
logger.atWarning().log("No successful TLD states fetched; skipping metrics push.");
}
}
}

View File

@@ -0,0 +1,80 @@
// 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 com.google.common.base.Throwables;
import com.google.gson.Gson;
import com.google.gson.JsonParseException;
import google.registry.mosapi.MosApiModels.TldServiceState;
import jakarta.inject.Inject;
import java.io.IOException;
import java.util.Collections;
import okhttp3.Response;
import okhttp3.ResponseBody;
/** Facade for MoSAPI's service monitoring endpoints. */
public class ServiceMonitoringClient {
private static final String MONITORING_STATE_ENDPOINT = "v2/monitoring/state";
private final MosApiClient mosApiClient;
private final Gson gson;
@Inject
public ServiceMonitoringClient(MosApiClient mosApiClient, Gson gson) {
this.mosApiClient = mosApiClient;
this.gson = gson;
}
/**
* Fetches the current state of all monitored services for a given TLD.
*
* @see <a href="https://www.icann.org/mosapi-specification.pdf">ICANN MoSAPI Specification,
* Section 5.1</a>
*/
public TldServiceState getTldServiceState(String tld) throws MosApiException {
try (Response response =
mosApiClient.sendGetRequest(
tld, MONITORING_STATE_ENDPOINT, Collections.emptyMap(), Collections.emptyMap())) {
ResponseBody responseBody = response.body();
if (responseBody == null) {
throw new MosApiException(
String.format(
"MoSAPI Service Monitoring API " + "returned an empty body with status: %d",
response.code()));
}
String bodyString = responseBody.string();
if (!response.isSuccessful()) {
throw parseErrorResponse(response.code(), bodyString);
}
return gson.fromJson(bodyString, TldServiceState.class);
} catch (IOException | JsonParseException e) {
Throwables.throwIfInstanceOf(e, MosApiException.class);
// Catch Gson's runtime exceptions (parsing errors) and wrap them
throw new MosApiException("Failed to parse TLD service state response", e);
}
}
/** Parses an unsuccessful MoSAPI response into a domain-specific {@link MosApiException}. */
private MosApiException parseErrorResponse(int statusCode, String bodyString) {
try {
MosApiErrorResponse error = gson.fromJson(bodyString, MosApiErrorResponse.class);
return MosApiException.create(error);
} catch (JsonParseException e) {
return new MosApiException(
String.format("MoSAPI json parsing error (%d): %s", statusCode, bodyString), e);
}
}
}

View File

@@ -0,0 +1,60 @@
// Copyright 2026 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 com.google.common.flogger.FluentLogger;
import com.google.common.net.MediaType;
import google.registry.request.Action;
import google.registry.request.HttpException.InternalServerErrorException;
import google.registry.request.Response;
import google.registry.request.auth.Auth;
import jakarta.inject.Inject;
/**
* An action that triggers Metrics action for the current MoSAPI service state result for all TLDs.
*/
@Action(
service = Action.Service.BACKEND,
path = TriggerServiceStateAction.PATH,
method = Action.Method.GET,
auth = Auth.AUTH_ADMIN)
public class TriggerServiceStateAction implements Runnable {
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
public static final String PATH = "/_dr/task/triggerMosApiServiceState";
private final MosApiStateService stateService;
private final Response response;
@Inject
public TriggerServiceStateAction(MosApiStateService stateService, Response response) {
this.stateService = stateService;
this.response = response;
}
@Override
public void run() {
response.setContentType(MediaType.PLAIN_TEXT_UTF_8);
try {
logger.atInfo().log("Beginning to trigger MoSAPI metrics for all TLDs.");
stateService.triggerMetricsForAllServiceStateSummaries();
response.setStatus(200);
response.setPayload("MoSAPI metrics triggered successfully for all TLDs.");
} catch (Exception e) {
logger.atSevere().withCause(e).log("Error triggering MoSAPI metrics.");
throw new InternalServerErrorException("Failed to process MoSAPI metrics.");
}
}
}

View File

@@ -0,0 +1,206 @@
// 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 java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
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();
}
/**
* Provides a fixed thread pool for parallel TLD processing.
*
* <p>Strictly bound to 4 threads to comply with MoSAPI session limits (4 concurrent sessions per
* certificate). This is used by MosApiStateService to fetch data in parallel.
*
* @see <a href="https://www.icann.org/mosapi-specification.pdf">ICANN MoSAPI Specification,
* Section 12.3</a>
*/
@Provides
@Singleton
@Named("mosapiTldExecutor")
static ExecutorService provideMosapiTldExecutor(
@Config("mosapiTldThreadCnt") int threadPoolSize) {
return Executors.newFixedThreadPool(threadPoolSize);
}
}

View File

@@ -0,0 +1,33 @@
// 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 static google.registry.request.RequestParameters.extractOptionalParameter;
import dagger.Module;
import dagger.Provides;
import google.registry.request.Parameter;
import jakarta.servlet.http.HttpServletRequest;
import java.util.Optional;
/** Dagger module for MoSAPI requests. */
@Module
public final class MosApiRequestModule {
@Provides
@Parameter("tld")
static Optional<String> provideTld(HttpServletRequest req) {
return extractOptionalParameter(req, "tld");
}
}

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

@@ -19,7 +19,6 @@ import static java.nio.charset.StandardCharsets.UTF_8;
import com.google.common.flogger.FluentLogger;
import google.registry.model.ImmutableObject;
import google.registry.model.contact.Contact;
import google.registry.model.domain.Domain;
import google.registry.model.host.Host;
import google.registry.model.rde.RdeMode;
@@ -118,12 +117,6 @@ public final class RdeMarshaller implements Serializable {
}
}
/** Turns {@link Contact} object into an XML fragment. */
public DepositFragment marshalContact(Contact contact) {
return marshalResource(
RdeResourceType.CONTACT, contact, ContactToXjcConverter.convert(contact));
}
/** Turns {@link Domain} object into an XML fragment. */
public DepositFragment marshalDomain(Domain domain, RdeMode mode) {
return marshalResource(

View File

@@ -25,7 +25,6 @@ import java.util.EnumSet;
/** Types of objects that get embedded in an escrow deposit. */
public enum RdeResourceType {
CONTACT("urn:ietf:params:xml:ns:rdeContact-1.0", EnumSet.of(FULL)),
DOMAIN("urn:ietf:params:xml:ns:rdeDomain-1.0", EnumSet.of(FULL, THIN)),
HOST("urn:ietf:params:xml:ns:rdeHost-1.0", EnumSet.of(FULL)),
REGISTRAR("urn:ietf:params:xml:ns:rdeRegistrar-1.0", EnumSet.of(FULL, THIN)),

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

@@ -0,0 +1,120 @@
// 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.tmch;
import static com.google.common.base.Suppliers.memoize;
import static com.google.common.io.Resources.getResource;
import static com.google.common.io.Resources.readLines;
import static google.registry.tmch.RstTmchUtils.RstEnvironment.OTE;
import static google.registry.tmch.RstTmchUtils.RstEnvironment.PROD;
import static google.registry.util.RegistryEnvironment.SANDBOX;
import static java.nio.charset.StandardCharsets.UTF_8;
import com.google.common.base.Supplier;
import com.google.common.collect.ImmutableMap;
import com.google.common.flogger.FluentLogger;
import google.registry.model.smd.SignedMarkRevocationList;
import google.registry.model.tmch.ClaimsList;
import google.registry.util.RegistryEnvironment;
import java.io.IOException;
import java.net.URL;
import java.util.Locale;
import java.util.Optional;
/**
* Utilities supporting TMCH-related RST testing in the Sandbox environment.
*
* <p>For logistic reasons we must conduct RST testing in the Sandbox environments. RST tests
* require the use of special labels hosted on their website. To isolate these labels from regular
* customers conducting onboarding tests, we manually download the test files as resources, and
* serve them up only to RST TLDs.
*/
public class RstTmchUtils {
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
/**
* The RST environments.
*
* <p>We conduct both OTE and PROD RST tests in Sandbox.
*/
enum RstEnvironment {
OTE,
PROD
}
private static final ImmutableMap<RstEnvironment, Supplier<Optional<ClaimsList>>> CLAIMS_CACHE =
ImmutableMap.of(
OTE, memoize(() -> getClaimsList(OTE)), PROD, memoize(() -> getClaimsList(PROD)));
private static final ImmutableMap<RstEnvironment, Supplier<Optional<SignedMarkRevocationList>>>
SMDRL_CACHE =
ImmutableMap.of(
OTE, memoize(() -> getSmdrList(OTE)), PROD, memoize(() -> getSmdrList(PROD)));
/** Returns appropriate test labels if {@code tld} is for RST testing; otherwise returns empty. */
public static Optional<ClaimsList> getClaimsList(String tld) {
return getRstEnvironment(tld).map(CLAIMS_CACHE::get).flatMap(Supplier::get);
}
/** Returns appropriate test labels if {@code tld} is for RST testing; otherwise returns empty. */
public static Optional<SignedMarkRevocationList> getSmdrList(String tld) {
return getRstEnvironment(tld).map(SMDRL_CACHE::get).flatMap(Supplier::get);
}
static Optional<RstEnvironment> getRstEnvironment(String tld) {
if (!RegistryEnvironment.get().equals(SANDBOX)) {
return Optional.empty();
}
if (tld.startsWith("cc-rst-test-")) {
return Optional.of(OTE);
}
if (tld.startsWith("zz--")) {
return Optional.of(PROD);
}
return Optional.empty();
}
private static Optional<ClaimsList> getClaimsList(RstEnvironment rstEnvironment) {
if (!RegistryEnvironment.get().equals(SANDBOX)) {
return Optional.empty();
}
String resourceName = rstEnvironment.name().toLowerCase(Locale.ROOT) + ".rst.dnl.csv";
URL resource = getResource(RstTmchUtils.class, resourceName);
try {
return Optional.of(ClaimsListParser.parse(readLines(resource, UTF_8)));
} catch (IOException e) {
// Do not throw.
logger.atSevere().withCause(e).log(
"Could not load Claims list %s for %s in Sandbox.", resourceName, rstEnvironment);
return Optional.empty();
}
}
private static Optional<SignedMarkRevocationList> getSmdrList(RstEnvironment rstEnvironment) {
if (!RegistryEnvironment.get().equals(SANDBOX)) {
return Optional.empty();
}
String resourceName = rstEnvironment.name().toLowerCase(Locale.ROOT) + ".rst.smdrl.csv";
URL resource = getResource(RstTmchUtils.class, resourceName);
try {
return Optional.of(SmdrlCsvParser.parse(readLines(resource, UTF_8)));
} catch (IOException e) {
// Do not throw.
logger.atSevere().withCause(e).log(
"Could not load SMDR list %s for %s in Sandbox.", resourceName, rstEnvironment);
return Optional.empty();
}
}
}

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

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

View File

@@ -38,7 +38,6 @@ import java.io.StringWriter;
import java.io.Writer;
import java.nio.charset.Charset;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import javax.annotation.Nullable;
import javax.xml.XMLConstants;
@@ -82,7 +81,7 @@ public class XmlTransformer {
* @param schemaFilenames schema files, used only for validating, and relative to this package.
* @param recognizedClasses the classes that can be used to marshal to and from
*/
public XmlTransformer(List<String> schemaFilenames, Class<?>... recognizedClasses) {
public XmlTransformer(ImmutableList<String> schemaFilenames, Class<?>... recognizedClasses) {
try {
this.jaxbContext = JAXBContext.newInstance(recognizedClasses);
this.schema = loadXmlSchemas(schemaFilenames);
@@ -251,7 +250,7 @@ public class XmlTransformer {
}
/** Creates a single {@link Schema} from multiple {@code .xsd} files. */
public static Schema loadXmlSchemas(List<String> schemaFilenames) {
public static Schema loadXmlSchemas(ImmutableList<String> schemaFilenames) {
try (Closer closer = Closer.create()) {
StreamSource[] sources = new StreamSource[schemaFilenames.size()];
for (int i = 0; i < schemaFilenames.size(); ++i) {

View File

@@ -23,7 +23,7 @@
<element name="renew" type="fee:transformCommandType" />
<element name="renData" type="fee:transformResultType" />
<element name="transfer" type="fee:transformCommandType" />
<element name="trnData" type="fee:transferResultType" />
<element name="trnData" type="fee:transformResultType" />
<element name="update" type="fee:transformCommandType" />
<element name="updData" type="fee:transformResultType" />
<element name="delData" type="fee:transformResultType" />
@@ -33,32 +33,24 @@
<sequence>
<element name="currency" type="fee:currencyType"
minOccurs="0" />
<element name="command" type="fee:commandCheckType"
maxOccurs="unbounded" />
<element name="command" type="fee:commandType"
minOccurs="1" maxOccurs="unbounded" />
</sequence>
</complexType>
<complexType name="commandCheckType">
<sequence>
<element name="period"
type="domain:periodType"
minOccurs="0" />
<element name="class"
type="token"
minOccurs="0" />
<element name="date"
type="dateTime"
minOccurs="0" />
</sequence>
<attribute name="name" type="fee:commandTypeValue" />
<attribute name="phase" type="token" />
<attribute name="subphase" type="token" />
<complexType name="objectIdentifierType">
<simpleContent>
<extension base="eppcom:labelType">
<attribute name="element"
type="NMTOKEN" default="name" />
</extension>
</simpleContent>
</complexType>
<!-- server <check> result -->
<complexType name="chkDataType">
<sequence>
<element name="currency" type="fee:currencyType" minOccurs="0"/>
<element name="currency" type="fee:currencyType" />
<element name="cd" type="fee:objectCDType"
maxOccurs="unbounded" />
</sequence>
@@ -66,47 +58,13 @@
<complexType name="objectCDType">
<sequence>
<element name="object">
<complexType>
<sequence>
<any namespace="##other" processContents="lax"/>
</sequence>
</complexType>
</element>
<element name="command"
type="fee:commandCDType"
maxOccurs="unbounded" />
</sequence>
</complexType>
<complexType name="commandCDType">
<sequence>
<element name="period"
type="domain:periodType"
minOccurs="0" maxOccurs="1" />
<element name="fee"
type="fee:feeType"
<element name="objID" type="fee:objectIdentifierType" />
<element name="class" type="token" minOccurs="0" />
<element name="command" type="fee:commandDataType"
minOccurs="0" maxOccurs="unbounded" />
<element name="credit"
type="fee:creditType"
minOccurs="0" maxOccurs="unbounded" />
<element name="class"
type="token"
minOccurs="0" />
<element name="reason"
type="token"
minOccurs="0" />
<element name="date"
type="dateTime"
minOccurs="0" />
<element name="notAfter"
type="dateTime"
minOccurs="0" />
<element name="reason" type="fee:reasonType" minOccurs="0" />
</sequence>
<attribute name="avail" type="boolean" default="1" />
<attribute name="name" type="fee:commandTypeValue" />
<attribute name="phase" type="token" />
<attribute name="subphase" type="token" />
</complexType>
<!-- general transform (create, renew, update, transfer) command-->
@@ -121,32 +79,15 @@
</sequence>
</complexType>
<!-- general transform (create, renew, update, delete) result -->
<!-- general transform (create, renew, update) result -->
<complexType name="transformResultType">
<sequence>
<element name="currency" type="fee:currencyType" />
<element name="fee" type="fee:feeType"
minOccurs="0" maxOccurs="unbounded" />
<element name="credit" type="fee:creditType"
minOccurs="0" maxOccurs="unbounded" />
<element name="balance" type="fee:balanceType"
<element name="currency" type="fee:currencyType"
minOccurs="0" />
<element name="creditLimit" type="fee:creditLimitType"
minOccurs="0" />
</sequence>
</complexType>
<!-- transfer result -->
<complexType name="transferResultType">
<sequence>
<element name="currency" type="fee:currencyType" />
<!-- only used op="query" responses -->
<element name="period" type="domain:periodType"
minOccurs="0" />
<element name="fee" type="fee:feeType"
maxOccurs="unbounded" />
minOccurs="0" maxOccurs="unbounded" />
<element name="credit" type="fee:creditType"
minOccurs="0" maxOccurs="unbounded" />
<element name="balance" type="fee:balanceType"
@@ -163,10 +104,50 @@
</restriction>
</simpleType>
<simpleType name="commandTypeValue">
<complexType name="commandType">
<sequence>
<element name="period" type="domain:periodType"
minOccurs="0" maxOccurs="1" />
</sequence>
<attribute name="name" type="fee:commandEnum" use="required"/>
<attribute name="customName" type="token"/>
<attribute name="phase" type="token" />
<attribute name="subphase" type="token" />
</complexType>
<complexType name="commandDataType">
<complexContent>
<extension base="fee:commandType">
<sequence>
<element name="fee" type="fee:feeType"
minOccurs="0" maxOccurs="unbounded" />
<element name="credit" type="fee:creditType"
minOccurs="0" maxOccurs="unbounded" />
<element name="reason" type="fee:reasonType"
minOccurs="0" />
</sequence>
<attribute name="standard" type="boolean" default="0" />
</extension>
</complexContent>
</complexType>
<complexType name="reasonType">
<simpleContent>
<extension base="token">
<attribute name="lang" type="language" default="en"/>
</extension>
</simpleContent>
</complexType>
<simpleType name="commandEnum">
<restriction base="token">
<minLength value="3"/>
<maxLength value="16"/>
<enumeration value="create"/>
<enumeration value="delete"/>
<enumeration value="renew"/>
<enumeration value="update"/>
<enumeration value="transfer"/>
<enumeration value="restore"/>
<enumeration value="custom"/>
</restriction>
</simpleType>
@@ -186,9 +167,10 @@
<simpleContent>
<extension base="fee:nonNegativeDecimal">
<attribute name="description"/>
<attribute name="lang" type="language" default="en"/>
<attribute name="refundable" type="boolean" />
<attribute name="grace-period" type="duration" />
<attribute name="applied" default="immediate">
<attribute name="applied">
<simpleType>
<restriction base="token">
<enumeration value="immediate" />
@@ -204,6 +186,7 @@
<simpleContent>
<extension base="fee:negativeDecimal">
<attribute name="description"/>
<attribute name="lang" type="language" default="en"/>
</extension>
</simpleContent>
</complexType>

View File

@@ -0,0 +1,10 @@
1,2024-09-13T02:21:12.0Z
DNL,lookup-key,insertion-datetime
test---validate,2024091300/6/a/b/arJyPPf2CK7f21bVGne0qMgW0000000001,2024-09-13T02:21:12.0Z
test--validate,2024091300/6/a/b/arJyPPf2CK7f21bVGne0qMgW0000000001,2024-09-13T02:21:12.0Z
test-and-validate,2024091300/6/a/b/arJyPPf2CK7f21bVGne0qMgW0000000001,2024-09-13T02:21:12.0Z
test-andvalidate,2024091300/6/a/b/arJyPPf2CK7f21bVGne0qMgW0000000001,2024-09-13T02:21:12.0Z
test-validate,2024091300/6/a/b/arJyPPf2CK7f21bVGne0qMgW0000000001,2024-09-13T02:21:12.0Z
testand-validate,2024091300/6/a/b/arJyPPf2CK7f21bVGne0qMgW0000000001,2024-09-13T02:21:12.0Z
testandvalidate,2024091300/6/a/b/arJyPPf2CK7f21bVGne0qMgW0000000001,2024-09-13T02:21:12.0Z
testvalidate,2024091300/6/a/b/arJyPPf2CK7f21bVGne0qMgW0000000001,2024-09-13T02:21:12.0Z
1 1,2024-09-13T02:21:12.0Z
2 DNL,lookup-key,insertion-datetime
3 test---validate,2024091300/6/a/b/arJyPPf2CK7f21bVGne0qMgW0000000001,2024-09-13T02:21:12.0Z
4 test--validate,2024091300/6/a/b/arJyPPf2CK7f21bVGne0qMgW0000000001,2024-09-13T02:21:12.0Z
5 test-and-validate,2024091300/6/a/b/arJyPPf2CK7f21bVGne0qMgW0000000001,2024-09-13T02:21:12.0Z
6 test-andvalidate,2024091300/6/a/b/arJyPPf2CK7f21bVGne0qMgW0000000001,2024-09-13T02:21:12.0Z
7 test-validate,2024091300/6/a/b/arJyPPf2CK7f21bVGne0qMgW0000000001,2024-09-13T02:21:12.0Z
8 testand-validate,2024091300/6/a/b/arJyPPf2CK7f21bVGne0qMgW0000000001,2024-09-13T02:21:12.0Z
9 testandvalidate,2024091300/6/a/b/arJyPPf2CK7f21bVGne0qMgW0000000001,2024-09-13T02:21:12.0Z
10 testvalidate,2024091300/6/a/b/arJyPPf2CK7f21bVGne0qMgW0000000001,2024-09-13T02:21:12.0Z

View File

@@ -0,0 +1,7 @@
1,2022-11-22T01:49:36.9Z
smd-id,insertion-datetime
0000001761385117375880-65535,2013-07-15T00:00:00.0Z
0000001751501056761969-65535,2017-07-26T10:12:41.9Z
000000541526299609231-65535,2018-05-14T17:52:23.7Z
000000541602140609520-65535,2020-10-08T07:07:25.0Z
000000541669081776937-65535,2022-11-22T01:49:36.9Z
1 1 2022-11-22T01:49:36.9Z
2 smd-id insertion-datetime
3 0000001761385117375880-65535 2013-07-15T00:00:00.0Z
4 0000001751501056761969-65535 2017-07-26T10:12:41.9Z
5 000000541526299609231-65535 2018-05-14T17:52:23.7Z
6 000000541602140609520-65535 2020-10-08T07:07:25.0Z
7 000000541669081776937-65535 2022-11-22T01:49:36.9Z

View File

@@ -0,0 +1,10 @@
1,2024-09-13T02:21:12.0Z
DNL,lookup-key,insertion-datetime
test---validate,2024091300/6/a/b/arJyPPf2CK7f21bVGne0qMgW0000000001,2024-09-13T02:21:12.0Z
test--validate,2024091300/6/a/b/arJyPPf2CK7f21bVGne0qMgW0000000001,2024-09-13T02:21:12.0Z
test-and-validate,2024091300/6/a/b/arJyPPf2CK7f21bVGne0qMgW0000000001,2024-09-13T02:21:12.0Z
test-andvalidate,2024091300/6/a/b/arJyPPf2CK7f21bVGne0qMgW0000000001,2024-09-13T02:21:12.0Z
test-validate,2024091300/6/a/b/arJyPPf2CK7f21bVGne0qMgW0000000001,2024-09-13T02:21:12.0Z
testand-validate,2024091300/6/a/b/arJyPPf2CK7f21bVGne0qMgW0000000001,2024-09-13T02:21:12.0Z
testandvalidate,2024091300/6/a/b/arJyPPf2CK7f21bVGne0qMgW0000000001,2024-09-13T02:21:12.0Z
testvalidate,2024091300/6/a/b/arJyPPf2CK7f21bVGne0qMgW0000000001,2024-09-13T02:21:12.0Z
1 1,2024-09-13T02:21:12.0Z
2 DNL,lookup-key,insertion-datetime
3 test---validate,2024091300/6/a/b/arJyPPf2CK7f21bVGne0qMgW0000000001,2024-09-13T02:21:12.0Z
4 test--validate,2024091300/6/a/b/arJyPPf2CK7f21bVGne0qMgW0000000001,2024-09-13T02:21:12.0Z
5 test-and-validate,2024091300/6/a/b/arJyPPf2CK7f21bVGne0qMgW0000000001,2024-09-13T02:21:12.0Z
6 test-andvalidate,2024091300/6/a/b/arJyPPf2CK7f21bVGne0qMgW0000000001,2024-09-13T02:21:12.0Z
7 test-validate,2024091300/6/a/b/arJyPPf2CK7f21bVGne0qMgW0000000001,2024-09-13T02:21:12.0Z
8 testand-validate,2024091300/6/a/b/arJyPPf2CK7f21bVGne0qMgW0000000001,2024-09-13T02:21:12.0Z
9 testandvalidate,2024091300/6/a/b/arJyPPf2CK7f21bVGne0qMgW0000000001,2024-09-13T02:21:12.0Z
10 testvalidate,2024091300/6/a/b/arJyPPf2CK7f21bVGne0qMgW0000000001,2024-09-13T02:21:12.0Z

View File

@@ -0,0 +1,7 @@
1,2022-11-22T01:49:36.9Z
smd-id,insertion-datetime
0000001761385117375880-65535,2013-07-15T00:00:00.0Z
0000001751501056761969-65535,2017-07-26T10:12:41.9Z
000000541526299609231-65535,2018-05-14T17:52:23.7Z
000000541602140609520-65535,2020-10-08T07:07:25.0Z
000000541669081776937-65535,2022-11-22T01:49:36.9Z
1 1 2022-11-22T01:49:36.9Z
2 smd-id insertion-datetime
3 0000001761385117375880-65535 2013-07-15T00:00:00.0Z
4 0000001751501056761969-65535 2017-07-26T10:12:41.9Z
5 000000541526299609231-65535 2018-05-14T17:52:23.7Z
6 000000541602140609520-65535 2020-10-08T07:07:25.0Z
7 000000541669081776937-65535 2022-11-22T01:49:36.9Z

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

@@ -25,13 +25,11 @@ import static google.registry.model.rde.RdeMode.THIN;
import static google.registry.persistence.transaction.JpaTransactionManagerExtension.makeRegistrar1;
import static google.registry.persistence.transaction.JpaTransactionManagerExtension.makeRegistrar2;
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
import static google.registry.rde.RdeResourceType.CONTACT;
import static google.registry.rde.RdeResourceType.DOMAIN;
import static google.registry.rde.RdeResourceType.HOST;
import static google.registry.rde.RdeResourceType.REGISTRAR;
import static google.registry.testing.DatabaseHelper.createTld;
import static google.registry.testing.DatabaseHelper.newDomain;
import static google.registry.testing.DatabaseHelper.persistActiveContact;
import static google.registry.testing.DatabaseHelper.persistActiveDomain;
import static google.registry.testing.DatabaseHelper.persistActiveHost;
import static google.registry.testing.DatabaseHelper.persistEppResource;
@@ -54,10 +52,6 @@ import google.registry.gcs.GcsUtils;
import google.registry.keyring.api.PgpHelper;
import google.registry.model.common.Cursor;
import google.registry.model.common.Cursor.CursorType;
import google.registry.model.contact.Contact;
import google.registry.model.contact.ContactBase;
import google.registry.model.contact.ContactHistory;
import google.registry.model.domain.DesignatedContact;
import google.registry.model.domain.Domain;
import google.registry.model.domain.DomainBase;
import google.registry.model.domain.DomainHistory;
@@ -114,8 +108,6 @@ public class RdePipelineTest {
private static final String DOMAIN_NAME_PATTERN = "<rdeDomain:name>(.*)</rdeDomain:name>";
private static final String CONTACT_ID_PATTERN = "<rdeContact:id>(.*)</rdeContact:id>";
private static final String HOST_NAME_PATTERN = "<rdeHost:name>(.*)</rdeHost:name>";
// This is the default creation time for test data.
@@ -139,7 +131,6 @@ public class RdePipelineTest {
ImmutableList.of(
DepositFragment.create(DOMAIN, "<rdeDomain:domain/>\n", ""),
DepositFragment.create(REGISTRAR, "<rdeRegistrar:registrar/>\n", ""),
DepositFragment.create(CONTACT, "<rdeContact:contact/>\n", ""),
DepositFragment.create(HOST, "<rdeHost:host/>\n", ""));
private final GcsUtils gcsUtils = new GcsUtils(LocalStorageHelper.getOptions());
@@ -165,21 +156,6 @@ public class RdePipelineTest {
private RdePipeline rdePipeline;
private ContactHistory persistContactHistory(ContactBase contact) {
return persistResource(
new ContactHistory.Builder()
.setType(HistoryEntry.Type.HOST_CREATE)
.setXmlBytes("<xml></xml>".getBytes(UTF_8))
.setModificationTime(clock.nowUtc())
.setRegistrarId("TheRegistrar")
.setTrid(Trid.create("ABC-123", "server-trid"))
.setBySuperuser(false)
.setReason("reason")
.setRequestedByRegistrar(true)
.setContact(contact)
.build());
}
private DomainHistory persistDomainHistory(DomainBase domain) {
DomainTransactionRecord transactionRecord =
new DomainTransactionRecord.Builder()
@@ -254,20 +230,18 @@ public class RdePipelineTest {
RdeRevision.saveRevision("soy", now, FULL, 0);
});
// This contact is never referenced.
persistContactHistory(persistActiveContact("contactX"));
Contact contact1 = persistActiveContact("contact1234");
persistContactHistory(contact1);
Contact contact2 = persistActiveContact("contact456");
persistContactHistory(contact2);
// This host is never referenced.
persistHostHistory(persistActiveHost("ns0.domain.tld"));
Host host1 = persistActiveHost("ns1.external.tld");
persistHostHistory(host1);
Domain helloDomain =
persistEppResource(
newDomain("hello.soy", contact1).asBuilder().addNameserver(host1.createVKey()).build());
newDomain("hello.soy")
.asBuilder()
.addNameserver(host1.createVKey())
.setRegistrant(Optional.empty())
.setContacts(ImmutableSet.of())
.build());
persistDomainHistory(helloDomain);
persistHostHistory(persistActiveHost("not-used-subordinate.hello.soy"));
Host host2 = persistActiveHost("ns1.hello.soy");
@@ -276,14 +250,15 @@ public class RdePipelineTest {
// This domain has no registrant.
Domain kittyDomain =
persistEppResource(
newDomain("kitty.fun", contact2)
newDomain("kitty.fun")
.asBuilder()
.addNameservers(ImmutableSet.of(host1.createVKey(), host2.createVKey()))
.setRegistrant(Optional.empty())
.setContacts(ImmutableSet.of())
.build());
persistDomainHistory(kittyDomain);
// Should not appear because the TLD is not included in a pending deposit.
persistDomainHistory(persistEppResource(newDomain("lol.cat", contact1)));
persistDomainHistory(persistEppResource(newDomain("lol.cat")));
// To be deleted.
Domain deletedDomain = persistActiveDomain("deleted.soy");
persistDomainHistory(deletedDomain);
@@ -293,8 +268,7 @@ public class RdePipelineTest {
persistDomainHistory(deletedDomain.asBuilder().setDeletionTime(clock.nowUtc()).build());
kittyDomain = kittyDomain.asBuilder().setDomainName("cat.fun").build();
persistDomainHistory(kittyDomain);
Contact contact3 = persistActiveContact("contact789");
persistContactHistory(contact3);
// This is a subordinate domain in TLD .cat, which is not included in any pending deposit. But
// it should still be included as a subordinate host in the pendign deposit for .soy.
Host host3 = persistActiveHost("ns1.lol.cat");
@@ -302,17 +276,8 @@ public class RdePipelineTest {
persistDomainHistory(
helloDomain
.asBuilder()
.removeContacts(
helloDomain.getContacts().stream()
.filter(dc -> dc.getType() == DesignatedContact.Type.ADMIN)
.collect(toImmutableSet()))
.addContacts(
ImmutableSet.of(
DesignatedContact.create(DesignatedContact.Type.ADMIN, contact3.createVKey())))
.addNameserver(host3.createVKey())
.build());
// contact456 is renamed to contactABC.
persistContactHistory(contact2.asBuilder().setContactId("contactABC").build());
// ns1.hello.soy is renamed to ns2.hello.soy
persistHostHistory(host2.asBuilder().setHostName("ns2.hello.soy").build());
@@ -320,18 +285,11 @@ public class RdePipelineTest {
// resulting deposit fragments.
clock.advanceBy(Duration.standardDays(2));
persistDomainHistory(kittyDomain.asBuilder().setDeletionTime(clock.nowUtc()).build());
Contact futureContact = persistActiveContact("future-contact");
persistContactHistory(futureContact);
Host futureHost = persistActiveHost("ns1.future.tld");
persistHostHistory(futureHost);
persistDomainHistory(
persistEppResource(
newDomain("future.soy", futureContact)
.asBuilder()
.setNameservers(futureHost.createVKey())
.build()));
// contactABC is renamed to contactXYZ.
persistContactHistory(contact2.asBuilder().setContactId("contactXYZ").build());
newDomain("future.soy").asBuilder().setNameservers(futureHost.createVKey()).build()));
// ns2.hello.soy is renamed to ns3.hello.soy
persistHostHistory(host2.asBuilder().setHostName("ns3.hello.soy").build());
@@ -390,11 +348,9 @@ public class RdePipelineTest {
"""
<rdeDomain:domain>
<rdeDomain:name>cat.fun</rdeDomain:name>
<rdeDomain:roid>15-FUN</rdeDomain:roid>
<rdeDomain:roid>10-FUN</rdeDomain:roid>
<rdeDomain:uName>cat.fun</rdeDomain:uName>
<rdeDomain:status s="ok"/>
<rdeDomain:contact type="admin">contact456</rdeDomain:contact>
<rdeDomain:contact type="tech">contact456</rdeDomain:contact>
<rdeDomain:ns>
<domain:hostObj>ns1.external.tld</domain:hostObj>
<domain:hostObj>ns1.hello.soy</domain:hostObj>
@@ -407,14 +363,7 @@ public class RdePipelineTest {
""");
}
if (kv.getKey().mode().equals(FULL)) {
// Contact fragments for hello.soy.
if ("soy".equals(kv.getKey().tld())) {
assertThat(
getFragmentForType(kv, CONTACT)
.map(getXmlElement(CONTACT_ID_PATTERN))
.collect(toImmutableSet()))
.containsExactly("contact1234", "contact789");
// Host fragments for hello.soy.
assertThat(
getFragmentForType(kv, HOST)
@@ -428,12 +377,9 @@ public class RdePipelineTest {
"""
<rdeDomain:domain>
<rdeDomain:name>hello.soy</rdeDomain:name>
<rdeDomain:roid>E-SOY</rdeDomain:roid>
<rdeDomain:roid>8-SOY</rdeDomain:roid>
<rdeDomain:uName>hello.soy</rdeDomain:uName>
<rdeDomain:status s="ok"/>
<rdeDomain:registrant>contact1234</rdeDomain:registrant>
<rdeDomain:contact type="admin">contact789</rdeDomain:contact>
<rdeDomain:contact type="tech">contact1234</rdeDomain:contact>
<rdeDomain:ns>
<domain:hostObj>ns1.external.tld</domain:hostObj>
<domain:hostObj>ns1.lol.cat</domain:hostObj>
@@ -445,13 +391,6 @@ public class RdePipelineTest {
</rdeDomain:domain>\
""");
} else {
// Contact fragments for cat.fun.
assertThat(
getFragmentForType(kv, CONTACT)
.map(getXmlElement(CONTACT_ID_PATTERN))
.collect(toImmutableSet()))
.containsExactly("contactABC");
// Host fragments for cat.soy.
assertThat(
getFragmentForType(kv, HOST)
@@ -460,22 +399,19 @@ public class RdePipelineTest {
.containsExactly("ns1.external.tld", "ns2.hello.soy");
}
} else {
// BRDA does not contain contact or hosts.
// BRDA does not contain hosts.
assertThat(
Streams.stream(kv.getValue())
.anyMatch(
fragment ->
fragment.type().equals(CONTACT)
|| fragment.type().equals(HOST)))
.anyMatch(fragment -> fragment.type().equals(HOST)))
.isFalse();
// Domain fragments for hello.soy: Note that this contains no contact info.
// Domain fragments for hello.soy.
assertThat(domainFrags.stream().findFirst().get().xml().strip())
.isEqualTo(
"""
<rdeDomain:domain>
<rdeDomain:name>hello.soy</rdeDomain:name>
<rdeDomain:roid>E-SOY</rdeDomain:roid>
<rdeDomain:roid>8-SOY</rdeDomain:roid>
<rdeDomain:uName>hello.soy</rdeDomain:uName>
<rdeDomain:status s="ok"/>
<rdeDomain:ns>

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

@@ -14,7 +14,6 @@
package google.registry.flows;
import static org.joda.time.DateTimeZone.UTC;
import static org.joda.time.format.ISODateTimeFormat.dateTimeNoMillis;
import com.google.common.collect.ImmutableMap;
@@ -26,7 +25,7 @@ class EppLoggedOutTest extends EppTestCase {
@Test
void testHello() throws Exception {
DateTime now = DateTime.now(UTC);
DateTime now = clock.nowUtc();
assertThatCommand("hello.xml", null)
.atTime(now)
.hasResponse("greeting.xml", ImmutableMap.of("DATE", now.toString(dateTimeNoMillis())));

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

@@ -36,6 +36,7 @@ import google.registry.flows.EppTestComponent.FakesAndMocksModule;
import google.registry.flows.picker.FlowPicker;
import google.registry.model.billing.BillingBase;
import google.registry.model.domain.GracePeriod;
import google.registry.model.eppcommon.EppXmlTransformer;
import google.registry.model.eppcommon.ProtocolDefinition;
import google.registry.model.eppinput.EppInput;
import google.registry.model.eppoutput.EppOutput;
@@ -134,28 +135,6 @@ public abstract class FlowTestCase<F extends Flow> {
return TestDataHelper.loadFile(getClass(), filename, substitutions);
}
/**
* Converts an input or response EPP message with draft fee extension v12 to std v1.
*
* <p>There is no practical changes between draft v12 and the v1 standard. This method allows us
* to reuse v12 test data.
*/
protected String loadFeeV12FileAsStdV1(String filename) {
String content = loadFile(filename);
return content.replace("urn:ietf:params:xml:ns:fee-0.12", "urn:ietf:params:xml:ns:epp:fee-1.0");
}
/**
* Converts an input or response EPP message with draft fee extension v12 to std v1.
*
* <p>There is no practical changes between draft v12 and the v1 standard. This method allows us
* to reuse v12 test data.
*/
protected String loadFeeV12FileAsStdV1(String filename, Map<String, String> substitutions) {
String content = loadFile(filename, substitutions);
return content.replace("urn:ietf:params:xml:ns:fee-0.12", "urn:ietf:params:xml:ns:epp:fee-1.0");
}
@Nullable
protected String getClientTrid() throws Exception {
return eppLoader.getEpp().getCommandWrapper().getClTrid().orElse(null);
@@ -289,6 +268,8 @@ public abstract class FlowTestCase<F extends Flow> {
if (output.isResponse()) {
assertThat(output.isSuccess()).isTrue();
}
// Verify that expected xml is syntatically correct.
EppXmlTransformer.validateOutput(xml);
try {
assertXmlEquals(
xml, new String(marshal(output, ValidationMode.STRICT), UTF_8), ignoredPathsPlusTrid);

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

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