1
0
mirror of https://github.com/google/nomulus synced 2026-01-18 03:33:04 +00:00

Compare commits

...

30 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
338 changed files with 11524 additions and 7378 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

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

@@ -1462,6 +1462,12 @@ public final class RegistryConfig {
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

@@ -272,5 +272,6 @@ public class RegistryConfigSettings {
public String entityType;
public List<String> tlds;
public List<String> services;
public int tldThreadCnt;
}
}

View File

@@ -642,4 +642,8 @@ mosapi:
- "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

@@ -280,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,
@@ -312,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

@@ -218,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();
}
@@ -976,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));
}
@@ -1240,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");
}

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;
@@ -75,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;
@@ -245,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.

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

@@ -62,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;
@@ -151,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,
@@ -232,6 +236,8 @@ interface RequestComponent {
GenerateZoneFilesAction generateZoneFilesAction();
GetServiceStateAction getServiceStateAction();
IcannReportingStagingAction icannReportingStagingAction();
IcannReportingUploadAction icannReportingUploadAction();
@@ -334,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

@@ -12,7 +12,9 @@
// See the License for the specific language governing permissions and
// limitations under the License.
package google.registry.mosapi.model;
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.
@@ -20,4 +22,5 @@ package google.registry.mosapi.model;
* @see <a href="https://www.icann.org/mosapi-specification.pdf">ICANN MoSAPI Specification, Section
* 8</a>
*/
public record MosApiErrorResponse(String resultCode, String message, String description) {}
public record MosApiErrorResponse(
@Expose String resultCode, @Expose String message, @Expose String description) {}

View File

@@ -17,7 +17,6 @@ package google.registry.mosapi;
import static java.lang.annotation.ElementType.TYPE;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
import google.registry.mosapi.model.MosApiErrorResponse;
import java.io.IOException;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
@@ -42,6 +41,11 @@ public class MosApiException extends IOException {
this.errorResponse = null;
}
public MosApiException(String message) {
super(message);
this.errorResponse = null;
}
public Optional<MosApiErrorResponse> getErrorResponse() {
return Optional.ofNullable(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,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

@@ -32,6 +32,8 @@ 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;
@@ -184,4 +186,21 @@ public final class MosApiModule {
.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

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

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

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

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

@@ -91,14 +91,6 @@ class EppLifecycleHostTest extends EppTestCase {
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(
@@ -142,15 +134,6 @@ class EppLifecycleHostTest extends EppTestCase {
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

@@ -223,7 +223,7 @@ public class EppTestCase {
return eppMetricBuilder.build();
}
/** Create the two administrative contacts and two hosts. */
/** Create the two hosts. */
void createHosts() throws Exception {
DateTime createTime = DateTime.parse("2000-06-01T00:00:00Z");
assertThatCommand("host_create.xml", ImmutableMap.of("HOSTNAME", "ns1.example.external"))

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

View File

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

View File

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

View File

@@ -209,6 +209,10 @@ class DomainCreateFlowTest extends ResourceFlowTestCase<DomainCreateFlow, Domain
private static final String ENCODED_SMD =
TmchData.readEncodedSignedMark(TmchTestData.loadFile(SMD_FILE_PATH)).getEncodedData();
private static final ImmutableMap<String, String> FEE_STD_1_0_MAP =
ImmutableMap.of(
"FEE_VERSION", "epp:fee-1.0", "FEE_NS", "fee1_00", "CURRENCY", "USD", "FEE", "15.00");
private AllocationToken allocationToken;
DomainCreateFlowTest() {
@@ -640,132 +644,44 @@ class DomainCreateFlowTest extends ResourceFlowTestCase<DomainCreateFlow, Domain
}
@Test
void testSuccess_fee_v06() throws Exception {
setEppInput("domain_create_fee.xml", ImmutableMap.of("FEE_VERSION", "0.6", "CURRENCY", "USD"));
void testSuccess_fee_std_v1() throws Exception {
setEppInput("domain_create_fee.xml", FEE_STD_1_0_MAP);
persistContactsAndHosts();
doSuccessfulTest(
"tld",
"domain_create_response_fee.xml",
ImmutableMap.of("FEE_VERSION", "0.6", "FEE", "24.00"));
ImmutableMap.of("FEE_VERSION", "epp:fee-1.0", "FEE", "24.00"));
}
@Test
void testSuccess_fee_v11() throws Exception {
setEppInput("domain_create_fee.xml", ImmutableMap.of("FEE_VERSION", "0.11", "CURRENCY", "USD"));
void testSuccess_fee_withDefaultAttributes_std_v1() throws Exception {
setEppInput("domain_create_fee_defaults.xml", FEE_STD_1_0_MAP);
persistContactsAndHosts();
doSuccessfulTest(
"tld",
"domain_create_response_fee.xml",
ImmutableMap.of("FEE_VERSION", "0.11", "FEE", "24.00"));
ImmutableMap.of("FEE_VERSION", "epp:fee-1.0", "FEE", "24.00"));
}
@Test
void testSuccess_fee_v12() throws Exception {
setEppInput("domain_create_fee.xml", ImmutableMap.of("FEE_VERSION", "0.12", "CURRENCY", "USD"));
persistContactsAndHosts();
doSuccessfulTest(
"tld",
"domain_create_response_fee.xml",
ImmutableMap.of("FEE_VERSION", "0.12", "FEE", "24.00"));
}
@Test
void testSuccess_fee_withDefaultAttributes_v06() throws Exception {
setEppInput("domain_create_fee_defaults.xml", ImmutableMap.of("FEE_VERSION", "0.6"));
persistContactsAndHosts();
doSuccessfulTest(
"tld",
"domain_create_response_fee.xml",
ImmutableMap.of("FEE_VERSION", "0.6", "FEE", "24.00"));
}
@Test
void testSuccess_fee_withDefaultAttributes_v11() throws Exception {
setEppInput("domain_create_fee_defaults.xml", ImmutableMap.of("FEE_VERSION", "0.11"));
persistContactsAndHosts();
doSuccessfulTest(
"tld",
"domain_create_response_fee.xml",
ImmutableMap.of("FEE_VERSION", "0.11", "FEE", "24.00"));
}
@Test
void testSuccess_fee_withDefaultAttributes_v12() throws Exception {
setEppInput("domain_create_fee_defaults.xml", ImmutableMap.of("FEE_VERSION", "0.12"));
persistContactsAndHosts();
doSuccessfulTest(
"tld",
"domain_create_response_fee.xml",
ImmutableMap.of("FEE_VERSION", "0.12", "FEE", "24.00"));
}
@Test
void testFailure_refundableFee_v06() {
setEppInput("domain_create_fee_refundable.xml", ImmutableMap.of("FEE_VERSION", "0.6"));
void testFailure_refundableFee_std_v1() {
setEppInput("domain_create_fee_refundable.xml", FEE_STD_1_0_MAP);
persistContactsAndHosts();
EppException thrown = assertThrows(UnsupportedFeeAttributeException.class, this::runFlow);
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
void testFailure_refundableFee_v11() {
setEppInput("domain_create_fee_refundable.xml", ImmutableMap.of("FEE_VERSION", "0.11"));
void testFailure_gracePeriodFee_std_v1() {
setEppInput("domain_create_fee_grace_period.xml", FEE_STD_1_0_MAP);
persistContactsAndHosts();
EppException thrown = assertThrows(UnsupportedFeeAttributeException.class, this::runFlow);
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
void testFailure_refundableFee_v12() {
setEppInput("domain_create_fee_refundable.xml", ImmutableMap.of("FEE_VERSION", "0.12"));
persistContactsAndHosts();
EppException thrown = assertThrows(UnsupportedFeeAttributeException.class, this::runFlow);
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
void testFailure_gracePeriodFee_v06() {
setEppInput("domain_create_fee_grace_period.xml", ImmutableMap.of("FEE_VERSION", "0.6"));
persistContactsAndHosts();
EppException thrown = assertThrows(UnsupportedFeeAttributeException.class, this::runFlow);
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
void testFailure_gracePeriodFee_v11() {
setEppInput("domain_create_fee_grace_period.xml", ImmutableMap.of("FEE_VERSION", "0.11"));
persistContactsAndHosts();
EppException thrown = assertThrows(UnsupportedFeeAttributeException.class, this::runFlow);
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
void testFailure_gracePeriodFee_v12() {
setEppInput("domain_create_fee_grace_period.xml", ImmutableMap.of("FEE_VERSION", "0.12"));
persistContactsAndHosts();
EppException thrown = assertThrows(UnsupportedFeeAttributeException.class, this::runFlow);
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
void testFailure_appliedFee_v06() {
setEppInput("domain_create_fee_applied.xml", ImmutableMap.of("FEE_VERSION", "0.6"));
persistContactsAndHosts();
EppException thrown = assertThrows(UnsupportedFeeAttributeException.class, this::runFlow);
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
void testFailure_appliedFee_v11() {
setEppInput("domain_create_fee_applied.xml", ImmutableMap.of("FEE_VERSION", "0.11"));
persistContactsAndHosts();
EppException thrown = assertThrows(UnsupportedFeeAttributeException.class, this::runFlow);
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
void testFailure_appliedFee_v12() {
setEppInput("domain_create_fee_applied.xml", ImmutableMap.of("FEE_VERSION", "0.12"));
void testFailure_appliedFee_std_v1() {
setEppInput("domain_create_fee_applied.xml", FEE_STD_1_0_MAP);
persistContactsAndHosts();
EppException thrown = assertThrows(UnsupportedFeeAttributeException.class, this::runFlow);
assertAboutEppExceptions().that(thrown).marshalsToXml();
@@ -796,9 +712,9 @@ class DomainCreateFlowTest extends ResourceFlowTestCase<DomainCreateFlow, Domain
}
@Test
void testSuccess_premiumAndEap() throws Exception {
void testSuccess_premiumAndEap_std_v1() throws Exception {
createTld("example");
setEppInput("domain_create_premium_eap.xml");
setEppInput("domain_create_premium_eap.xml", FEE_STD_1_0_MAP);
persistContactsAndHosts("net");
persistResource(
Tld.get("example")
@@ -816,8 +732,7 @@ class DomainCreateFlowTest extends ResourceFlowTestCase<DomainCreateFlow, Domain
runFlowAssertResponse(
CommitMode.LIVE,
UserPrivileges.NORMAL,
loadFile(
"domain_create_response_premium_eap.xml", ImmutableMap.of("DOMAIN", "rich.example")));
loadFile("domain_create_response_premium_eap.xml", FEE_STD_1_0_MAP));
assertSuccessfulCreate("example", ImmutableSet.of(), 200);
assertNoLordn();
}
@@ -1047,8 +962,8 @@ class DomainCreateFlowTest extends ResourceFlowTestCase<DomainCreateFlow, Domain
}
@Test
void testFailure_wrongFeeAmount_v06() {
setEppInput("domain_create_fee.xml", ImmutableMap.of("FEE_VERSION", "0.6", "CURRENCY", "USD"));
void testFailure_wrongFeeAmount_std_v1() {
setEppInput("domain_create_fee.xml", FEE_STD_1_0_MAP);
persistResource(
Tld.get("tld")
.asBuilder()
@@ -1061,7 +976,7 @@ class DomainCreateFlowTest extends ResourceFlowTestCase<DomainCreateFlow, Domain
}
@Test
void testSuccess_wrongFeeAmountTooHigh_defaultToken_v06() throws Exception {
void testSuccess_wrongFeeAmountTooHigh_defaultToken_std_v1() throws Exception {
setupDefaultTokenWithDiscount();
persistResource(
Tld.get("tld")
@@ -1069,65 +984,14 @@ class DomainCreateFlowTest extends ResourceFlowTestCase<DomainCreateFlow, Domain
.setCreateBillingCostTransitions(ImmutableSortedMap.of(START_OF_TIME, Money.of(USD, 8)))
.build());
// Expects fee of $24
setEppInput("domain_create_fee.xml", ImmutableMap.of("FEE_VERSION", "0.6", "CURRENCY", "USD"));
persistContactsAndHosts();
// $15 is 50% off the first year registration ($8) and 0% 0ff the 2nd year (renewal at $11)
runFlowAssertResponse(
loadFile(
"domain_create_response_fee.xml",
ImmutableMap.of("FEE_VERSION", "0.6", "FEE", "15.00")));
}
@Test
void testFailure_wrongFeeAmountTooLow_defaultToken_v06() throws Exception {
setupDefaultTokenWithDiscount();
persistResource(
Tld.get("tld")
.asBuilder()
.setCreateBillingCostTransitions(
ImmutableSortedMap.of(START_OF_TIME, Money.of(USD, 100)))
.build());
// Expects fee of $24
setEppInput("domain_create_fee.xml", ImmutableMap.of("FEE_VERSION", "0.6", "CURRENCY", "USD"));
persistContactsAndHosts();
EppException thrown = assertThrows(FeesMismatchException.class, this::runFlow);
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
void testFailure_wrongFeeAmount_v11() {
setEppInput("domain_create_fee.xml", ImmutableMap.of("FEE_VERSION", "0.11", "CURRENCY", "USD"));
persistResource(
Tld.get("tld")
.asBuilder()
.setCreateBillingCostTransitions(
ImmutableSortedMap.of(START_OF_TIME, Money.of(USD, 20)))
.build());
persistContactsAndHosts();
EppException thrown = assertThrows(FeesMismatchException.class, this::runFlow);
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
void testSuccess_wrongFeeAmountTooHigh_defaultToken_v11() throws Exception {
setupDefaultTokenWithDiscount();
persistResource(
Tld.get("tld")
.asBuilder()
.setCreateBillingCostTransitions(ImmutableSortedMap.of(START_OF_TIME, Money.of(USD, 8)))
.build());
// Expects fee of $24
setEppInput("domain_create_fee.xml", ImmutableMap.of("FEE_VERSION", "0.11", "CURRENCY", "USD"));
setEppInput("domain_create_fee.xml", FEE_STD_1_0_MAP);
persistContactsAndHosts();
// $12 is equal to 50% off the first year registration and 0% 0ff the 2nd year
runFlowAssertResponse(
loadFile(
"domain_create_response_fee.xml",
ImmutableMap.of("FEE_VERSION", "0.11", "FEE", "15.00")));
runFlowAssertResponse(loadFile("domain_create_response_fee.xml", FEE_STD_1_0_MAP));
}
@Test
void testFailure_wrongFeeAmountTooLow_defaultToken_v11() throws Exception {
void testFailure_wrongFeeAmountTooLow_defaultToken_std_v1() throws Exception {
setupDefaultTokenWithDiscount();
persistResource(
Tld.get("tld")
@@ -1136,79 +1000,16 @@ class DomainCreateFlowTest extends ResourceFlowTestCase<DomainCreateFlow, Domain
ImmutableSortedMap.of(START_OF_TIME, Money.of(USD, 100)))
.build());
// Expects fee of $24
setEppInput("domain_create_fee.xml", ImmutableMap.of("FEE_VERSION", "0.11", "CURRENCY", "USD"));
setEppInput("domain_create_fee.xml", FEE_STD_1_0_MAP);
persistContactsAndHosts();
EppException thrown = assertThrows(FeesMismatchException.class, this::runFlow);
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
void testFailure_wrongFeeAmount_v12() {
setEppInput("domain_create_fee.xml", ImmutableMap.of("FEE_VERSION", "0.12", "CURRENCY", "USD"));
persistResource(
Tld.get("tld")
.asBuilder()
.setCreateBillingCostTransitions(
ImmutableSortedMap.of(START_OF_TIME, Money.of(USD, 20)))
.build());
persistContactsAndHosts();
EppException thrown = assertThrows(FeesMismatchException.class, this::runFlow);
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
void testSuccess_wrongFeeAmountTooHigh_defaultToken_v12() throws Exception {
setupDefaultTokenWithDiscount();
persistResource(
Tld.get("tld")
.asBuilder()
.setCreateBillingCostTransitions(ImmutableSortedMap.of(START_OF_TIME, Money.of(USD, 8)))
.build());
// Expects fee of $24
setEppInput("domain_create_fee.xml", ImmutableMap.of("FEE_VERSION", "0.12", "CURRENCY", "USD"));
persistContactsAndHosts();
// $12 is equal to 50% off the first year registration and 0% 0ff the 2nd year
runFlowAssertResponse(
loadFile(
"domain_create_response_fee.xml",
ImmutableMap.of("FEE_VERSION", "0.12", "FEE", "15.00")));
}
@Test
void testFailure_wrongFeeAmountTooLow_defaultToken_v12() throws Exception {
setupDefaultTokenWithDiscount();
persistResource(
Tld.get("tld")
.asBuilder()
.setCreateBillingCostTransitions(
ImmutableSortedMap.of(START_OF_TIME, Money.of(USD, 100)))
.build());
// Expects fee of $24
setEppInput("domain_create_fee.xml", ImmutableMap.of("FEE_VERSION", "0.12", "CURRENCY", "USD"));
persistContactsAndHosts();
EppException thrown = assertThrows(FeesMismatchException.class, this::runFlow);
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
void testFailure_wrongCurrency_v06() {
setEppInput("domain_create_fee.xml", ImmutableMap.of("FEE_VERSION", "0.6", "CURRENCY", "EUR"));
persistContactsAndHosts();
EppException thrown = assertThrows(CurrencyUnitMismatchException.class, this::runFlow);
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
void testFailure_wrongCurrency_v11() {
setEppInput("domain_create_fee.xml", ImmutableMap.of("FEE_VERSION", "0.11", "CURRENCY", "EUR"));
persistContactsAndHosts();
EppException thrown = assertThrows(CurrencyUnitMismatchException.class, this::runFlow);
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
void testFailure_wrongCurrency_v12() {
setEppInput("domain_create_fee.xml", ImmutableMap.of("FEE_VERSION", "0.12", "CURRENCY", "EUR"));
void testFailure_wrongCurrency_std_v1() {
setEppInput(
"domain_create_fee.xml", ImmutableMap.of("FEE_VERSION", "epp:fee-1.0", "CURRENCY", "EUR"));
persistContactsAndHosts();
EppException thrown = assertThrows(CurrencyUnitMismatchException.class, this::runFlow);
assertAboutEppExceptions().that(thrown).marshalsToXml();
@@ -1216,7 +1017,8 @@ class DomainCreateFlowTest extends ResourceFlowTestCase<DomainCreateFlow, Domain
@Test
void testFailure_unknownCurrency() {
setEppInput("domain_create_fee.xml", ImmutableMap.of("FEE_VERSION", "0.12", "CURRENCY", "BAD"));
setEppInput(
"domain_create_fee.xml", ImmutableMap.of("FEE_VERSION", "epp:fee-1.0", "CURRENCY", "BAD"));
persistContactsAndHosts();
EppException thrown = assertThrows(UnknownCurrencyEppException.class, this::runFlow);
assertAboutEppExceptions().that(thrown).marshalsToXml();
@@ -1580,7 +1382,7 @@ class DomainCreateFlowTest extends ResourceFlowTestCase<DomainCreateFlow, Domain
}
@Test
void testSuccess_allocationToken_multiYearDiscount_worksForPremiums() throws Exception {
void testSuccess_allocationToken_multiYearDiscount_worksForPremiums_std_v1() throws Exception {
createTld("example");
persistContactsAndHosts();
persistResource(
@@ -1601,11 +1403,17 @@ class DomainCreateFlowTest extends ResourceFlowTestCase<DomainCreateFlow, Domain
clock.advanceOneMilli();
setEppInput(
"domain_create_premium_allocationtoken.xml",
ImmutableMap.of("YEARS", "3", "FEE", "104.00"));
ImmutableMap.of("FEE_VERSION", "epp:fee-1.0", "YEARS", "3", "FEE", "104.00"));
runFlowAssertResponse(
loadFile(
"domain_create_response_premium.xml",
ImmutableMap.of("EXDATE", "2002-04-03T22:00:00.0Z", "FEE", "104.00")));
ImmutableMap.of(
"FEE_VERSION",
"epp:fee-1.0",
"EXDATE",
"2002-04-03T22:00:00.0Z",
"FEE",
"104.00")));
BillingEvent billingEvent =
Iterables.getOnlyElement(DatabaseHelper.loadAllOf(BillingEvent.class));
assertThat(billingEvent.getTargetId()).isEqualTo("rich.example");
@@ -1614,7 +1422,7 @@ class DomainCreateFlowTest extends ResourceFlowTestCase<DomainCreateFlow, Domain
}
@Test
void testSuccess_allocationToken_singleYearDiscount_worksForPremiums() throws Exception {
void testSuccess_allocationToken_singleYearDiscount_worksForPremiums_std_v1() throws Exception {
createTld("example");
persistContactsAndHosts();
persistResource(
@@ -1634,11 +1442,17 @@ class DomainCreateFlowTest extends ResourceFlowTestCase<DomainCreateFlow, Domain
clock.advanceOneMilli();
setEppInput(
"domain_create_premium_allocationtoken.xml",
ImmutableMap.of("YEARS", "3", "FEE", "204.44"));
ImmutableMap.of("FEE_VERSION", "epp:fee-1.0", "YEARS", "3", "FEE", "204.44"));
runFlowAssertResponse(
loadFile(
"domain_create_response_premium.xml",
ImmutableMap.of("EXDATE", "2002-04-03T22:00:00.0Z", "FEE", "204.44")));
ImmutableMap.of(
"FEE_VERSION",
"epp:fee-1.0",
"EXDATE",
"2002-04-03T22:00:00.0Z",
"FEE",
"204.44")));
BillingEvent billingEvent =
Iterables.getOnlyElement(DatabaseHelper.loadAllOf(BillingEvent.class));
assertThat(billingEvent.getTargetId()).isEqualTo("rich.example");
@@ -1797,7 +1611,7 @@ class DomainCreateFlowTest extends ResourceFlowTestCase<DomainCreateFlow, Domain
}
@Test
void testSuccess_doesNotApplyNonPremiumDefaultTokenToPremiumName() throws Exception {
void testSuccess_doesNotApplyNonPremiumDefaultTokenToPremiumName_std_v1() throws Exception {
persistContactsAndHosts();
createTld("example");
persistResource(
@@ -1805,11 +1619,17 @@ class DomainCreateFlowTest extends ResourceFlowTestCase<DomainCreateFlow, Domain
.asBuilder()
.setAllowedTlds(ImmutableSet.of("example"))
.build());
setEppInput("domain_create_premium.xml");
setEppInput("domain_create_premium.xml", FEE_STD_1_0_MAP);
runFlowAssertResponse(
loadFile(
"domain_create_response_premium.xml",
ImmutableMap.of("EXDATE", "2001-04-03T22:00:00.0Z", "FEE", "200.00")));
ImmutableMap.of(
"FEE_VERSION",
"epp:fee-1.0",
"EXDATE",
"2001-04-03T22:00:00.0Z",
"FEE",
"200.00")));
assertSuccessfulCreate("example", ImmutableSet.of(), 200);
}
@@ -1995,9 +1815,9 @@ class DomainCreateFlowTest extends ResourceFlowTestCase<DomainCreateFlow, Domain
}
@Test
void testSuccess_superuserOverridesPremiumNameBlock() throws Exception {
void testSuccess_superuserOverridesPremiumNameBlock_std_v1() throws Exception {
createTld("example");
setEppInput("domain_create_premium.xml");
setEppInput("domain_create_premium.xml", FEE_STD_1_0_MAP);
persistContactsAndHosts("net");
// Modify the Registrar to block premium names.
persistResource(loadRegistrar("TheRegistrar").asBuilder().setBlockPremiumNames(true).build());
@@ -2006,7 +1826,13 @@ class DomainCreateFlowTest extends ResourceFlowTestCase<DomainCreateFlow, Domain
SUPERUSER,
loadFile(
"domain_create_response_premium.xml",
ImmutableMap.of("EXDATE", "2001-04-03T22:00:00.0Z", "FEE", "200.00")));
ImmutableMap.of(
"FEE_VERSION",
"epp:fee-1.0",
"EXDATE",
"2001-04-03T22:00:00.0Z",
"FEE",
"200.00")));
assertSuccessfulCreate("example", ImmutableSet.of(), 200);
}
@@ -2137,9 +1963,9 @@ class DomainCreateFlowTest extends ResourceFlowTestCase<DomainCreateFlow, Domain
}
@Test
void testFailure_premiumBlocked() {
void testFailure_premiumBlocked_std_v1() {
createTld("example");
setEppInput("domain_create_premium.xml");
setEppInput("domain_create_premium.xml", FEE_STD_1_0_MAP);
persistContactsAndHosts("net");
// Modify the Registrar to block premium names.
persistResource(loadRegistrar("TheRegistrar").asBuilder().setBlockPremiumNames(true).build());
@@ -2157,60 +1983,20 @@ class DomainCreateFlowTest extends ResourceFlowTestCase<DomainCreateFlow, Domain
}
@Test
void testFailure_omitFeeExtensionOnLogin_v06() {
void testFailure_omitFeeExtensionOnLogin_std_v1() {
for (String uri : FEE_EXTENSION_URIS) {
removeServiceExtensionUri(uri);
}
createTld("net");
setEppInput("domain_create_fee.xml", ImmutableMap.of("FEE_VERSION", "0.6", "CURRENCY", "USD"));
setEppInput("domain_create_fee.xml", FEE_STD_1_0_MAP);
persistContactsAndHosts();
EppException thrown = assertThrows(UndeclaredServiceExtensionException.class, this::runFlow);
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
void testFailure_omitFeeExtensionOnLogin_v11() {
for (String uri : FEE_EXTENSION_URIS) {
removeServiceExtensionUri(uri);
}
createTld("net");
setEppInput("domain_create_fee.xml", ImmutableMap.of("FEE_VERSION", "0.11", "CURRENCY", "USD"));
persistContactsAndHosts();
EppException thrown = assertThrows(UndeclaredServiceExtensionException.class, this::runFlow);
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
void testFailure_omitFeeExtensionOnLogin_v12() {
for (String uri : FEE_EXTENSION_URIS) {
removeServiceExtensionUri(uri);
}
createTld("net");
setEppInput("domain_create_fee.xml", ImmutableMap.of("FEE_VERSION", "0.12", "CURRENCY", "USD"));
persistContactsAndHosts();
EppException thrown = assertThrows(UndeclaredServiceExtensionException.class, this::runFlow);
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
void testFailure_feeGivenInWrongScale_v06() {
setEppInput("domain_create_fee_bad_scale.xml", ImmutableMap.of("FEE_VERSION", "0.6"));
persistContactsAndHosts();
EppException thrown = assertThrows(CurrencyValueScaleException.class, this::runFlow);
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
void testFailure_feeGivenInWrongScale_v11() {
setEppInput("domain_create_fee_bad_scale.xml", ImmutableMap.of("FEE_VERSION", "0.11"));
persistContactsAndHosts();
EppException thrown = assertThrows(CurrencyValueScaleException.class, this::runFlow);
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
void testFailure_feeGivenInWrongScale_v12() {
setEppInput("domain_create_fee_bad_scale.xml", ImmutableMap.of("FEE_VERSION", "0.12"));
void testFailure_feeGivenInWrongScale_std_v1() {
setEppInput("domain_create_fee_bad_scale.xml", FEE_STD_1_0_MAP);
persistContactsAndHosts();
EppException thrown = assertThrows(CurrencyValueScaleException.class, this::runFlow);
assertAboutEppExceptions().that(thrown).marshalsToXml();
@@ -2699,8 +2485,8 @@ class DomainCreateFlowTest extends ResourceFlowTestCase<DomainCreateFlow, Domain
}
@Test
void testFailure_eapFee_combined() {
setEppInput("domain_create_eap_combined_fee.xml", ImmutableMap.of("FEE_VERSION", "0.6"));
void testFailure_eapFee_combined_std_v1() {
setEppInput("domain_create_eap_combined_fee.xml", FEE_STD_1_0_MAP);
persistContactsAndHosts();
setEapForTld("tld");
EppException thrown = assertThrows(FeeDescriptionParseException.class, this::runFlow);
@@ -2709,12 +2495,12 @@ class DomainCreateFlowTest extends ResourceFlowTestCase<DomainCreateFlow, Domain
}
@Test
void testFailure_eapFee_description_swapped() {
void testFailure_eapFee_description_swapped_std_v1() {
setEppInput(
"domain_create_eap_fee.xml",
ImmutableMap.of(
"FEE_VERSION",
"0.6",
"epp:fee-1.0",
"DESCRIPTION_1",
"Early Access Period",
"DESCRIPTION_2",
@@ -2727,11 +2513,11 @@ class DomainCreateFlowTest extends ResourceFlowTestCase<DomainCreateFlow, Domain
}
@Test
void testFailure_eapFee_totalAmountNotMatched() {
void testFailure_eapFee_totalAmountNotMatched_std_v1() {
setEppInput(
"domain_create_extra_fees.xml",
new ImmutableMap.Builder<String, String>()
.put("FEE_VERSION", "0.6")
.put("FEE_VERSION", "epp:fee-1.0")
.put("DESCRIPTION_1", "create")
.put("FEE_1", "24")
.put("DESCRIPTION_2", "Early Access Period")
@@ -2747,11 +2533,11 @@ class DomainCreateFlowTest extends ResourceFlowTestCase<DomainCreateFlow, Domain
}
@Test
void testSuccess_eapFee_multipleEAPfees_doNotAddToExpectedValue() {
void testSuccess_eapFee_multipleEAPfees_doNotAddToExpectedValue_std_v1() {
setEppInput(
"domain_create_extra_fees.xml",
new ImmutableMap.Builder<String, String>()
.put("FEE_VERSION", "0.6")
.put("FEE_VERSION", "epp:fee-1.0")
.put("DESCRIPTION_1", "create")
.put("FEE_1", "24")
.put("DESCRIPTION_2", "Early Access Period")
@@ -2767,11 +2553,11 @@ class DomainCreateFlowTest extends ResourceFlowTestCase<DomainCreateFlow, Domain
}
@Test
void testSuccess_eapFee_multipleEAPfees_addToExpectedValue() throws Exception {
void testSuccess_eapFee_multipleEAPfees_addToExpectedValue_std_v1() throws Exception {
setEppInput(
"domain_create_extra_fees.xml",
new ImmutableMap.Builder<String, String>()
.put("FEE_VERSION", "0.6")
.put("FEE_VERSION", "epp:fee-1.0")
.put("DESCRIPTION_1", "create")
.put("FEE_1", "24")
.put("DESCRIPTION_2", "Early Access Period")
@@ -2781,33 +2567,36 @@ class DomainCreateFlowTest extends ResourceFlowTestCase<DomainCreateFlow, Domain
.build());
persistContactsAndHosts();
setEapForTld("tld");
doSuccessfulTest(
"tld", "domain_create_response_eap_fee.xml", ImmutableMap.of("FEE_VERSION", "0.6"));
doSuccessfulTest("tld", "domain_create_response_eap_fee.xml", FEE_STD_1_0_MAP);
}
@Test
void testSuccess_eapFee_fullDescription_includingArbitraryExpiryTime() throws Exception {
void testSuccess_eapFee_fullDescription_includingArbitraryExpiryTime_std_v1() throws Exception {
setEppInput(
"domain_create_eap_fee.xml",
ImmutableMap.of(
"FEE_VERSION",
"0.6",
"epp:fee-1.0",
"DESCRIPTION_1",
"create",
"DESCRIPTION_2",
"Early Access Period, fee expires: 2022-03-01T00:00:00.000Z"));
persistContactsAndHosts();
setEapForTld("tld");
doSuccessfulTest(
"tld", "domain_create_response_eap_fee.xml", ImmutableMap.of("FEE_VERSION", "0.6"));
doSuccessfulTest("tld", "domain_create_response_eap_fee.xml", FEE_STD_1_0_MAP);
}
@Test
void testFailure_eapFee_description_multipleMatch() {
void testFailure_eapFee_description_multipleMatch_std_v1() {
setEppInput(
"domain_create_eap_fee.xml",
ImmutableMap.of(
"FEE_VERSION", "0.6", "DESCRIPTION_1", "create", "DESCRIPTION_2", "renew transfer"));
"FEE_VERSION",
"epp:fee-1.0",
"DESCRIPTION_1",
"create",
"DESCRIPTION_2",
"renew transfer"));
persistContactsAndHosts();
setEapForTld("tld");
EppException thrown = assertThrows(FeeDescriptionMultipleMatchesException.class, this::runFlow);
@@ -2816,54 +2605,17 @@ class DomainCreateFlowTest extends ResourceFlowTestCase<DomainCreateFlow, Domain
}
@Test
void testSuccess_eapFeeApplied_v06() throws Exception {
void testSuccess_eapFeeApplied_std_v1() throws Exception {
setEppInput(
"domain_create_eap_fee.xml",
ImmutableMap.of(
"FEE_VERSION",
"0.6",
"DESCRIPTION_1",
"create",
"DESCRIPTION_2",
"Early Access Period"));
new ImmutableMap.Builder<String, String>()
.putAll(FEE_STD_1_0_MAP)
.put("DESCRIPTION_1", "create")
.put("DESCRIPTION_2", "Early Access Period")
.build());
persistContactsAndHosts();
setEapForTld("tld");
doSuccessfulTest(
"tld", "domain_create_response_eap_fee.xml", ImmutableMap.of("FEE_VERSION", "0.6"));
}
@Test
void testSuccess_eapFeeApplied_v11() throws Exception {
setEppInput(
"domain_create_eap_fee.xml",
ImmutableMap.of(
"FEE_VERSION",
"0.11",
"DESCRIPTION_1",
"create",
"DESCRIPTION_2",
"Early Access Period"));
persistContactsAndHosts();
setEapForTld("tld");
doSuccessfulTest(
"tld", "domain_create_response_eap_fee.xml", ImmutableMap.of("FEE_VERSION", "0.11"));
}
@Test
void testSuccess_eapFeeApplied_v12() throws Exception {
setEppInput(
"domain_create_eap_fee.xml",
ImmutableMap.of(
"FEE_VERSION",
"0.12",
"DESCRIPTION_1",
"create",
"DESCRIPTION_2",
"Early Access Period"));
persistContactsAndHosts();
setEapForTld("tld");
doSuccessfulTest(
"tld", "domain_create_response_eap_fee.xml", ImmutableMap.of("FEE_VERSION", "0.12"));
doSuccessfulTest("tld", "domain_create_response_eap_fee.xml", FEE_STD_1_0_MAP);
}
@Test
@@ -3002,7 +2754,7 @@ class DomainCreateFlowTest extends ResourceFlowTestCase<DomainCreateFlow, Domain
}
@Test
void testSuccess_nonAnchorTenant_nonPremiumRenewal() throws Exception {
void testSuccess_nonAnchorTenant_nonPremiumRenewal_std_v1() throws Exception {
createTld("example");
AllocationToken token =
persistResource(
@@ -3016,13 +2768,13 @@ class DomainCreateFlowTest extends ResourceFlowTestCase<DomainCreateFlow, Domain
// Creation is still $100 but it'll create a NONPREMIUM renewal
setEppInput(
"domain_create_premium_allocationtoken.xml",
ImmutableMap.of("YEARS", "2", "FEE", "111.00"));
ImmutableMap.of("FEE_VERSION", "epp:fee-1.0", "YEARS", "2", "FEE", "111.00"));
runFlow();
assertSuccessfulCreate("example", ImmutableSet.of(), token, 111);
}
@Test
void testSuccess_specifiedRenewalPriceToken_specifiedRecurrencePrice() throws Exception {
void testSuccess_specifiedRenewalPriceToken_specifiedRecurrencePrice_std_v1() throws Exception {
createTld("example");
AllocationToken token =
persistResource(
@@ -3037,7 +2789,7 @@ class DomainCreateFlowTest extends ResourceFlowTestCase<DomainCreateFlow, Domain
// Creation is still $100 but it'll create a $1 renewal
setEppInput(
"domain_create_premium_allocationtoken.xml",
ImmutableMap.of("YEARS", "2", "FEE", "101.00"));
ImmutableMap.of("FEE_VERSION", "epp:fee-1.0", "YEARS", "2", "FEE", "101.00"));
runFlow();
assertSuccessfulCreate("example", ImmutableSet.of(), token, 101, 1);
}
@@ -3087,7 +2839,7 @@ class DomainCreateFlowTest extends ResourceFlowTestCase<DomainCreateFlow, Domain
}
@Test
void testSuccess_nonpremiumCreateToken() throws Exception {
void testSuccess_nonpremiumCreateToken_std_v1() throws Exception {
createTld("example");
persistContactsAndHosts();
persistResource(
@@ -3098,8 +2850,9 @@ class DomainCreateFlowTest extends ResourceFlowTestCase<DomainCreateFlow, Domain
.setDomainName("rich.example")
.build());
setEppInput(
"domain_create_premium_allocationtoken.xml", ImmutableMap.of("YEARS", "1", "FEE", "13.00"));
runFlowAssertResponse(loadFile("domain_create_nonpremium_token_response.xml"));
"domain_create_premium_allocationtoken.xml",
ImmutableMap.of("FEE_VERSION", "epp:fee-1.0", "YEARS", "1", "FEE", "13.00"));
runFlowAssertResponse(loadFile("domain_create_nonpremium_token_response.xml", FEE_STD_1_0_MAP));
}
@Test
@@ -3495,7 +3248,7 @@ class DomainCreateFlowTest extends ResourceFlowTestCase<DomainCreateFlow, Domain
void testTieredPricingPromoResponse() throws Exception {
sessionMetadata.setRegistrarId("NewRegistrar");
setupDefaultTokenWithDiscount("NewRegistrar");
setEppInput("domain_create_fee.xml", ImmutableMap.of("FEE_VERSION", "0.12", "CURRENCY", "USD"));
setEppInput("domain_create_fee.xml", FEE_STD_1_0_MAP);
persistContactsAndHosts();
// Fee in the result should be 24 (create cost of 13 plus renew cost of 11) even though the
@@ -3503,29 +3256,29 @@ class DomainCreateFlowTest extends ResourceFlowTestCase<DomainCreateFlow, Domain
runFlowAssertResponse(
loadFile(
"domain_create_response_fee.xml",
ImmutableMap.of("FEE_VERSION", "0.12", "FEE", "24.00")));
ImmutableMap.of("FEE_VERSION", "epp:fee-1.0", "FEE", "24.00")));
// Expected cost is half off the create cost (13/2 == 6.50) plus one full-cost renew (11)
assertThat(Iterables.getOnlyElement(loadAllOf(BillingEvent.class)).getCost())
.isEqualTo(Money.of(USD, 17.50));
}
@Test
void testTieredPricingPromo_registrarNotIncluded_standardResponse() throws Exception {
void testTieredPricingPromo_registrarNotIncluded_standardResponse_std_v1() throws Exception {
setupDefaultTokenWithDiscount("NewRegistrar");
setEppInput("domain_create_fee.xml", ImmutableMap.of("FEE_VERSION", "0.12", "CURRENCY", "USD"));
setEppInput("domain_create_fee.xml", FEE_STD_1_0_MAP);
persistContactsAndHosts();
// For a registrar not included in the tiered pricing promo, costs should be 24
runFlowAssertResponse(
loadFile(
"domain_create_response_fee.xml",
ImmutableMap.of("FEE_VERSION", "0.12", "FEE", "24.00")));
ImmutableMap.of("FEE_VERSION", "epp:fee-1.0", "FEE", "24.00")));
assertThat(Iterables.getOnlyElement(loadAllOf(BillingEvent.class)).getCost())
.isEqualTo(Money.of(USD, 24));
}
@Test
void testTieredPricingPromo_registrarIncluded_noTokenActive() throws Exception {
void testTieredPricingPromo_registrarIncluded_noTokenActive_std_v1() throws Exception {
sessionMetadata.setRegistrarId("NewRegistrar");
persistActiveDomain("example1.tld");
@@ -3540,14 +3293,14 @@ class DomainCreateFlowTest extends ResourceFlowTestCase<DomainCreateFlow, Domain
TokenStatus.VALID))
.build());
setEppInput("domain_create_fee.xml", ImmutableMap.of("FEE_VERSION", "0.12", "CURRENCY", "USD"));
setEppInput("domain_create_fee.xml", FEE_STD_1_0_MAP);
persistContactsAndHosts();
// The token hasn't started yet, so the cost should be create (13) plus renew (11)
runFlowAssertResponse(
loadFile(
"domain_create_response_fee.xml",
ImmutableMap.of("FEE_VERSION", "0.12", "FEE", "24.00")));
ImmutableMap.of("FEE_VERSION", "epp:fee-1.0", "FEE", "24.00")));
assertThat(Iterables.getOnlyElement(loadAllOf(BillingEvent.class)).getCost())
.isEqualTo(Money.of(USD, 24));
}

View File

@@ -0,0 +1,440 @@
// 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.flows.domain;
import static com.google.common.truth.Truth.assertThat;
import static google.registry.model.reporting.HistoryEntry.Type.DOMAIN_CREATE;
import static google.registry.model.reporting.HistoryEntry.Type.DOMAIN_DELETE;
import static google.registry.testing.DatabaseHelper.assertBillingEvents;
import static google.registry.testing.DatabaseHelper.assertDomainDnsRequests;
import static google.registry.testing.DatabaseHelper.assertPollMessages;
import static google.registry.testing.DatabaseHelper.createTld;
import static google.registry.testing.DatabaseHelper.getOnlyHistoryEntryOfType;
import static google.registry.testing.DatabaseHelper.getOnlyPollMessage;
import static google.registry.testing.DatabaseHelper.getPollMessages;
import static google.registry.testing.DatabaseHelper.loadByKey;
import static google.registry.testing.DatabaseHelper.persistActiveContact;
import static google.registry.testing.DatabaseHelper.persistResource;
import static google.registry.testing.DomainSubject.assertAboutDomains;
import static google.registry.util.DateTimeUtils.END_OF_TIME;
import static google.registry.util.DateTimeUtils.START_OF_TIME;
import static org.joda.money.CurrencyUnit.USD;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.ImmutableSortedMap;
import google.registry.model.billing.BillingBase.Flag;
import google.registry.model.billing.BillingBase.Reason;
import google.registry.model.billing.BillingCancellation;
import google.registry.model.billing.BillingEvent;
import google.registry.model.billing.BillingRecurrence;
import google.registry.model.contact.Contact;
import google.registry.model.domain.Domain;
import google.registry.model.domain.DomainHistory;
import google.registry.model.domain.GracePeriod;
import google.registry.model.domain.rgp.GracePeriodStatus;
import google.registry.model.eppcommon.ProtocolDefinition.ServiceExtension;
import google.registry.model.eppcommon.StatusValue;
import google.registry.model.poll.PollMessage;
import google.registry.model.tld.Tld;
import google.registry.testing.DatabaseHelper;
import java.util.Map;
import java.util.Optional;
import org.joda.money.Money;
import org.joda.time.DateTime;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
/** Tests for {@link DomainDeleteFlow} that use the old fee extensions (0.6, 0.11, 0.12). */
public class DomainDeleteFlowOldFeeExtensionsTest
extends ProductionSimulatingFeeExtensionsTest<DomainDeleteFlow> {
private static final DateTime TIME_BEFORE_FLOW = DateTime.parse("2000-06-06T22:00:00.0Z");
private static final DateTime A_MONTH_AGO = TIME_BEFORE_FLOW.minusMonths(1);
private static final DateTime A_MONTH_FROM_NOW = TIME_BEFORE_FLOW.plusMonths(1);
private static final ImmutableMap<String, String> FEE_06_MAP =
ImmutableMap.of("FEE_VERSION", "fee-0.6", "FEE_NS", "fee");
private static final ImmutableMap<String, String> FEE_11_MAP =
ImmutableMap.of("FEE_VERSION", "fee-0.11", "FEE_NS", "fee11");
private static final ImmutableMap<String, String> FEE_12_MAP =
ImmutableMap.of("FEE_VERSION", "fee-0.12", "FEE_NS", "fee12");
private Domain domain;
private DomainHistory earlierHistoryEntry;
@BeforeEach
void beforeEachomainDeleteFlowOldFeeExtensionsTest() {
clock.setTo(TIME_BEFORE_FLOW);
setEppInput("domain_delete.xml");
createTld("tld");
}
@Test
void testSuccess_renewGracePeriodCredit_v06() throws Exception {
removeServiceExtensionUri(ServiceExtension.FEE_0_11.getUri());
removeServiceExtensionUri(ServiceExtension.FEE_0_12.getUri());
doSuccessfulTest_noAddGracePeriod("domain_delete_response_pending_fee.xml", FEE_06_MAP);
}
@Test
void testSuccess_renewGracePeriodCredit_v11() throws Exception {
removeServiceExtensionUri(ServiceExtension.FEE_0_12.getUri());
doSuccessfulTest_noAddGracePeriod("domain_delete_response_pending_fee.xml", FEE_11_MAP);
}
@Test
void testSuccess_renewGracePeriodCredit_v12() throws Exception {
doSuccessfulTest_noAddGracePeriod("domain_delete_response_pending_fee.xml", FEE_12_MAP);
}
@Test
void testSuccess_addGracePeriodCredit_v06() throws Exception {
removeServiceExtensionUri(ServiceExtension.FEE_0_11.getUri());
removeServiceExtensionUri(ServiceExtension.FEE_0_12.getUri());
doAddGracePeriodDeleteTest(GracePeriodStatus.ADD, "domain_delete_response_fee.xml", FEE_06_MAP);
}
@Test
void testSuccess_addGracePeriodCredit_v11() throws Exception {
removeServiceExtensionUri(ServiceExtension.FEE_0_12.getUri());
doAddGracePeriodDeleteTest(GracePeriodStatus.ADD, "domain_delete_response_fee.xml", FEE_11_MAP);
}
@Test
void testSuccess_addGracePeriodCredit_v12() throws Exception {
doAddGracePeriodDeleteTest(GracePeriodStatus.ADD, "domain_delete_response_fee.xml", FEE_12_MAP);
}
@Test
void testSuccess_autoRenewGracePeriod_v06() throws Exception {
removeServiceExtensionUri(ServiceExtension.FEE_0_11.getUri());
removeServiceExtensionUri(ServiceExtension.FEE_0_12.getUri());
setUpAutorenewGracePeriod();
clock.advanceOneMilli();
runFlowAssertResponse(loadFile("domain_delete_response_autorenew_fee.xml", FEE_06_MAP));
}
@Test
void testSuccess_autoRenewGracePeriod_v11() throws Exception {
removeServiceExtensionUri(ServiceExtension.FEE_0_12.getUri());
setUpAutorenewGracePeriod();
clock.advanceOneMilli();
runFlowAssertResponse(loadFile("domain_delete_response_autorenew_fee.xml", FEE_11_MAP));
}
@Test
void testSuccess_autoRenewGracePeriod_v12() throws Exception {
setUpAutorenewGracePeriod();
clock.advanceOneMilli();
runFlowAssertResponse(loadFile("domain_delete_response_autorenew_fee.xml", FEE_12_MAP));
}
@Test
void testSuccess_autoRenewGracePeriod_priceChanges_v06() throws Exception {
removeServiceExtensionUri(ServiceExtension.FEE_0_11.getUri());
removeServiceExtensionUri(ServiceExtension.FEE_0_12.getUri());
persistResource(
Tld.get("tld")
.asBuilder()
.setRenewBillingCostTransitions(
ImmutableSortedMap.of(
START_OF_TIME,
Money.of(USD, 11),
TIME_BEFORE_FLOW.minusDays(5),
Money.of(USD, 20)))
.build());
setUpAutorenewGracePeriod();
clock.advanceOneMilli();
runFlowAssertResponse(loadFile("domain_delete_response_autorenew_fee.xml", FEE_06_MAP));
}
@Test
void testSuccess_autoRenewGracePeriod_priceChanges_v11() throws Exception {
removeServiceExtensionUri(ServiceExtension.FEE_0_12.getUri());
persistResource(
Tld.get("tld")
.asBuilder()
.setRenewBillingCostTransitions(
ImmutableSortedMap.of(
START_OF_TIME,
Money.of(USD, 11),
TIME_BEFORE_FLOW.minusDays(5),
Money.of(USD, 20)))
.build());
setUpAutorenewGracePeriod();
clock.advanceOneMilli();
runFlowAssertResponse(loadFile("domain_delete_response_autorenew_fee.xml", FEE_11_MAP));
}
@Test
void testSuccess_autoRenewGracePeriod_priceChanges_v12() throws Exception {
persistResource(
Tld.get("tld")
.asBuilder()
.setRenewBillingCostTransitions(
ImmutableSortedMap.of(
START_OF_TIME,
Money.of(USD, 11),
TIME_BEFORE_FLOW.minusDays(5),
Money.of(USD, 20)))
.build());
setUpAutorenewGracePeriod();
clock.advanceOneMilli();
runFlowAssertResponse(loadFile("domain_delete_response_autorenew_fee.xml", FEE_12_MAP));
}
@Test
void testSuccess_freeCreation_deletionDuringGracePeriod_v12() throws Exception {
// Deletion during the add grace period should still work even if the credit is 0
setUpSuccessfulTest();
BillingEvent graceBillingEvent =
persistResource(createBillingEvent(Reason.CREATE, Money.of(USD, 0)));
setUpGracePeriods(
GracePeriod.forBillingEvent(GracePeriodStatus.ADD, domain.getRepoId(), graceBillingEvent));
clock.advanceOneMilli();
runFlowAssertResponse(loadFile("domain_delete_response_fee_free_grace_v12.xml"));
}
private void createReferencedEntities(DateTime expirationTime) throws Exception {
// Persist a linked contact.
Contact contact = persistActiveContact("sh8013");
domain =
persistResource(
DatabaseHelper.newDomain(getUniqueIdFromCommand())
.asBuilder()
.setCreationTimeForTest(TIME_BEFORE_FLOW)
.setRegistrant(Optional.of(contact.createVKey()))
.setRegistrationExpirationTime(expirationTime)
.build());
earlierHistoryEntry =
persistResource(
new DomainHistory.Builder()
.setType(DOMAIN_CREATE)
.setDomain(domain)
.setModificationTime(clock.nowUtc())
.setRegistrarId(domain.getCreationRegistrarId())
.build());
}
private void setUpSuccessfulTest() throws Exception {
createReferencedEntities(A_MONTH_FROM_NOW);
BillingRecurrence autorenewBillingEvent =
persistResource(createAutorenewBillingEvent("TheRegistrar").build());
PollMessage.Autorenew autorenewPollMessage =
persistResource(createAutorenewPollMessage("TheRegistrar").build());
domain =
persistResource(
domain
.asBuilder()
.setAutorenewBillingEvent(autorenewBillingEvent.createVKey())
.setAutorenewPollMessage(autorenewPollMessage.createVKey())
.build());
assertMutatingFlow(true);
}
private void doSuccessfulTest_noAddGracePeriod(String responseFilename) throws Exception {
doSuccessfulTest_noAddGracePeriod(responseFilename, ImmutableMap.of());
}
private void doSuccessfulTest_noAddGracePeriod(
String responseFilename, Map<String, String> substitutions) throws Exception {
// Persist the billing event so it can be retrieved for cancellation generation and checking.
setUpSuccessfulTest();
BillingEvent renewBillingEvent =
persistResource(createBillingEvent(Reason.RENEW, Money.of(USD, 456)));
setUpGracePeriods(
GracePeriod.forBillingEvent(GracePeriodStatus.RENEW, domain.getRepoId(), renewBillingEvent),
// This grace period has no associated billing event, so it won't cause a cancellation.
GracePeriod.create(
GracePeriodStatus.TRANSFER,
domain.getRepoId(),
TIME_BEFORE_FLOW.plusDays(1),
"NewRegistrar",
null));
// We should see exactly one poll message, which is for the autorenew 1 month in the future.
assertPollMessages(createAutorenewPollMessage("TheRegistrar").build());
DateTime expectedExpirationTime = domain.getRegistrationExpirationTime().minusYears(2);
clock.advanceOneMilli();
runFlowAssertResponse(loadFile(responseFilename, substitutions));
Domain resource = reloadResourceByForeignKey();
// Check that the domain is in the pending delete state.
assertAboutDomains()
.that(resource)
.hasStatusValue(StatusValue.PENDING_DELETE)
.and()
.hasDeletionTime(
clock
.nowUtc()
.plus(Tld.get("tld").getRedemptionGracePeriodLength())
.plus(Tld.get("tld").getPendingDeleteLength()))
.and()
.hasExactlyStatusValues(StatusValue.INACTIVE, StatusValue.PENDING_DELETE)
.and()
.hasOneHistoryEntryEachOfTypes(DOMAIN_CREATE, DOMAIN_DELETE);
// We leave the original expiration time unchanged; if the expiration time is before the
// deletion time, that means once it passes the domain will experience a "phantom autorenew"
// where the expirationTime advances and the grace period appears, but since the delete flow
// closed the autorenew recurrences immediately, there are no other autorenew effects.
assertAboutDomains().that(resource).hasRegistrationExpirationTime(expectedExpirationTime);
assertLastHistoryContainsResource(resource);
// All existing grace periods that were for billable actions should cause cancellations.
assertAutorenewClosedAndCancellationCreatedFor(
renewBillingEvent, getOnlyHistoryEntryOfType(resource, DOMAIN_DELETE, DomainHistory.class));
// All existing grace periods should be gone, and a new REDEMPTION one should be added.
assertThat(resource.getGracePeriods())
.containsExactly(
GracePeriod.create(
GracePeriodStatus.REDEMPTION,
domain.getRepoId(),
clock.nowUtc().plus(Tld.get("tld").getRedemptionGracePeriodLength()),
"TheRegistrar",
null,
resource.getGracePeriods().iterator().next().getGracePeriodId()));
assertDeletionPollMessageFor(resource, "Domain deleted.");
}
private void assertDeletionPollMessageFor(Domain domain, String expectedMessage) {
// There should be a future poll message at the deletion time. The previous autorenew poll
// message should now be deleted.
assertAboutDomains().that(domain).hasDeletePollMessage();
DateTime deletionTime = domain.getDeletionTime();
assertThat(getPollMessages("TheRegistrar", deletionTime.minusMinutes(1))).isEmpty();
assertThat(getPollMessages("TheRegistrar", deletionTime)).hasSize(1);
assertThat(domain.getDeletePollMessage())
.isEqualTo(getOnlyPollMessage("TheRegistrar").createVKey());
PollMessage.OneTime deletePollMessage = loadByKey(domain.getDeletePollMessage());
assertThat(deletePollMessage.getMsg()).isEqualTo(expectedMessage);
}
private void setUpGracePeriods(GracePeriod... gracePeriods) {
domain =
persistResource(
domain.asBuilder().setGracePeriods(ImmutableSet.copyOf(gracePeriods)).build());
}
private void assertAutorenewClosedAndCancellationCreatedFor(
BillingEvent graceBillingEvent, DomainHistory historyEntryDomainDelete) {
assertAutorenewClosedAndCancellationCreatedFor(
graceBillingEvent, historyEntryDomainDelete, clock.nowUtc());
}
private void assertAutorenewClosedAndCancellationCreatedFor(
BillingEvent graceBillingEvent, DomainHistory historyEntryDomainDelete, DateTime eventTime) {
assertBillingEvents(
createAutorenewBillingEvent("TheRegistrar").setRecurrenceEndTime(eventTime).build(),
graceBillingEvent,
new BillingCancellation.Builder()
.setReason(graceBillingEvent.getReason())
.setTargetId("example.tld")
.setRegistrarId("TheRegistrar")
.setEventTime(eventTime)
.setBillingTime(TIME_BEFORE_FLOW.plusDays(1))
.setBillingEvent(graceBillingEvent.createVKey())
.setDomainHistory(historyEntryDomainDelete)
.build());
}
private BillingRecurrence.Builder createAutorenewBillingEvent(String registrarId) {
return new BillingRecurrence.Builder()
.setReason(Reason.RENEW)
.setFlags(ImmutableSet.of(Flag.AUTO_RENEW))
.setTargetId("example.tld")
.setRegistrarId(registrarId)
.setEventTime(A_MONTH_FROM_NOW)
.setRecurrenceEndTime(END_OF_TIME)
.setDomainHistory(earlierHistoryEntry);
}
private PollMessage.Autorenew.Builder createAutorenewPollMessage(String registrarId) {
return new PollMessage.Autorenew.Builder()
.setTargetId("example.tld")
.setRegistrarId(registrarId)
.setEventTime(A_MONTH_FROM_NOW)
.setAutorenewEndTime(END_OF_TIME)
.setHistoryEntry(earlierHistoryEntry);
}
private BillingEvent createBillingEvent(Reason reason, Money cost) {
return new BillingEvent.Builder()
.setReason(reason)
.setTargetId("example.tld")
.setRegistrarId("TheRegistrar")
.setCost(cost)
.setPeriodYears(2)
.setEventTime(TIME_BEFORE_FLOW.minusDays(4))
.setBillingTime(TIME_BEFORE_FLOW.plusDays(1))
.setDomainHistory(earlierHistoryEntry)
.build();
}
private void doAddGracePeriodDeleteTest(
GracePeriodStatus gracePeriodStatus, String responseFilename) throws Exception {
doAddGracePeriodDeleteTest(gracePeriodStatus, responseFilename, ImmutableMap.of());
}
private void doAddGracePeriodDeleteTest(
GracePeriodStatus gracePeriodStatus,
String responseFilename,
Map<String, String> substitutions)
throws Exception {
// Persist the billing event so it can be retrieved for cancellation generation and checking.
setUpSuccessfulTest();
BillingEvent graceBillingEvent =
persistResource(createBillingEvent(Reason.CREATE, Money.of(USD, 123)));
setUpGracePeriods(
GracePeriod.forBillingEvent(gracePeriodStatus, domain.getRepoId(), graceBillingEvent));
// We should see exactly one poll message, which is for the autorenew 1 month in the future.
assertPollMessages(createAutorenewPollMessage("TheRegistrar").build());
clock.advanceOneMilli();
runFlowAssertResponse(loadFile(responseFilename, substitutions));
// Check that the domain is fully deleted.
assertThat(reloadResourceByForeignKey()).isNull();
// The add grace period is for a billable action, so it should trigger a cancellation.
assertAutorenewClosedAndCancellationCreatedFor(
graceBillingEvent, getOnlyHistoryEntryOfType(domain, DOMAIN_DELETE, DomainHistory.class));
assertDomainDnsRequests("example.tld");
// There should be no poll messages. The previous autorenew poll message should now be deleted.
assertThat(getPollMessages("TheRegistrar")).isEmpty();
}
private void setUpAutorenewGracePeriod() throws Exception {
createReferencedEntities(A_MONTH_AGO.plusYears(1));
BillingRecurrence autorenewBillingEvent =
persistResource(
createAutorenewBillingEvent("TheRegistrar").setEventTime(A_MONTH_AGO).build());
PollMessage.Autorenew autorenewPollMessage =
persistResource(
createAutorenewPollMessage("TheRegistrar").setEventTime(A_MONTH_AGO).build());
domain =
persistResource(
domain
.asBuilder()
.setGracePeriods(
ImmutableSet.of(
GracePeriod.createForRecurrence(
GracePeriodStatus.AUTO_RENEW,
domain.getRepoId(),
A_MONTH_AGO.plusDays(45),
"TheRegistrar",
autorenewBillingEvent.createVKey())))
.setAutorenewBillingEvent(autorenewBillingEvent.createVKey())
.setAutorenewPollMessage(autorenewPollMessage.createVKey())
.build());
assertMutatingFlow(true);
}
}

View File

@@ -86,7 +86,6 @@ import google.registry.model.domain.Domain;
import google.registry.model.domain.DomainHistory;
import google.registry.model.domain.GracePeriod;
import google.registry.model.domain.rgp.GracePeriodStatus;
import google.registry.model.eppcommon.ProtocolDefinition.ServiceExtension;
import google.registry.model.eppcommon.StatusValue;
import google.registry.model.eppcommon.Trid;
import google.registry.model.host.Host;
@@ -122,12 +121,8 @@ class DomainDeleteFlowTest extends ResourceFlowTestCase<DomainDeleteFlow, Domain
private static final DateTime A_MONTH_AGO = TIME_BEFORE_FLOW.minusMonths(1);
private static final DateTime A_MONTH_FROM_NOW = TIME_BEFORE_FLOW.plusMonths(1);
private static final ImmutableMap<String, String> FEE_06_MAP =
ImmutableMap.of("FEE_VERSION", "0.6", "FEE_NS", "fee");
private static final ImmutableMap<String, String> FEE_11_MAP =
ImmutableMap.of("FEE_VERSION", "0.11", "FEE_NS", "fee11");
private static final ImmutableMap<String, String> FEE_12_MAP =
ImmutableMap.of("FEE_VERSION", "0.12", "FEE_NS", "fee12");
private static final ImmutableMap<String, String> FEE_STD_1_0_MAP =
ImmutableMap.of("FEE_VERSION", "epp:fee-1.0", "FEE_NS", "fee1_00");
DomainDeleteFlowTest() {
setEppInput("domain_delete.xml");
@@ -396,24 +391,9 @@ class DomainDeleteFlowTest extends ResourceFlowTestCase<DomainDeleteFlow, Domain
}
@Test
void testSuccess_addGracePeriodCredit_v06() throws Exception {
removeServiceExtensionUri(ServiceExtension.FEE_0_11.getUri());
removeServiceExtensionUri(ServiceExtension.FEE_0_12.getUri());
removeServiceExtensionUri(ServiceExtension.FEE_1_00.getUri());
doAddGracePeriodDeleteTest(GracePeriodStatus.ADD, "domain_delete_response_fee.xml", FEE_06_MAP);
}
@Test
void testSuccess_addGracePeriodCredit_v11() throws Exception {
removeServiceExtensionUri(ServiceExtension.FEE_0_12.getUri());
removeServiceExtensionUri(ServiceExtension.FEE_1_00.getUri());
doAddGracePeriodDeleteTest(GracePeriodStatus.ADD, "domain_delete_response_fee.xml", FEE_11_MAP);
}
@Test
void testSuccess_addGracePeriodCredit_v12() throws Exception {
removeServiceExtensionUri(ServiceExtension.FEE_1_00.getUri());
doAddGracePeriodDeleteTest(GracePeriodStatus.ADD, "domain_delete_response_fee.xml", FEE_12_MAP);
void testSuccess_addGracePeriodCredit_std_v1() throws Exception {
doAddGracePeriodDeleteTest(
GracePeriodStatus.ADD, "domain_delete_response_fee.xml", FEE_STD_1_0_MAP);
}
private void doSuccessfulTest_noAddGracePeriod(String responseFilename) throws Exception {
@@ -497,24 +477,8 @@ class DomainDeleteFlowTest extends ResourceFlowTestCase<DomainDeleteFlow, Domain
}
@Test
void testSuccess_renewGracePeriodCredit_v06() throws Exception {
removeServiceExtensionUri(ServiceExtension.FEE_0_11.getUri());
removeServiceExtensionUri(ServiceExtension.FEE_0_12.getUri());
removeServiceExtensionUri(ServiceExtension.FEE_1_00.getUri());
doSuccessfulTest_noAddGracePeriod("domain_delete_response_pending_fee.xml", FEE_06_MAP);
}
@Test
void testSuccess_renewGracePeriodCredit_v11() throws Exception {
removeServiceExtensionUri(ServiceExtension.FEE_0_12.getUri());
removeServiceExtensionUri(ServiceExtension.FEE_1_00.getUri());
doSuccessfulTest_noAddGracePeriod("domain_delete_response_pending_fee.xml", FEE_11_MAP);
}
@Test
void testSuccess_renewGracePeriodCredit_v12() throws Exception {
removeServiceExtensionUri(ServiceExtension.FEE_1_00.getUri());
doSuccessfulTest_noAddGracePeriod("domain_delete_response_pending_fee.xml", FEE_12_MAP);
void testSuccess_renewGracePeriodCredit_std_v1() throws Exception {
doSuccessfulTest_noAddGracePeriod("domain_delete_response_pending_fee.xml", FEE_STD_1_0_MAP);
}
@Test
@@ -556,37 +520,14 @@ class DomainDeleteFlowTest extends ResourceFlowTestCase<DomainDeleteFlow, Domain
}
@Test
void testSuccess_autoRenewGracePeriod_v06() throws Exception {
removeServiceExtensionUri(ServiceExtension.FEE_0_11.getUri());
removeServiceExtensionUri(ServiceExtension.FEE_0_12.getUri());
removeServiceExtensionUri(ServiceExtension.FEE_1_00.getUri());
void testSuccess_autoRenewGracePeriod_std_v1() throws Exception {
setUpAutorenewGracePeriod();
clock.advanceOneMilli();
runFlowAssertResponse(loadFile("domain_delete_response_autorenew_fee.xml", FEE_06_MAP));
runFlowAssertResponse(loadFile("domain_delete_response_autorenew_fee.xml", FEE_STD_1_0_MAP));
}
@Test
void testSuccess_autoRenewGracePeriod_v11() throws Exception {
removeServiceExtensionUri(ServiceExtension.FEE_0_12.getUri());
removeServiceExtensionUri(ServiceExtension.FEE_1_00.getUri());
setUpAutorenewGracePeriod();
clock.advanceOneMilli();
runFlowAssertResponse(loadFile("domain_delete_response_autorenew_fee.xml", FEE_11_MAP));
}
@Test
void testSuccess_autoRenewGracePeriod_v12() throws Exception {
removeServiceExtensionUri(ServiceExtension.FEE_1_00.getUri());
setUpAutorenewGracePeriod();
clock.advanceOneMilli();
runFlowAssertResponse(loadFile("domain_delete_response_autorenew_fee.xml", FEE_12_MAP));
}
@Test
void testSuccess_autoRenewGracePeriod_priceChanges_v06() throws Exception {
removeServiceExtensionUri(ServiceExtension.FEE_0_11.getUri());
removeServiceExtensionUri(ServiceExtension.FEE_0_12.getUri());
removeServiceExtensionUri(ServiceExtension.FEE_1_00.getUri());
void testSuccess_autoRenewGracePeriod_priceChanges_std_v1() throws Exception {
persistResource(
Tld.get("tld")
.asBuilder()
@@ -599,44 +540,7 @@ class DomainDeleteFlowTest extends ResourceFlowTestCase<DomainDeleteFlow, Domain
.build());
setUpAutorenewGracePeriod();
clock.advanceOneMilli();
runFlowAssertResponse(loadFile("domain_delete_response_autorenew_fee.xml", FEE_06_MAP));
}
@Test
void testSuccess_autoRenewGracePeriod_priceChanges_v11() throws Exception {
removeServiceExtensionUri(ServiceExtension.FEE_0_12.getUri());
removeServiceExtensionUri(ServiceExtension.FEE_1_00.getUri());
persistResource(
Tld.get("tld")
.asBuilder()
.setRenewBillingCostTransitions(
ImmutableSortedMap.of(
START_OF_TIME,
Money.of(USD, 11),
TIME_BEFORE_FLOW.minusDays(5),
Money.of(USD, 20)))
.build());
setUpAutorenewGracePeriod();
clock.advanceOneMilli();
runFlowAssertResponse(loadFile("domain_delete_response_autorenew_fee.xml", FEE_11_MAP));
}
@Test
void testSuccess_autoRenewGracePeriod_priceChanges_v12() throws Exception {
removeServiceExtensionUri(ServiceExtension.FEE_1_00.getUri());
persistResource(
Tld.get("tld")
.asBuilder()
.setRenewBillingCostTransitions(
ImmutableSortedMap.of(
START_OF_TIME,
Money.of(USD, 11),
TIME_BEFORE_FLOW.minusDays(5),
Money.of(USD, 20)))
.build());
setUpAutorenewGracePeriod();
clock.advanceOneMilli();
runFlowAssertResponse(loadFile("domain_delete_response_autorenew_fee.xml", FEE_12_MAP));
runFlowAssertResponse(loadFile("domain_delete_response_autorenew_fee.xml", FEE_STD_1_0_MAP));
}
@Test
@@ -1296,7 +1200,6 @@ class DomainDeleteFlowTest extends ResourceFlowTestCase<DomainDeleteFlow, Domain
@Test
void testSuccess_freeCreation_deletionDuringGracePeriod() throws Exception {
removeServiceExtensionUri(ServiceExtension.FEE_1_00.getUri());
// Deletion during the add grace period should still work even if the credit is 0
setUpSuccessfulTest();
BillingEvent graceBillingEvent =
@@ -1304,7 +1207,7 @@ class DomainDeleteFlowTest extends ResourceFlowTestCase<DomainDeleteFlow, Domain
setUpGracePeriods(
GracePeriod.forBillingEvent(GracePeriodStatus.ADD, domain.getRepoId(), graceBillingEvent));
clock.advanceOneMilli();
runFlowAssertResponse(loadFile("domain_delete_response_fee_free_grace.xml"));
runFlowAssertResponse(loadFile("domain_delete_response_fee_free_grace_stdv1.xml"));
}
@Test

View File

@@ -0,0 +1,523 @@
// 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.flows.domain;
import static google.registry.model.billing.BillingBase.RenewalPriceBehavior.DEFAULT;
import static google.registry.model.billing.BillingBase.RenewalPriceBehavior.NONPREMIUM;
import static google.registry.model.billing.BillingBase.RenewalPriceBehavior.SPECIFIED;
import static google.registry.testing.DatabaseHelper.assertNoBillingEvents;
import static google.registry.testing.DatabaseHelper.createTld;
import static google.registry.testing.DatabaseHelper.persistActiveHost;
import static google.registry.testing.DatabaseHelper.persistBillingRecurrenceForDomain;
import static google.registry.testing.DatabaseHelper.persistPremiumList;
import static google.registry.testing.DatabaseHelper.persistResource;
import static google.registry.testing.EppExceptionSubject.assertAboutEppExceptions;
import static google.registry.testing.TestDataHelper.updateSubstitutions;
import static org.joda.money.CurrencyUnit.USD;
import static org.junit.jupiter.api.Assertions.assertThrows;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import google.registry.flows.EppException;
import google.registry.flows.FlowUtils.UnknownCurrencyEppException;
import google.registry.flows.domain.DomainFlowUtils.BadPeriodUnitException;
import google.registry.flows.domain.DomainFlowUtils.CurrencyUnitMismatchException;
import google.registry.flows.domain.DomainFlowUtils.FeeChecksDontSupportPhasesException;
import google.registry.flows.domain.DomainFlowUtils.RestoresAreAlwaysForOneYearException;
import google.registry.flows.domain.DomainFlowUtils.TransfersAreAlwaysForOneYearException;
import google.registry.model.billing.BillingBase.RenewalPriceBehavior;
import google.registry.model.domain.Domain;
import google.registry.model.domain.DomainAuthInfo;
import google.registry.model.eppcommon.AuthInfo.PasswordAuth;
import google.registry.model.eppcommon.StatusValue;
import google.registry.model.host.Host;
import google.registry.model.tld.Tld;
import google.registry.persistence.transaction.JpaTransactionManagerExtension;
import java.util.regex.Pattern;
import javax.annotation.Nullable;
import org.joda.money.Money;
import org.joda.time.DateTime;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
/** Tests for {@link DomainInfoFlow} that use the old fee extensions (0.6, 0.11, 0.12). */
public class DomainInfoFlowOldFeeExtensionsTest
extends ProductionSimulatingFeeExtensionsTest<DomainInfoFlow> {
/**
* The domain_info_fee.xml default substitutions common to most tests.
*
* <p>It doesn't set a default value to the COMMAND and PERIOD keys, because they are different in
* every test.
*/
private static final ImmutableMap<String, String> SUBSTITUTION_BASE =
ImmutableMap.of(
"NAME", "example.tld",
"CURRENCY", "USD",
"UNIT", "y");
private static final Pattern OK_PATTERN = Pattern.compile("\"ok\"");
private Host host1;
private Host host2;
private Host host3;
private Domain domain;
@BeforeEach
void beforeEachDomainInfoFlowOldFeeExtensionsTest() {
setEppInput("domain_info.xml");
clock.setTo(DateTime.parse("2005-03-03T22:00:00.000Z"));
sessionMetadata.setRegistrarId("NewRegistrar");
createTld("tld");
persistResource(
JpaTransactionManagerExtension.makeRegistrar1()
.asBuilder()
.setRegistrarId("ClientZ")
.build());
}
private void persistTestEntities(String domainName, boolean inactive) {
host1 = persistActiveHost("ns1.example.tld");
host2 = persistActiveHost("ns1.example.net");
domain =
persistResource(
new Domain.Builder()
.setDomainName(domainName)
.setRepoId("2FF-TLD")
.setPersistedCurrentSponsorRegistrarId("NewRegistrar")
.setCreationRegistrarId("TheRegistrar")
.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"))
.setRegistrationExpirationTime(DateTime.parse("2005-04-03T22:00:00.0Z"))
.setNameservers(
inactive ? null : ImmutableSet.of(host1.createVKey(), host2.createVKey()))
.setAuthInfo(DomainAuthInfo.create(PasswordAuth.create("2fooBAR")))
.build());
// Set the superordinate domain of ns1.example.com to example.com. In reality, this would have
// happened in the flow that created it, but here we just overwrite it in the database.
host1 = persistResource(host1.asBuilder().setSuperordinateDomain(domain.createVKey()).build());
// Create a subordinate host that is not delegated to by anyone.
host3 =
persistResource(
new Host.Builder()
.setHostName("ns2.example.tld")
.setRepoId("3FF-TLD")
.setSuperordinateDomain(domain.createVKey())
.build());
// Add the subordinate host keys to the existing domain.
domain =
persistResource(
domain
.asBuilder()
.setSubordinateHosts(ImmutableSet.of(host1.getHostName(), host3.getHostName()))
.build());
}
private void persistTestEntities(boolean inactive) {
persistTestEntities("example.tld", inactive);
}
private void doSuccessfulTest(
String expectedXmlFilename,
boolean inactive,
ImmutableMap<String, String> substitutions,
boolean expectHistoryAndBilling)
throws Exception {
assertMutatingFlow(true);
String expected =
loadFile(expectedXmlFilename, updateSubstitutions(substitutions, "ROID", "2FF-TLD"));
if (inactive) {
expected = OK_PATTERN.matcher(expected).replaceAll("\"inactive\"");
}
runFlowAssertResponse(expected);
if (!expectHistoryAndBilling) {
assertNoHistory();
assertNoBillingEvents();
}
}
private void doSuccessfulTest(String expectedXmlFilename, boolean inactive) throws Exception {
doSuccessfulTest(expectedXmlFilename, inactive, ImmutableMap.of(), false);
}
private void doSuccessfulTest(String expectedXmlFilename) throws Exception {
persistTestEntities(false);
doSuccessfulTest(expectedXmlFilename, false);
}
private void doSuccessfulTestNoNameservers(String expectedXmlFilename) throws Exception {
persistTestEntities(true);
doSuccessfulTest(expectedXmlFilename, true);
}
/** sets up a sample recurring billing event as part of the domain creation process. */
private void setUpBillingEventForExistingDomain() {
setUpBillingEventForExistingDomain(DEFAULT, null);
}
private void setUpBillingEventForExistingDomain(
RenewalPriceBehavior renewalPriceBehavior, @Nullable Money renewalPrice) {
domain = persistBillingRecurrenceForDomain(domain, renewalPriceBehavior, renewalPrice);
}
@Test
void testFeeExtension_restoreCommand_pendingDelete_withRenewal() throws Exception {
createTld("example");
setEppInput(
"domain_info_fee.xml",
updateSubstitutions(
SUBSTITUTION_BASE, "NAME", "rich.example", "COMMAND", "restore", "PERIOD", "1"));
persistTestEntities("rich.example", false);
setUpBillingEventForExistingDomain();
persistResource(
domain
.asBuilder()
.setDeletionTime(clock.nowUtc().plusDays(25))
.setRegistrationExpirationTime(clock.nowUtc().minusDays(1))
.setStatusValues(ImmutableSet.of(StatusValue.PENDING_DELETE))
.build());
doSuccessfulTest(
"domain_info_fee_restore_response_with_renewal.xml", false, ImmutableMap.of(), true);
}
/**
* Test create command. Fee extension version 6 is the only one which supports fee extensions on
* info commands and responses, so we don't need to test the other versions.
*/
@Test
void testFeeExtension_createCommand() throws Exception {
setEppInput(
"domain_info_fee.xml",
updateSubstitutions(SUBSTITUTION_BASE, "COMMAND", "create", "PERIOD", "2"));
persistTestEntities(false);
setUpBillingEventForExistingDomain();
doSuccessfulTest(
"domain_info_fee_response.xml",
false,
ImmutableMap.of(
"COMMAND", "create",
"DESCRIPTION", "create",
"PERIOD", "2",
"FEE", "24.00"),
true);
}
/** Test renew command. */
@Test
void testFeeExtension_renewCommand() throws Exception {
setEppInput(
"domain_info_fee.xml",
updateSubstitutions(SUBSTITUTION_BASE, "COMMAND", "renew", "PERIOD", "2"));
persistTestEntities(false);
setUpBillingEventForExistingDomain();
doSuccessfulTest(
"domain_info_fee_response.xml",
false,
ImmutableMap.of(
"COMMAND", "renew",
"DESCRIPTION", "renew",
"PERIOD", "2",
"FEE", "22.00"),
true);
}
/** Test transfer command. */
@Test
void testFeeExtension_transferCommand() throws Exception {
setEppInput(
"domain_info_fee.xml",
updateSubstitutions(SUBSTITUTION_BASE, "COMMAND", "transfer", "PERIOD", "1"));
persistTestEntities(false);
setUpBillingEventForExistingDomain();
doSuccessfulTest(
"domain_info_fee_response.xml",
false,
ImmutableMap.of(
"COMMAND", "transfer",
"DESCRIPTION", "renew",
"PERIOD", "1",
"FEE", "11.00"),
true);
}
/** Test restore command. */
@Test
void testFeeExtension_restoreCommand() throws Exception {
setEppInput(
"domain_info_fee.xml",
updateSubstitutions(SUBSTITUTION_BASE, "COMMAND", "restore", "PERIOD", "1"));
persistTestEntities(false);
setUpBillingEventForExistingDomain();
doSuccessfulTest("domain_info_fee_restore_response.xml", false, ImmutableMap.of(), true);
}
@Test
void testFeeExtension_restoreCommand_pendingDelete_noRenewal() throws Exception {
setEppInput(
"domain_info_fee.xml",
updateSubstitutions(SUBSTITUTION_BASE, "COMMAND", "restore", "PERIOD", "1"));
persistTestEntities(false);
setUpBillingEventForExistingDomain();
persistResource(
domain
.asBuilder()
.setDeletionTime(clock.nowUtc().plusDays(25))
.setStatusValues(ImmutableSet.of(StatusValue.PENDING_DELETE))
.build());
doSuccessfulTest(
"domain_info_fee_restore_response_no_renewal.xml", false, ImmutableMap.of(), true);
}
/** Test create command on a premium label. */
@Test
void testFeeExtension_createCommandPremium() throws Exception {
createTld("example");
setEppInput(
"domain_info_fee.xml",
updateSubstitutions(
SUBSTITUTION_BASE, "NAME", "rich.example", "COMMAND", "create", "PERIOD", "1"));
persistTestEntities("rich.example", false);
setUpBillingEventForExistingDomain();
doSuccessfulTest(
"domain_info_fee_premium_response.xml",
false,
ImmutableMap.of("COMMAND", "create", "DESCRIPTION", "create"),
true);
}
/** Test renew command on a premium label. */
@Test
void testFeeExtension_renewCommandPremium() throws Exception {
createTld("example");
setEppInput(
"domain_info_fee.xml",
updateSubstitutions(
SUBSTITUTION_BASE, "NAME", "rich.example", "COMMAND", "renew", "PERIOD", "1"));
persistTestEntities("rich.example", false);
setUpBillingEventForExistingDomain();
doSuccessfulTest(
"domain_info_fee_premium_response.xml",
false,
ImmutableMap.of("COMMAND", "renew", "DESCRIPTION", "renew"),
true);
}
@Test
void testFeeExtension_renewCommandPremium_anchorTenant() throws Exception {
createTld("tld");
persistResource(
Tld.get("tld")
.asBuilder()
.setPremiumList(persistPremiumList("tld", USD, "example,USD 70"))
.build());
setEppInput(
"domain_info_fee.xml",
updateSubstitutions(SUBSTITUTION_BASE, "COMMAND", "renew", "PERIOD", "1"));
persistTestEntities("example.tld", false);
setUpBillingEventForExistingDomain(NONPREMIUM, null);
doSuccessfulTest(
"domain_info_fee_response.xml",
false,
ImmutableMap.of("COMMAND", "renew", "DESCRIPTION", "renew", "FEE", "11.00", "PERIOD", "1"),
true);
}
@Test
void testFeeExtension_renewCommandPremium_internalRegistration() throws Exception {
createTld("tld");
persistResource(
Tld.get("tld")
.asBuilder()
.setPremiumList(persistPremiumList("tld", USD, "example,USD 70"))
.build());
setEppInput(
"domain_info_fee.xml",
updateSubstitutions(SUBSTITUTION_BASE, "COMMAND", "renew", "PERIOD", "1"));
persistTestEntities("example.tld", false);
setUpBillingEventForExistingDomain(SPECIFIED, Money.of(USD, 3));
doSuccessfulTest(
"domain_info_fee_response.xml",
false,
ImmutableMap.of("COMMAND", "renew", "DESCRIPTION", "renew", "FEE", "3.00", "PERIOD", "1"),
true);
}
@Test
void testFeeExtension_renewCommandPremium_anchorTenant_multiYear() throws Exception {
createTld("tld");
persistResource(
Tld.get("tld")
.asBuilder()
.setPremiumList(persistPremiumList("tld", USD, "example,USD 70"))
.build());
setEppInput(
"domain_info_fee.xml",
updateSubstitutions(SUBSTITUTION_BASE, "COMMAND", "renew", "PERIOD", "3"));
persistTestEntities("example.tld", false);
setUpBillingEventForExistingDomain(NONPREMIUM, null);
doSuccessfulTest(
"domain_info_fee_response.xml",
false,
ImmutableMap.of("COMMAND", "renew", "DESCRIPTION", "renew", "FEE", "33.00", "PERIOD", "3"),
true);
}
@Test
void testFeeExtension_renewCommandPremium_internalRegistration_multiYear() throws Exception {
createTld("tld");
persistResource(
Tld.get("tld")
.asBuilder()
.setPremiumList(persistPremiumList("tld", USD, "example,USD 70"))
.build());
setEppInput(
"domain_info_fee.xml",
updateSubstitutions(SUBSTITUTION_BASE, "COMMAND", "renew", "PERIOD", "3"));
persistTestEntities("example.tld", false);
setUpBillingEventForExistingDomain(SPECIFIED, Money.of(USD, 3));
doSuccessfulTest(
"domain_info_fee_response.xml",
false,
ImmutableMap.of("COMMAND", "renew", "DESCRIPTION", "renew", "FEE", "9.00", "PERIOD", "3"),
true);
}
@Test
void testFeeExtension_renewCommandStandard_internalRegistration() throws Exception {
createTld("tld");
setEppInput(
"domain_info_fee.xml",
updateSubstitutions(SUBSTITUTION_BASE, "COMMAND", "renew", "PERIOD", "1"));
persistTestEntities("example.tld", false);
setUpBillingEventForExistingDomain(SPECIFIED, Money.of(USD, 3));
doSuccessfulTest(
"domain_info_fee_response.xml",
false,
ImmutableMap.of("COMMAND", "renew", "DESCRIPTION", "renew", "FEE", "3.00", "PERIOD", "1"),
true);
}
/** Test transfer command on a premium label. */
@Test
void testFeeExtension_transferCommandPremium() throws Exception {
createTld("example");
setEppInput(
"domain_info_fee.xml",
updateSubstitutions(
SUBSTITUTION_BASE, "NAME", "rich.example", "COMMAND", "transfer", "PERIOD", "1"));
persistTestEntities("rich.example", false);
setUpBillingEventForExistingDomain();
doSuccessfulTest(
"domain_info_fee_premium_response.xml",
false,
ImmutableMap.of("COMMAND", "transfer", "DESCRIPTION", "renew"),
true);
}
/** Test restore command on a premium label. */
@Test
void testFeeExtension_restoreCommandPremium() throws Exception {
createTld("example");
setEppInput(
"domain_info_fee.xml",
updateSubstitutions(
SUBSTITUTION_BASE, "NAME", "rich.example", "COMMAND", "restore", "PERIOD", "1"));
persistTestEntities("rich.example", false);
setUpBillingEventForExistingDomain();
doSuccessfulTest(
"domain_info_fee_restore_premium_response.xml", false, ImmutableMap.of(), true);
}
/** Test setting the currency explicitly to a wrong value. */
@Test
void testFeeExtension_wrongCurrency() {
setEppInput(
"domain_info_fee.xml",
updateSubstitutions(
SUBSTITUTION_BASE, "COMMAND", "create", "CURRENCY", "EUR", "PERIOD", "1"));
persistTestEntities(false);
setUpBillingEventForExistingDomain();
EppException thrown = assertThrows(CurrencyUnitMismatchException.class, this::runFlow);
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
/** Test requesting a period that isn't in years. */
@Test
void testFeeExtension_periodNotInYears() {
setEppInput(
"domain_info_fee.xml",
updateSubstitutions(SUBSTITUTION_BASE, "COMMAND", "create", "PERIOD", "2", "UNIT", "m"));
persistTestEntities(false);
setUpBillingEventForExistingDomain();
EppException thrown = assertThrows(BadPeriodUnitException.class, this::runFlow);
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
/** Test a command that specifies a phase. */
@Test
void testFeeExtension_commandPhase() {
setEppInput("domain_info_fee_command_phase.xml");
persistTestEntities(false);
setUpBillingEventForExistingDomain();
EppException thrown = assertThrows(FeeChecksDontSupportPhasesException.class, this::runFlow);
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
/** Test a command that specifies a subphase. */
@Test
void testFeeExtension_commandSubphase() {
setEppInput("domain_info_fee_command_subphase.xml");
persistTestEntities(false);
setUpBillingEventForExistingDomain();
EppException thrown = assertThrows(FeeChecksDontSupportPhasesException.class, this::runFlow);
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
/** Test a restore for more than one year. */
@Test
void testFeeExtension_multiyearRestore() {
setEppInput(
"domain_info_fee.xml",
updateSubstitutions(SUBSTITUTION_BASE, "COMMAND", "restore", "PERIOD", "2"));
persistTestEntities(false);
setUpBillingEventForExistingDomain();
EppException thrown = assertThrows(RestoresAreAlwaysForOneYearException.class, this::runFlow);
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
/** Test a transfer for more than one year. */
@Test
void testFeeExtension_multiyearTransfer() {
setEppInput(
"domain_info_fee.xml",
updateSubstitutions(SUBSTITUTION_BASE, "COMMAND", "transfer", "PERIOD", "2"));
persistTestEntities(false);
setUpBillingEventForExistingDomain();
EppException thrown = assertThrows(TransfersAreAlwaysForOneYearException.class, this::runFlow);
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
void testFeeExtension_unknownCurrency() {
setEppInput(
"domain_info_fee.xml",
updateSubstitutions(
SUBSTITUTION_BASE, "COMMAND", "create", "CURRENCY", "BAD", "PERIOD", "1"));
EppException thrown = assertThrows(UnknownCurrencyEppException.class, this::runFlow);
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
}

View File

@@ -17,15 +17,12 @@ package google.registry.flows.domain;
import static com.google.common.io.BaseEncoding.base16;
import static com.google.common.truth.Truth.assertThat;
import static google.registry.model.billing.BillingBase.RenewalPriceBehavior.DEFAULT;
import static google.registry.model.billing.BillingBase.RenewalPriceBehavior.NONPREMIUM;
import static google.registry.model.billing.BillingBase.RenewalPriceBehavior.SPECIFIED;
import static google.registry.model.eppcommon.EppXmlTransformer.marshal;
import static google.registry.model.tld.Tld.TldState.QUIET_PERIOD;
import static google.registry.testing.DatabaseHelper.assertNoBillingEvents;
import static google.registry.testing.DatabaseHelper.createTld;
import static google.registry.testing.DatabaseHelper.persistActiveHost;
import static google.registry.testing.DatabaseHelper.persistBillingRecurrenceForDomain;
import static google.registry.testing.DatabaseHelper.persistPremiumList;
import static google.registry.testing.DatabaseHelper.persistResource;
import static google.registry.testing.EppExceptionSubject.assertAboutEppExceptions;
import static google.registry.testing.TestDataHelper.updateSubstitutions;
@@ -41,15 +38,9 @@ import com.google.common.collect.ImmutableSet;
import com.google.common.collect.ImmutableSortedMap;
import google.registry.flows.EppException;
import google.registry.flows.FlowUtils.NotLoggedInException;
import google.registry.flows.FlowUtils.UnknownCurrencyEppException;
import google.registry.flows.ResourceFlowTestCase;
import google.registry.flows.ResourceFlowUtils.BadAuthInfoForResourceException;
import google.registry.flows.ResourceFlowUtils.ResourceDoesNotExistException;
import google.registry.flows.domain.DomainFlowUtils.BadPeriodUnitException;
import google.registry.flows.domain.DomainFlowUtils.CurrencyUnitMismatchException;
import google.registry.flows.domain.DomainFlowUtils.FeeChecksDontSupportPhasesException;
import google.registry.flows.domain.DomainFlowUtils.RestoresAreAlwaysForOneYearException;
import google.registry.flows.domain.DomainFlowUtils.TransfersAreAlwaysForOneYearException;
import google.registry.model.billing.BillingBase.Flag;
import google.registry.model.billing.BillingBase.Reason;
import google.registry.model.billing.BillingBase.RenewalPriceBehavior;
@@ -586,353 +577,6 @@ class DomainInfoFlowTest extends ResourceFlowTestCase<DomainInfoFlow, Domain> {
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
/**
* Test create command. Fee extension version 6 is the only one which supports fee extensions on
* info commands and responses, so we don't need to test the other versions.
*/
@Test
void testFeeExtension_createCommand() throws Exception {
setEppInput(
"domain_info_fee.xml",
updateSubstitutions(SUBSTITUTION_BASE, "COMMAND", "create", "PERIOD", "2"));
persistTestEntities(false);
setUpBillingEventForExistingDomain();
doSuccessfulTest(
"domain_info_fee_response.xml",
false,
ImmutableMap.of(
"COMMAND", "create",
"DESCRIPTION", "create",
"PERIOD", "2",
"FEE", "24.00"),
true);
}
/** Test renew command. */
@Test
void testFeeExtension_renewCommand() throws Exception {
setEppInput(
"domain_info_fee.xml",
updateSubstitutions(SUBSTITUTION_BASE, "COMMAND", "renew", "PERIOD", "2"));
persistTestEntities(false);
setUpBillingEventForExistingDomain();
doSuccessfulTest(
"domain_info_fee_response.xml",
false,
ImmutableMap.of(
"COMMAND", "renew",
"DESCRIPTION", "renew",
"PERIOD", "2",
"FEE", "22.00"),
true);
}
/** Test transfer command. */
@Test
void testFeeExtension_transferCommand() throws Exception {
setEppInput(
"domain_info_fee.xml",
updateSubstitutions(SUBSTITUTION_BASE, "COMMAND", "transfer", "PERIOD", "1"));
persistTestEntities(false);
setUpBillingEventForExistingDomain();
doSuccessfulTest(
"domain_info_fee_response.xml",
false,
ImmutableMap.of(
"COMMAND", "transfer",
"DESCRIPTION", "renew",
"PERIOD", "1",
"FEE", "11.00"),
true);
}
/** Test restore command. */
@Test
void testFeeExtension_restoreCommand() throws Exception {
setEppInput(
"domain_info_fee.xml",
updateSubstitutions(SUBSTITUTION_BASE, "COMMAND", "restore", "PERIOD", "1"));
persistTestEntities(false);
setUpBillingEventForExistingDomain();
doSuccessfulTest("domain_info_fee_restore_response.xml", false, ImmutableMap.of(), true);
}
@Test
void testFeeExtension_restoreCommand_pendingDelete_noRenewal() throws Exception {
setEppInput(
"domain_info_fee.xml",
updateSubstitutions(SUBSTITUTION_BASE, "COMMAND", "restore", "PERIOD", "1"));
persistTestEntities(false);
setUpBillingEventForExistingDomain();
persistResource(
domain
.asBuilder()
.setDeletionTime(clock.nowUtc().plusDays(25))
.setStatusValues(ImmutableSet.of(StatusValue.PENDING_DELETE))
.build());
doSuccessfulTest(
"domain_info_fee_restore_response_no_renewal.xml", false, ImmutableMap.of(), true);
}
@Test
void testFeeExtension_restoreCommand_pendingDelete_withRenewal() throws Exception {
createTld("example");
setEppInput(
"domain_info_fee.xml",
updateSubstitutions(
SUBSTITUTION_BASE, "NAME", "rich.example", "COMMAND", "restore", "PERIOD", "1"));
persistTestEntities("rich.example", false);
setUpBillingEventForExistingDomain();
persistResource(
domain
.asBuilder()
.setDeletionTime(clock.nowUtc().plusDays(25))
.setRegistrationExpirationTime(clock.nowUtc().minusDays(1))
.setStatusValues(ImmutableSet.of(StatusValue.PENDING_DELETE))
.build());
doSuccessfulTest(
"domain_info_fee_restore_response_with_renewal.xml", false, ImmutableMap.of(), true);
}
/** Test create command on a premium label. */
@Test
void testFeeExtension_createCommandPremium() throws Exception {
createTld("example");
setEppInput(
"domain_info_fee.xml",
updateSubstitutions(
SUBSTITUTION_BASE, "NAME", "rich.example", "COMMAND", "create", "PERIOD", "1"));
persistTestEntities("rich.example", false);
setUpBillingEventForExistingDomain();
doSuccessfulTest(
"domain_info_fee_premium_response.xml",
false,
ImmutableMap.of("COMMAND", "create", "DESCRIPTION", "create"),
true);
}
/** Test renew command on a premium label. */
@Test
void testFeeExtension_renewCommandPremium() throws Exception {
createTld("example");
setEppInput(
"domain_info_fee.xml",
updateSubstitutions(
SUBSTITUTION_BASE, "NAME", "rich.example", "COMMAND", "renew", "PERIOD", "1"));
persistTestEntities("rich.example", false);
setUpBillingEventForExistingDomain();
doSuccessfulTest(
"domain_info_fee_premium_response.xml",
false,
ImmutableMap.of("COMMAND", "renew", "DESCRIPTION", "renew"),
true);
}
@Test
void testFeeExtension_renewCommandPremium_anchorTenant() throws Exception {
createTld("tld");
persistResource(
Tld.get("tld")
.asBuilder()
.setPremiumList(persistPremiumList("tld", USD, "example,USD 70"))
.build());
setEppInput(
"domain_info_fee.xml",
updateSubstitutions(SUBSTITUTION_BASE, "COMMAND", "renew", "PERIOD", "1"));
persistTestEntities("example.tld", false);
setUpBillingEventForExistingDomain(NONPREMIUM, null);
doSuccessfulTest(
"domain_info_fee_response.xml",
false,
ImmutableMap.of("COMMAND", "renew", "DESCRIPTION", "renew", "FEE", "11.00", "PERIOD", "1"),
true);
}
@Test
void testFeeExtension_renewCommandPremium_internalRegistration() throws Exception {
createTld("tld");
persistResource(
Tld.get("tld")
.asBuilder()
.setPremiumList(persistPremiumList("tld", USD, "example,USD 70"))
.build());
setEppInput(
"domain_info_fee.xml",
updateSubstitutions(SUBSTITUTION_BASE, "COMMAND", "renew", "PERIOD", "1"));
persistTestEntities("example.tld", false);
setUpBillingEventForExistingDomain(SPECIFIED, Money.of(USD, 3));
doSuccessfulTest(
"domain_info_fee_response.xml",
false,
ImmutableMap.of("COMMAND", "renew", "DESCRIPTION", "renew", "FEE", "3.00", "PERIOD", "1"),
true);
}
@Test
void testFeeExtension_renewCommandPremium_anchorTenant_multiYear() throws Exception {
createTld("tld");
persistResource(
Tld.get("tld")
.asBuilder()
.setPremiumList(persistPremiumList("tld", USD, "example,USD 70"))
.build());
setEppInput(
"domain_info_fee.xml",
updateSubstitutions(SUBSTITUTION_BASE, "COMMAND", "renew", "PERIOD", "3"));
persistTestEntities("example.tld", false);
setUpBillingEventForExistingDomain(NONPREMIUM, null);
doSuccessfulTest(
"domain_info_fee_response.xml",
false,
ImmutableMap.of("COMMAND", "renew", "DESCRIPTION", "renew", "FEE", "33.00", "PERIOD", "3"),
true);
}
@Test
void testFeeExtension_renewCommandPremium_internalRegistration_multiYear() throws Exception {
createTld("tld");
persistResource(
Tld.get("tld")
.asBuilder()
.setPremiumList(persistPremiumList("tld", USD, "example,USD 70"))
.build());
setEppInput(
"domain_info_fee.xml",
updateSubstitutions(SUBSTITUTION_BASE, "COMMAND", "renew", "PERIOD", "3"));
persistTestEntities("example.tld", false);
setUpBillingEventForExistingDomain(SPECIFIED, Money.of(USD, 3));
doSuccessfulTest(
"domain_info_fee_response.xml",
false,
ImmutableMap.of("COMMAND", "renew", "DESCRIPTION", "renew", "FEE", "9.00", "PERIOD", "3"),
true);
}
@Test
void testFeeExtension_renewCommandStandard_internalRegistration() throws Exception {
createTld("tld");
setEppInput(
"domain_info_fee.xml",
updateSubstitutions(SUBSTITUTION_BASE, "COMMAND", "renew", "PERIOD", "1"));
persistTestEntities("example.tld", false);
setUpBillingEventForExistingDomain(SPECIFIED, Money.of(USD, 3));
doSuccessfulTest(
"domain_info_fee_response.xml",
false,
ImmutableMap.of("COMMAND", "renew", "DESCRIPTION", "renew", "FEE", "3.00", "PERIOD", "1"),
true);
}
/** Test transfer command on a premium label. */
@Test
void testFeeExtension_transferCommandPremium() throws Exception {
createTld("example");
setEppInput(
"domain_info_fee.xml",
updateSubstitutions(
SUBSTITUTION_BASE, "NAME", "rich.example", "COMMAND", "transfer", "PERIOD", "1"));
persistTestEntities("rich.example", false);
setUpBillingEventForExistingDomain();
doSuccessfulTest(
"domain_info_fee_premium_response.xml",
false,
ImmutableMap.of("COMMAND", "transfer", "DESCRIPTION", "renew"),
true);
}
/** Test restore command on a premium label. */
@Test
void testFeeExtension_restoreCommandPremium() throws Exception {
createTld("example");
setEppInput(
"domain_info_fee.xml",
updateSubstitutions(
SUBSTITUTION_BASE, "NAME", "rich.example", "COMMAND", "restore", "PERIOD", "1"));
persistTestEntities("rich.example", false);
setUpBillingEventForExistingDomain();
doSuccessfulTest(
"domain_info_fee_restore_premium_response.xml", false, ImmutableMap.of(), true);
}
/** Test setting the currency explicitly to a wrong value. */
@Test
void testFeeExtension_wrongCurrency() {
setEppInput(
"domain_info_fee.xml",
updateSubstitutions(
SUBSTITUTION_BASE, "COMMAND", "create", "CURRENCY", "EUR", "PERIOD", "1"));
persistTestEntities(false);
setUpBillingEventForExistingDomain();
EppException thrown = assertThrows(CurrencyUnitMismatchException.class, this::runFlow);
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
void testFeeExtension_unknownCurrency() {
setEppInput(
"domain_info_fee.xml",
updateSubstitutions(
SUBSTITUTION_BASE, "COMMAND", "create", "CURRENCY", "BAD", "PERIOD", "1"));
EppException thrown = assertThrows(UnknownCurrencyEppException.class, this::runFlow);
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
/** Test requesting a period that isn't in years. */
@Test
void testFeeExtension_periodNotInYears() {
setEppInput(
"domain_info_fee.xml",
updateSubstitutions(SUBSTITUTION_BASE, "COMMAND", "create", "PERIOD", "2", "UNIT", "m"));
persistTestEntities(false);
setUpBillingEventForExistingDomain();
EppException thrown = assertThrows(BadPeriodUnitException.class, this::runFlow);
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
/** Test a command that specifies a phase. */
@Test
void testFeeExtension_commandPhase() {
setEppInput("domain_info_fee_command_phase.xml");
persistTestEntities(false);
setUpBillingEventForExistingDomain();
EppException thrown = assertThrows(FeeChecksDontSupportPhasesException.class, this::runFlow);
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
/** Test a command that specifies a subphase. */
@Test
void testFeeExtension_commandSubphase() {
setEppInput("domain_info_fee_command_subphase.xml");
persistTestEntities(false);
setUpBillingEventForExistingDomain();
EppException thrown = assertThrows(FeeChecksDontSupportPhasesException.class, this::runFlow);
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
/** Test a restore for more than one year. */
@Test
void testFeeExtension_multiyearRestore() {
setEppInput(
"domain_info_fee.xml",
updateSubstitutions(SUBSTITUTION_BASE, "COMMAND", "restore", "PERIOD", "2"));
persistTestEntities(false);
setUpBillingEventForExistingDomain();
EppException thrown = assertThrows(RestoresAreAlwaysForOneYearException.class, this::runFlow);
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
/** Test a transfer for more than one year. */
@Test
void testFeeExtension_multiyearTransfer() {
setEppInput(
"domain_info_fee.xml",
updateSubstitutions(SUBSTITUTION_BASE, "COMMAND", "transfer", "PERIOD", "2"));
persistTestEntities(false);
setUpBillingEventForExistingDomain();
EppException thrown = assertThrows(TransfersAreAlwaysForOneYearException.class, this::runFlow);
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
void testIcannActivityReportField_getsLogged() throws Exception {
persistTestEntities(false);

View File

@@ -0,0 +1,827 @@
// 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.flows.domain;
import static com.google.common.truth.Truth.assertThat;
import static google.registry.model.billing.BillingBase.RenewalPriceBehavior.DEFAULT;
import static google.registry.model.billing.BillingBase.RenewalPriceBehavior.NONPREMIUM;
import static google.registry.model.billing.BillingBase.RenewalPriceBehavior.SPECIFIED;
import static google.registry.model.domain.token.AllocationToken.TokenType.DEFAULT_PROMO;
import static google.registry.testing.DatabaseHelper.assertBillingEvents;
import static google.registry.testing.DatabaseHelper.assertPollMessages;
import static google.registry.testing.DatabaseHelper.createTld;
import static google.registry.testing.DatabaseHelper.getOnlyHistoryEntryOfType;
import static google.registry.testing.DatabaseHelper.loadByKey;
import static google.registry.testing.DatabaseHelper.loadRegistrar;
import static google.registry.testing.DatabaseHelper.persistPremiumList;
import static google.registry.testing.DatabaseHelper.persistResource;
import static google.registry.testing.DatabaseHelper.persistResources;
import static google.registry.testing.DomainSubject.assertAboutDomains;
import static google.registry.testing.EppExceptionSubject.assertAboutEppExceptions;
import static google.registry.testing.HistoryEntrySubject.assertAboutHistoryEntries;
import static google.registry.testing.TestDataHelper.updateSubstitutions;
import static google.registry.util.DateTimeUtils.END_OF_TIME;
import static google.registry.util.DateTimeUtils.START_OF_TIME;
import static org.joda.money.CurrencyUnit.EUR;
import static org.joda.money.CurrencyUnit.USD;
import static org.junit.jupiter.api.Assertions.assertThrows;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.ImmutableSortedMap;
import com.google.common.collect.Iterables;
import google.registry.flows.EppException;
import google.registry.flows.domain.DomainFlowUtils.CurrencyUnitMismatchException;
import google.registry.flows.domain.DomainFlowUtils.CurrencyValueScaleException;
import google.registry.flows.domain.DomainFlowUtils.FeesMismatchException;
import google.registry.flows.domain.DomainFlowUtils.UnsupportedFeeAttributeException;
import google.registry.model.billing.BillingBase.Flag;
import google.registry.model.billing.BillingBase.Reason;
import google.registry.model.billing.BillingBase.RenewalPriceBehavior;
import google.registry.model.billing.BillingEvent;
import google.registry.model.billing.BillingRecurrence;
import google.registry.model.domain.Domain;
import google.registry.model.domain.DomainHistory;
import google.registry.model.domain.GracePeriod;
import google.registry.model.domain.rgp.GracePeriodStatus;
import google.registry.model.domain.token.AllocationToken;
import google.registry.model.eppcommon.StatusValue;
import google.registry.model.poll.PollMessage;
import google.registry.model.reporting.HistoryEntry;
import google.registry.model.tld.Tld;
import google.registry.testing.DatabaseHelper;
import java.util.Map;
import javax.annotation.Nullable;
import org.joda.money.Money;
import org.joda.time.DateTime;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
/** Tests for {@link DomainRenewFlow} that use the old fee extensions (0.6, 0.11, 0.12). */
public class DomainRenewFlowOldFeeExtensionsTest
extends ProductionSimulatingFeeExtensionsTest<DomainRenewFlow> {
private static final ImmutableMap<String, String> FEE_BASE_MAP =
ImmutableMap.of(
"NAME", "example.tld",
"PERIOD", "5",
"EX_DATE", "2005-04-03T22:00:00.0Z",
"FEE", "55.00",
"CURRENCY", "USD");
private static final ImmutableMap<String, String> FEE_06_MAP =
updateSubstitutions(FEE_BASE_MAP, "FEE_VERSION", "fee-0.6", "FEE_NS", "fee");
private static final ImmutableMap<String, String> FEE_11_MAP =
updateSubstitutions(FEE_BASE_MAP, "FEE_VERSION", "fee-0.11", "FEE_NS", "fee11");
private static final ImmutableMap<String, String> FEE_12_MAP =
updateSubstitutions(FEE_BASE_MAP, "FEE_VERSION", "fee-0.12", "FEE_NS", "fee12");
private final DateTime expirationTime = DateTime.parse("2000-04-03T22:00:00.0Z");
@BeforeEach
void beforeEachDomainRenewFlowOldFeeExtensionsTest() {
clock.setTo(expirationTime.minusMillis(20));
createTld("tld");
persistResource(
loadRegistrar("TheRegistrar")
.asBuilder()
.setBillingAccountMap(ImmutableMap.of(USD, "123", EUR, "567"))
.build());
setEppInput("domain_renew.xml", ImmutableMap.of("DOMAIN", "example.tld", "YEARS", "5"));
}
private void persistDomain(StatusValue... statusValues) throws Exception {
persistDomain(DEFAULT, null, statusValues);
}
private void persistDomain(
RenewalPriceBehavior renewalPriceBehavior,
@Nullable Money renewalPrice,
StatusValue... statusValues)
throws Exception {
Domain domain = DatabaseHelper.newDomain(getUniqueIdFromCommand());
try {
DomainHistory historyEntryDomainCreate =
new DomainHistory.Builder()
.setDomain(domain)
.setType(HistoryEntry.Type.DOMAIN_CREATE)
.setModificationTime(clock.nowUtc())
.setRegistrarId(domain.getCreationRegistrarId())
.build();
BillingRecurrence autorenewEvent =
new BillingRecurrence.Builder()
.setReason(Reason.RENEW)
.setFlags(ImmutableSet.of(Flag.AUTO_RENEW))
.setTargetId(getUniqueIdFromCommand())
.setRegistrarId("TheRegistrar")
.setEventTime(expirationTime)
.setRecurrenceEndTime(END_OF_TIME)
.setDomainHistory(historyEntryDomainCreate)
.setRenewalPriceBehavior(renewalPriceBehavior)
.setRenewalPrice(renewalPrice)
.build();
PollMessage.Autorenew autorenewPollMessage =
new PollMessage.Autorenew.Builder()
.setTargetId(getUniqueIdFromCommand())
.setRegistrarId("TheRegistrar")
.setEventTime(expirationTime)
.setAutorenewEndTime(END_OF_TIME)
.setMsg("Domain was auto-renewed.")
.setHistoryEntry(historyEntryDomainCreate)
.build();
Domain newDomain =
domain
.asBuilder()
.setRegistrationExpirationTime(expirationTime)
.setStatusValues(ImmutableSet.copyOf(statusValues))
.setAutorenewBillingEvent(autorenewEvent.createVKey())
.setAutorenewPollMessage(autorenewPollMessage.createVKey())
.build();
persistResources(
ImmutableSet.of(
historyEntryDomainCreate, autorenewEvent,
autorenewPollMessage, newDomain));
} catch (Exception e) {
throw new RuntimeException("Error persisting domain", e);
}
clock.advanceOneMilli();
}
@Test
void testFailure_wrongFeeAmount_v06() throws Exception {
setEppInput("domain_renew_fee.xml", FEE_06_MAP);
persistResource(
Tld.get("tld")
.asBuilder()
.setRenewBillingCostTransitions(ImmutableSortedMap.of(START_OF_TIME, Money.of(USD, 20)))
.build());
persistDomain();
EppException thrown = assertThrows(FeesMismatchException.class, this::runFlow);
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
void testFailure_wrongFeeAmount_v11() throws Exception {
setEppInput("domain_renew_fee.xml", FEE_11_MAP);
persistResource(
Tld.get("tld")
.asBuilder()
.setRenewBillingCostTransitions(ImmutableSortedMap.of(START_OF_TIME, Money.of(USD, 20)))
.build());
persistDomain();
EppException thrown = assertThrows(FeesMismatchException.class, this::runFlow);
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
void testFailure_wrongFeeAmount_v12() throws Exception {
setEppInput("domain_renew_fee.xml", FEE_12_MAP);
persistResource(
Tld.get("tld")
.asBuilder()
.setRenewBillingCostTransitions(ImmutableSortedMap.of(START_OF_TIME, Money.of(USD, 20)))
.build());
persistDomain();
EppException thrown = assertThrows(FeesMismatchException.class, this::runFlow);
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
void testFailure_wrongCurrency_v06() throws Exception {
setEppInput("domain_renew_fee.xml", FEE_06_MAP);
persistResource(
Tld.get("tld")
.asBuilder()
.setCurrency(EUR)
.setCreateBillingCostTransitions(
ImmutableSortedMap.of(START_OF_TIME, Money.of(EUR, 13)))
.setRestoreBillingCost(Money.of(EUR, 11))
.setRenewBillingCostTransitions(ImmutableSortedMap.of(START_OF_TIME, Money.of(EUR, 7)))
.setEapFeeSchedule(ImmutableSortedMap.of(START_OF_TIME, Money.zero(EUR)))
.setRegistryLockOrUnlockBillingCost(Money.of(EUR, 20))
.setServerStatusChangeBillingCost(Money.of(EUR, 19))
.build());
persistDomain();
EppException thrown = assertThrows(CurrencyUnitMismatchException.class, this::runFlow);
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
void testFailure_wrongCurrency_v11() throws Exception {
setEppInput("domain_renew_fee.xml", FEE_11_MAP);
persistResource(
Tld.get("tld")
.asBuilder()
.setCurrency(EUR)
.setCreateBillingCostTransitions(
ImmutableSortedMap.of(START_OF_TIME, Money.of(EUR, 13)))
.setRestoreBillingCost(Money.of(EUR, 11))
.setRenewBillingCostTransitions(ImmutableSortedMap.of(START_OF_TIME, Money.of(EUR, 7)))
.setEapFeeSchedule(ImmutableSortedMap.of(START_OF_TIME, Money.zero(EUR)))
.setRegistryLockOrUnlockBillingCost(Money.of(EUR, 20))
.setServerStatusChangeBillingCost(Money.of(EUR, 19))
.build());
persistDomain();
EppException thrown = assertThrows(CurrencyUnitMismatchException.class, this::runFlow);
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
void testFailure_wrongCurrency_v12() throws Exception {
setEppInput("domain_renew_fee.xml", FEE_12_MAP);
persistResource(
Tld.get("tld")
.asBuilder()
.setCurrency(EUR)
.setCreateBillingCostTransitions(
ImmutableSortedMap.of(START_OF_TIME, Money.of(EUR, 13)))
.setRestoreBillingCost(Money.of(EUR, 11))
.setRenewBillingCostTransitions(ImmutableSortedMap.of(START_OF_TIME, Money.of(EUR, 7)))
.setEapFeeSchedule(ImmutableSortedMap.of(START_OF_TIME, Money.zero(EUR)))
.setRegistryLockOrUnlockBillingCost(Money.of(EUR, 20))
.setServerStatusChangeBillingCost(Money.of(EUR, 19))
.build());
persistDomain();
EppException thrown = assertThrows(CurrencyUnitMismatchException.class, this::runFlow);
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
void testFailure_feeGivenInWrongScale_v06() throws Exception {
setEppInput("domain_renew_fee_bad_scale.xml", FEE_06_MAP);
persistDomain();
EppException thrown = assertThrows(CurrencyValueScaleException.class, this::runFlow);
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
void testFailure_feeGivenInWrongScale_v11() throws Exception {
setEppInput("domain_renew_fee_bad_scale.xml", FEE_11_MAP);
persistDomain();
EppException thrown = assertThrows(CurrencyValueScaleException.class, this::runFlow);
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
void testFailure_feeGivenInWrongScale_v12() throws Exception {
setEppInput("domain_renew_fee_bad_scale.xml", FEE_12_MAP);
persistDomain();
EppException thrown = assertThrows(CurrencyValueScaleException.class, this::runFlow);
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
void testSuccess_doesNotApplyNonPremiumDefaultTokenToPremiumName_v06() throws Exception {
ImmutableMap<String, String> customFeeMap = updateSubstitutions(FEE_06_MAP, "FEE", "500");
setEppInput("domain_renew_fee.xml", customFeeMap);
persistDomain();
AllocationToken defaultToken1 =
persistResource(
new AllocationToken.Builder()
.setToken("aaaaa")
.setTokenType(DEFAULT_PROMO)
.setDiscountFraction(0.5)
.setDiscountYears(1)
.setAllowedTlds(ImmutableSet.of("tld"))
.build());
persistResource(
Tld.get("tld")
.asBuilder()
.setDefaultPromoTokens(ImmutableList.of(defaultToken1.createVKey()))
.setPremiumList(persistPremiumList("tld", USD, "example,USD 100"))
.build());
runFlowAssertResponse(
loadFile(
"domain_renew_response_fee.xml",
ImmutableMap.of(
"NAME",
"example.tld",
"PERIOD",
"5",
"EX_DATE",
"2005-04-03T22:00:00.0Z",
"FEE",
"500.00",
"CURRENCY",
"USD",
"FEE_VERSION",
"fee-0.6",
"FEE_NS",
"fee")));
BillingEvent billingEvent =
Iterables.getOnlyElement(DatabaseHelper.loadAllOf(BillingEvent.class));
assertThat(billingEvent.getTargetId()).isEqualTo("example.tld");
assertThat(billingEvent.getAllocationToken()).isEmpty();
}
@Test
void testSuccess_internalRegiration_premiumDomain_v06() throws Exception {
persistResource(
Tld.get("tld")
.asBuilder()
.setPremiumList(persistPremiumList("tld", USD, "example,USD 100"))
.build());
persistDomain(SPECIFIED, Money.of(USD, 2));
setRegistrarIdForFlow("NewRegistrar");
ImmutableMap<String, String> customFeeMap = updateSubstitutions(FEE_06_MAP, "FEE", "10.00");
setEppInput("domain_renew_fee.xml", customFeeMap);
doSuccessfulTest(
"domain_renew_response_fee.xml",
5,
"NewRegistrar",
UserPrivileges.SUPERUSER,
customFeeMap,
Money.of(USD, 10),
SPECIFIED,
Money.of(USD, 2));
}
@Test
void testSuccess_wrongFeeAmountTooHigh_defaultToken_v06() throws Exception {
setEppInput("domain_renew_fee.xml", FEE_06_MAP);
persistDomain();
AllocationToken defaultToken1 =
persistResource(
new AllocationToken.Builder()
.setToken("aaaaa")
.setTokenType(DEFAULT_PROMO)
.setDiscountFraction(0.5)
.setDiscountYears(1)
.setAllowedTlds(ImmutableSet.of("tld"))
.build());
persistResource(
Tld.get("tld")
.asBuilder()
.setDefaultPromoTokens(ImmutableList.of(defaultToken1.createVKey()))
.build());
runFlowAssertResponse(
loadFile(
"domain_renew_response_fee.xml",
ImmutableMap.of(
"NAME",
"example.tld",
"PERIOD",
"5",
"EX_DATE",
"2005-04-03T22:00:00.0Z",
"FEE",
"49.50",
"CURRENCY",
"USD",
"FEE_VERSION",
"fee-0.6",
"FEE_NS",
"fee")));
}
@Test
void testFailure_wrongFeeAmountTooLow_defaultToken_v06() throws Exception {
setEppInput("domain_renew_fee.xml", FEE_06_MAP);
persistDomain();
AllocationToken defaultToken1 =
persistResource(
new AllocationToken.Builder()
.setToken("aaaaa")
.setTokenType(DEFAULT_PROMO)
.setDiscountFraction(0.5)
.setDiscountYears(1)
.setAllowedTlds(ImmutableSet.of("tld"))
.build());
persistResource(
Tld.get("tld")
.asBuilder()
.setDefaultPromoTokens(ImmutableList.of(defaultToken1.createVKey()))
.setRenewBillingCostTransitions(ImmutableSortedMap.of(START_OF_TIME, Money.of(USD, 20)))
.build());
EppException thrown = assertThrows(FeesMismatchException.class, this::runFlow);
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
void testSuccess_wrongFeeAmountTooHigh_defaultToken_v11() throws Exception {
setEppInput("domain_renew_fee.xml", FEE_11_MAP);
persistDomain();
AllocationToken defaultToken1 =
persistResource(
new AllocationToken.Builder()
.setToken("aaaaa")
.setTokenType(DEFAULT_PROMO)
.setDiscountFraction(0.5)
.setDiscountYears(1)
.setAllowedTlds(ImmutableSet.of("tld"))
.build());
persistResource(
Tld.get("tld")
.asBuilder()
.setDefaultPromoTokens(ImmutableList.of(defaultToken1.createVKey()))
.build());
runFlowAssertResponse(
loadFile(
"domain_renew_response_fee.xml",
ImmutableMap.of(
"NAME",
"example.tld",
"PERIOD",
"5",
"EX_DATE",
"2005-04-03T22:00:00.0Z",
"FEE",
"49.50",
"CURRENCY",
"USD",
"FEE_VERSION",
"fee-0.11",
"FEE_NS",
"fee")));
}
@Test
void testFailure_wrongFeeAmountTooLow_defaultToken_v11() throws Exception {
setEppInput("domain_renew_fee.xml", FEE_11_MAP);
persistDomain();
AllocationToken defaultToken1 =
persistResource(
new AllocationToken.Builder()
.setToken("aaaaa")
.setTokenType(DEFAULT_PROMO)
.setDiscountFraction(0.5)
.setDiscountYears(1)
.setAllowedTlds(ImmutableSet.of("tld"))
.build());
persistResource(
Tld.get("tld")
.asBuilder()
.setDefaultPromoTokens(ImmutableList.of(defaultToken1.createVKey()))
.setRenewBillingCostTransitions(ImmutableSortedMap.of(START_OF_TIME, Money.of(USD, 20)))
.build());
EppException thrown = assertThrows(FeesMismatchException.class, this::runFlow);
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
void testSuccess_wrongFeeAmountTooHigh_defaultToken_v12() throws Exception {
setEppInput("domain_renew_fee.xml", FEE_12_MAP);
persistDomain();
AllocationToken defaultToken1 =
persistResource(
new AllocationToken.Builder()
.setToken("aaaaa")
.setTokenType(DEFAULT_PROMO)
.setDiscountFraction(0.5)
.setDiscountYears(1)
.setAllowedTlds(ImmutableSet.of("tld"))
.build());
persistResource(
Tld.get("tld")
.asBuilder()
.setDefaultPromoTokens(ImmutableList.of(defaultToken1.createVKey()))
.build());
runFlowAssertResponse(
loadFile(
"domain_renew_response_fee.xml",
ImmutableMap.of(
"NAME",
"example.tld",
"PERIOD",
"5",
"EX_DATE",
"2005-04-03T22:00:00.0Z",
"FEE",
"49.50",
"CURRENCY",
"USD",
"FEE_VERSION",
"fee-0.12",
"FEE_NS",
"fee")));
}
@Test
void testFailure_wrongFeeAmountTooLow_defaultToken_v12() throws Exception {
setEppInput("domain_renew_fee.xml", FEE_06_MAP);
persistDomain();
AllocationToken defaultToken1 =
persistResource(
new AllocationToken.Builder()
.setToken("aaaaa")
.setTokenType(DEFAULT_PROMO)
.setDiscountFraction(0.5)
.setDiscountYears(1)
.setAllowedTlds(ImmutableSet.of("tld"))
.build());
persistResource(
Tld.get("tld")
.asBuilder()
.setDefaultPromoTokens(ImmutableList.of(defaultToken1.createVKey()))
.setRenewBillingCostTransitions(ImmutableSortedMap.of(START_OF_TIME, Money.of(USD, 20)))
.build());
EppException thrown = assertThrows(FeesMismatchException.class, this::runFlow);
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
void testFailure_refundableFee_v06() throws Exception {
setEppInput("domain_renew_fee_refundable.xml", FEE_06_MAP);
persistDomain();
EppException thrown = assertThrows(UnsupportedFeeAttributeException.class, this::runFlow);
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
void testFailure_refundableFee_v11() throws Exception {
setEppInput("domain_renew_fee_refundable.xml", FEE_11_MAP);
persistDomain();
EppException thrown = assertThrows(UnsupportedFeeAttributeException.class, this::runFlow);
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
void testFailure_refundableFee_v12() throws Exception {
setEppInput("domain_renew_fee_refundable.xml", FEE_12_MAP);
persistDomain();
EppException thrown = assertThrows(UnsupportedFeeAttributeException.class, this::runFlow);
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
void testFailure_gracePeriodFee_v06() throws Exception {
setEppInput("domain_renew_fee_grace_period.xml", FEE_06_MAP);
persistDomain();
EppException thrown = assertThrows(UnsupportedFeeAttributeException.class, this::runFlow);
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
void testFailure_gracePeriodFee_v11() throws Exception {
setEppInput("domain_renew_fee_grace_period.xml", FEE_11_MAP);
persistDomain();
EppException thrown = assertThrows(UnsupportedFeeAttributeException.class, this::runFlow);
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
void testFailure_gracePeriodFee_v12() throws Exception {
setEppInput("domain_renew_fee_grace_period.xml", FEE_12_MAP);
persistDomain();
EppException thrown = assertThrows(UnsupportedFeeAttributeException.class, this::runFlow);
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
void testFailure_appliedFee_v06() throws Exception {
setEppInput("domain_renew_fee_applied.xml", FEE_06_MAP);
persistDomain();
EppException thrown = assertThrows(UnsupportedFeeAttributeException.class, this::runFlow);
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
void testFailure_appliedFee_v11() throws Exception {
setEppInput("domain_renew_fee_applied.xml", FEE_11_MAP);
persistDomain();
EppException thrown = assertThrows(UnsupportedFeeAttributeException.class, this::runFlow);
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
void testFailure_appliedFee_v12() throws Exception {
setEppInput("domain_renew_fee_applied.xml", FEE_12_MAP);
persistDomain();
EppException thrown = assertThrows(UnsupportedFeeAttributeException.class, this::runFlow);
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
void testSuccess_fee_v06() throws Exception {
setEppInput("domain_renew_fee.xml", FEE_06_MAP);
persistDomain();
doSuccessfulTest("domain_renew_response_fee.xml", 5, FEE_06_MAP);
}
@Test
void testSuccess_fee_v11() throws Exception {
setEppInput("domain_renew_fee.xml", FEE_11_MAP);
persistDomain();
doSuccessfulTest("domain_renew_response_fee.xml", 5, FEE_11_MAP);
}
@Test
void testSuccess_fee_v12() throws Exception {
setEppInput("domain_renew_fee.xml", FEE_12_MAP);
persistDomain();
doSuccessfulTest("domain_renew_response_fee.xml", 5, FEE_12_MAP);
}
@Test
void testSuccess_fee_withDefaultAttributes_v06() throws Exception {
setEppInput("domain_renew_fee_defaults.xml", FEE_06_MAP);
persistDomain();
doSuccessfulTest("domain_renew_response_fee.xml", 5, FEE_06_MAP);
}
@Test
void testSuccess_fee_withDefaultAttributes_v11() throws Exception {
setEppInput("domain_renew_fee_defaults.xml", FEE_11_MAP);
persistDomain();
doSuccessfulTest("domain_renew_response_fee.xml", 5, FEE_11_MAP);
}
@Test
void testSuccess_fee_withDefaultAttributes_v12() throws Exception {
setEppInput("domain_renew_fee_defaults.xml", FEE_12_MAP);
persistDomain();
doSuccessfulTest("domain_renew_response_fee.xml", 5, FEE_12_MAP);
}
@Test
void testSuccess_anchorTenant_premiumDomain_v06() throws Exception {
persistResource(
Tld.get("tld")
.asBuilder()
.setPremiumList(persistPremiumList("tld", USD, "example,USD 100"))
.build());
persistDomain(NONPREMIUM, null);
setRegistrarIdForFlow("NewRegistrar");
ImmutableMap<String, String> customFeeMap = updateSubstitutions(FEE_06_MAP, "FEE", "55.00");
setEppInput("domain_renew_fee.xml", customFeeMap);
doSuccessfulTest(
"domain_renew_response_fee.xml",
5,
"NewRegistrar",
UserPrivileges.SUPERUSER,
customFeeMap,
Money.of(USD, 55),
NONPREMIUM,
null);
}
@Test
void testSuccess_customLogicFee_v06() throws Exception {
// The "costly-renew" domain has an additional RENEW fee of 100 from custom logic on top of the
// normal $11 standard renew price for this TLD.
ImmutableMap<String, String> customFeeMap =
updateSubstitutions(
FEE_06_MAP,
"NAME",
"costly-renew.tld",
"PERIOD",
"1",
"EX_DATE",
"2001-04-03T22:00:00.0Z",
"FEE",
"111.00");
setEppInput("domain_renew_fee.xml", customFeeMap);
persistDomain();
doSuccessfulTest(
"domain_renew_response_fee.xml",
1,
"TheRegistrar",
UserPrivileges.NORMAL,
customFeeMap,
Money.of(USD, 111));
}
private void doSuccessfulTest(String responseFilename, int renewalYears) throws Exception {
doSuccessfulTest(responseFilename, renewalYears, ImmutableMap.of());
}
private void doSuccessfulTest(
String responseFilename, int renewalYears, Map<String, String> substitutions)
throws Exception {
doSuccessfulTest(
responseFilename,
renewalYears,
"TheRegistrar",
UserPrivileges.NORMAL,
substitutions,
Money.of(USD, 11).multipliedBy(renewalYears),
DEFAULT,
null);
}
private void doSuccessfulTest(
String responseFilename,
int renewalYears,
String renewalClientId,
UserPrivileges userPrivileges,
Map<String, String> substitutions,
Money totalRenewCost)
throws Exception {
doSuccessfulTest(
responseFilename,
renewalYears,
renewalClientId,
userPrivileges,
substitutions,
totalRenewCost,
DEFAULT,
null);
}
private void doSuccessfulTest(
String responseFilename,
int renewalYears,
String renewalClientId,
UserPrivileges userPrivileges,
Map<String, String> substitutions,
Money totalRenewCost,
RenewalPriceBehavior renewalPriceBehavior,
@Nullable Money renewalPrice)
throws Exception {
assertMutatingFlow(true);
DateTime currentExpiration = reloadResourceByForeignKey().getRegistrationExpirationTime();
DateTime newExpiration = currentExpiration.plusYears(renewalYears);
runFlowAssertResponse(
CommitMode.LIVE, userPrivileges, loadFile(responseFilename, substitutions));
Domain domain = reloadResourceByForeignKey();
assertLastHistoryContainsResource(domain);
DomainHistory historyEntryDomainRenew =
getOnlyHistoryEntryOfType(domain, HistoryEntry.Type.DOMAIN_RENEW, DomainHistory.class);
assertThat(loadByKey(domain.getAutorenewBillingEvent()).getEventTime())
.isEqualTo(newExpiration);
assertAboutDomains()
.that(domain)
.isActiveAt(clock.nowUtc())
.and()
.hasRegistrationExpirationTime(newExpiration)
.and()
.hasOneHistoryEntryEachOfTypes(
HistoryEntry.Type.DOMAIN_CREATE, HistoryEntry.Type.DOMAIN_RENEW)
.and()
.hasLastEppUpdateTime(clock.nowUtc())
.and()
.hasLastEppUpdateRegistrarId(renewalClientId);
assertAboutHistoryEntries().that(historyEntryDomainRenew).hasPeriodYears(renewalYears);
BillingEvent renewBillingEvent =
new BillingEvent.Builder()
.setReason(Reason.RENEW)
.setTargetId(getUniqueIdFromCommand())
.setRegistrarId(renewalClientId)
.setCost(totalRenewCost)
.setPeriodYears(renewalYears)
.setEventTime(clock.nowUtc())
.setBillingTime(clock.nowUtc().plus(Tld.get("tld").getRenewGracePeriodLength()))
.setDomainHistory(historyEntryDomainRenew)
.build();
assertBillingEvents(
renewBillingEvent,
new BillingRecurrence.Builder()
.setReason(Reason.RENEW)
.setRenewalPriceBehavior(renewalPriceBehavior)
.setRenewalPrice(renewalPrice)
.setFlags(ImmutableSet.of(Flag.AUTO_RENEW))
.setTargetId(getUniqueIdFromCommand())
.setRegistrarId("TheRegistrar")
.setEventTime(expirationTime)
.setRecurrenceEndTime(clock.nowUtc())
.setDomainHistory(
getOnlyHistoryEntryOfType(
domain, HistoryEntry.Type.DOMAIN_CREATE, DomainHistory.class))
.build(),
new BillingRecurrence.Builder()
.setReason(Reason.RENEW)
.setRenewalPriceBehavior(renewalPriceBehavior)
.setRenewalPrice(renewalPrice)
.setFlags(ImmutableSet.of(Flag.AUTO_RENEW))
.setTargetId(getUniqueIdFromCommand())
.setRegistrarId("TheRegistrar")
.setEventTime(domain.getRegistrationExpirationTime())
.setRecurrenceEndTime(END_OF_TIME)
.setDomainHistory(historyEntryDomainRenew)
.build());
// There should only be the new autorenew poll message, as the old one will have been deleted
// since it had no messages left to deliver.
assertPollMessages(
new PollMessage.Autorenew.Builder()
.setTargetId(getUniqueIdFromCommand())
.setRegistrarId("TheRegistrar")
.setEventTime(domain.getRegistrationExpirationTime())
.setAutorenewEndTime(END_OF_TIME)
.setMsg("Domain was auto-renewed.")
.setHistoryEntry(historyEntryDomainRenew)
.build());
assertGracePeriods(
domain.getGracePeriods(),
ImmutableMap.of(
GracePeriod.create(
GracePeriodStatus.RENEW,
domain.getRepoId(),
clock.nowUtc().plus(Tld.get("tld").getRenewGracePeriodLength()),
renewalClientId,
null),
renewBillingEvent));
}
}

View File

@@ -119,12 +119,8 @@ class DomainRenewFlowTest extends ResourceFlowTestCase<DomainRenewFlow, Domain>
"FEE", "55.00",
"CURRENCY", "USD");
private static final ImmutableMap<String, String> FEE_06_MAP =
updateSubstitutions(FEE_BASE_MAP, "FEE_VERSION", "0.6", "FEE_NS", "fee");
private static final ImmutableMap<String, String> FEE_11_MAP =
updateSubstitutions(FEE_BASE_MAP, "FEE_VERSION", "0.11", "FEE_NS", "fee11");
private static final ImmutableMap<String, String> FEE_12_MAP =
updateSubstitutions(FEE_BASE_MAP, "FEE_VERSION", "0.12", "FEE_NS", "fee12");
private static final ImmutableMap<String, String> FEE_STD_1_0_MAP =
updateSubstitutions(FEE_BASE_MAP, "FEE_VERSION", "epp:fee-1.0", "FEE_NS", "fee1_00");
private final DateTime expirationTime = DateTime.parse("2000-04-03T22:00:00.0Z");
@@ -390,7 +386,7 @@ class DomainRenewFlowTest extends ResourceFlowTestCase<DomainRenewFlow, Domain>
}
@Test
void testSuccess_internalRegiration_premiumDomain() throws Exception {
void testSuccess_internalRegiration_premiumDomain_std_v1() throws Exception {
persistResource(
Tld.get("tld")
.asBuilder()
@@ -398,7 +394,8 @@ class DomainRenewFlowTest extends ResourceFlowTestCase<DomainRenewFlow, Domain>
.build());
persistDomain(SPECIFIED, Money.of(USD, 2));
setRegistrarIdForFlow("NewRegistrar");
ImmutableMap<String, String> customFeeMap = updateSubstitutions(FEE_06_MAP, "FEE", "10.00");
ImmutableMap<String, String> customFeeMap =
updateSubstitutions(FEE_STD_1_0_MAP, "FEE", "10.00");
setEppInput("domain_renew_fee.xml", customFeeMap);
doSuccessfulTest(
"domain_renew_response_fee.xml",
@@ -427,7 +424,7 @@ class DomainRenewFlowTest extends ResourceFlowTestCase<DomainRenewFlow, Domain>
}
@Test
void testSuccess_anchorTenant_premiumDomain() throws Exception {
void testSuccess_anchorTenant_premiumDomain_std_v1() throws Exception {
persistResource(
Tld.get("tld")
.asBuilder()
@@ -435,7 +432,8 @@ class DomainRenewFlowTest extends ResourceFlowTestCase<DomainRenewFlow, Domain>
.build());
persistDomain(NONPREMIUM, null);
setRegistrarIdForFlow("NewRegistrar");
ImmutableMap<String, String> customFeeMap = updateSubstitutions(FEE_06_MAP, "FEE", "55.00");
ImmutableMap<String, String> customFeeMap =
updateSubstitutions(FEE_STD_1_0_MAP, "FEE", "55.00");
setEppInput("domain_renew_fee.xml", customFeeMap);
doSuccessfulTest(
"domain_renew_response_fee.xml",
@@ -449,12 +447,12 @@ class DomainRenewFlowTest extends ResourceFlowTestCase<DomainRenewFlow, Domain>
}
@Test
void testSuccess_customLogicFee() throws Exception {
void testSuccess_customLogicFee_std_v1() throws Exception {
// The "costly-renew" domain has an additional RENEW fee of 100 from custom logic on top of the
// normal $11 standard renew price for this TLD.
ImmutableMap<String, String> customFeeMap =
updateSubstitutions(
FEE_06_MAP,
FEE_STD_1_0_MAP,
"NAME",
"costly-renew.tld",
"PERIOD",
@@ -475,121 +473,45 @@ class DomainRenewFlowTest extends ResourceFlowTestCase<DomainRenewFlow, Domain>
}
@Test
void testSuccess_fee_v06() throws Exception {
setEppInput("domain_renew_fee.xml", FEE_06_MAP);
void testSuccess_fee_std_v1() throws Exception {
setEppInput("domain_renew_fee.xml", FEE_STD_1_0_MAP);
persistDomain();
doSuccessfulTest("domain_renew_response_fee.xml", 5, FEE_06_MAP);
doSuccessfulTest("domain_renew_response_fee.xml", 5, FEE_STD_1_0_MAP);
}
@Test
void testSuccess_fee_v11() throws Exception {
setEppInput("domain_renew_fee.xml", FEE_11_MAP);
void testSuccess_fee_withDefaultAttributes_std_v1() throws Exception {
setEppInput("domain_renew_fee_defaults.xml", FEE_STD_1_0_MAP);
persistDomain();
doSuccessfulTest("domain_renew_response_fee.xml", 5, FEE_11_MAP);
}
@Test
void testSuccess_fee_v12() throws Exception {
setEppInput("domain_renew_fee.xml", FEE_12_MAP);
persistDomain();
doSuccessfulTest("domain_renew_response_fee.xml", 5, FEE_12_MAP);
}
@Test
void testSuccess_fee_withDefaultAttributes_v06() throws Exception {
setEppInput("domain_renew_fee_defaults.xml", FEE_06_MAP);
persistDomain();
doSuccessfulTest("domain_renew_response_fee.xml", 5, FEE_06_MAP);
}
@Test
void testSuccess_fee_withDefaultAttributes_v11() throws Exception {
setEppInput("domain_renew_fee_defaults.xml", FEE_11_MAP);
persistDomain();
doSuccessfulTest("domain_renew_response_fee.xml", 5, FEE_11_MAP);
}
@Test
void testSuccess_fee_withDefaultAttributes_v12() throws Exception {
setEppInput("domain_renew_fee_defaults.xml", FEE_12_MAP);
persistDomain();
doSuccessfulTest("domain_renew_response_fee.xml", 5, FEE_12_MAP);
doSuccessfulTest("domain_renew_response_fee.xml", 5, FEE_STD_1_0_MAP);
}
@Test
void testFailure_fee_unknownCurrency() {
setEppInput("domain_renew_fee.xml", updateSubstitutions(FEE_06_MAP, "CURRENCY", "BAD"));
setEppInput("domain_renew_fee.xml", updateSubstitutions(FEE_STD_1_0_MAP, "CURRENCY", "BAD"));
EppException thrown = assertThrows(UnknownCurrencyEppException.class, this::persistDomain);
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
void testFailure_refundableFee_v06() throws Exception {
setEppInput("domain_renew_fee_refundable.xml", FEE_06_MAP);
void testFailure_refundableFee_std_v1() throws Exception {
setEppInput("domain_renew_fee_refundable.xml", FEE_STD_1_0_MAP);
persistDomain();
EppException thrown = assertThrows(UnsupportedFeeAttributeException.class, this::runFlow);
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
void testFailure_refundableFee_v11() throws Exception {
setEppInput("domain_renew_fee_refundable.xml", FEE_11_MAP);
void testFailure_gracePeriodFee_std_v1() throws Exception {
setEppInput("domain_renew_fee_grace_period.xml", FEE_STD_1_0_MAP);
persistDomain();
EppException thrown = assertThrows(UnsupportedFeeAttributeException.class, this::runFlow);
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
void testFailure_refundableFee_v12() throws Exception {
setEppInput("domain_renew_fee_refundable.xml", FEE_12_MAP);
persistDomain();
EppException thrown = assertThrows(UnsupportedFeeAttributeException.class, this::runFlow);
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
void testFailure_gracePeriodFee_v06() throws Exception {
setEppInput("domain_renew_fee_grace_period.xml", FEE_06_MAP);
persistDomain();
EppException thrown = assertThrows(UnsupportedFeeAttributeException.class, this::runFlow);
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
void testFailure_gracePeriodFee_v11() throws Exception {
setEppInput("domain_renew_fee_grace_period.xml", FEE_11_MAP);
persistDomain();
EppException thrown = assertThrows(UnsupportedFeeAttributeException.class, this::runFlow);
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
void testFailure_gracePeriodFee_v12() throws Exception {
setEppInput("domain_renew_fee_grace_period.xml", FEE_12_MAP);
persistDomain();
EppException thrown = assertThrows(UnsupportedFeeAttributeException.class, this::runFlow);
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
void testFailure_appliedFee_v06() throws Exception {
setEppInput("domain_renew_fee_applied.xml", FEE_06_MAP);
persistDomain();
EppException thrown = assertThrows(UnsupportedFeeAttributeException.class, this::runFlow);
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
void testFailure_appliedFee_v11() throws Exception {
setEppInput("domain_renew_fee_applied.xml", FEE_11_MAP);
persistDomain();
EppException thrown = assertThrows(UnsupportedFeeAttributeException.class, this::runFlow);
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
void testFailure_appliedFee_v12() throws Exception {
setEppInput("domain_renew_fee_applied.xml", FEE_12_MAP);
void testFailure_appliedFee_std_v1() throws Exception {
setEppInput("domain_renew_fee_applied.xml", FEE_STD_1_0_MAP);
persistDomain();
EppException thrown = assertThrows(UnsupportedFeeAttributeException.class, this::runFlow);
assertAboutEppExceptions().that(thrown).marshalsToXml();
@@ -991,8 +913,8 @@ class DomainRenewFlowTest extends ResourceFlowTestCase<DomainRenewFlow, Domain>
}
@Test
void testFailure_wrongFeeAmount_v06() throws Exception {
setEppInput("domain_renew_fee.xml", FEE_06_MAP);
void testFailure_wrongFeeAmount_std_v1() throws Exception {
setEppInput("domain_renew_fee.xml", FEE_STD_1_0_MAP);
persistResource(
Tld.get("tld")
.asBuilder()
@@ -1004,34 +926,8 @@ class DomainRenewFlowTest extends ResourceFlowTestCase<DomainRenewFlow, Domain>
}
@Test
void testFailure_wrongFeeAmount_v11() throws Exception {
setEppInput("domain_renew_fee.xml", FEE_11_MAP);
persistResource(
Tld.get("tld")
.asBuilder()
.setRenewBillingCostTransitions(ImmutableSortedMap.of(START_OF_TIME, Money.of(USD, 20)))
.build());
persistDomain();
EppException thrown = assertThrows(FeesMismatchException.class, this::runFlow);
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
void testFailure_wrongFeeAmount_v12() throws Exception {
setEppInput("domain_renew_fee.xml", FEE_12_MAP);
persistResource(
Tld.get("tld")
.asBuilder()
.setRenewBillingCostTransitions(ImmutableSortedMap.of(START_OF_TIME, Money.of(USD, 20)))
.build());
persistDomain();
EppException thrown = assertThrows(FeesMismatchException.class, this::runFlow);
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
void testFailure_wrongCurrency_v06() throws Exception {
setEppInput("domain_renew_fee.xml", FEE_06_MAP);
void testFailure_wrongCurrency_std_v1() throws Exception {
setEppInput("domain_renew_fee.xml", FEE_STD_1_0_MAP);
persistResource(
Tld.get("tld")
.asBuilder()
@@ -1050,64 +946,8 @@ class DomainRenewFlowTest extends ResourceFlowTestCase<DomainRenewFlow, Domain>
}
@Test
void testFailure_wrongCurrency_v11() throws Exception {
setEppInput("domain_renew_fee.xml", FEE_11_MAP);
persistResource(
Tld.get("tld")
.asBuilder()
.setCurrency(EUR)
.setCreateBillingCostTransitions(
ImmutableSortedMap.of(START_OF_TIME, Money.of(EUR, 13)))
.setRestoreBillingCost(Money.of(EUR, 11))
.setRenewBillingCostTransitions(ImmutableSortedMap.of(START_OF_TIME, Money.of(EUR, 7)))
.setEapFeeSchedule(ImmutableSortedMap.of(START_OF_TIME, Money.zero(EUR)))
.setRegistryLockOrUnlockBillingCost(Money.of(EUR, 20))
.setServerStatusChangeBillingCost(Money.of(EUR, 19))
.build());
persistDomain();
EppException thrown = assertThrows(CurrencyUnitMismatchException.class, this::runFlow);
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
void testFailure_wrongCurrency_v12() throws Exception {
setEppInput("domain_renew_fee.xml", FEE_12_MAP);
persistResource(
Tld.get("tld")
.asBuilder()
.setCurrency(EUR)
.setCreateBillingCostTransitions(
ImmutableSortedMap.of(START_OF_TIME, Money.of(EUR, 13)))
.setRestoreBillingCost(Money.of(EUR, 11))
.setRenewBillingCostTransitions(ImmutableSortedMap.of(START_OF_TIME, Money.of(EUR, 7)))
.setEapFeeSchedule(ImmutableSortedMap.of(START_OF_TIME, Money.zero(EUR)))
.setRegistryLockOrUnlockBillingCost(Money.of(EUR, 20))
.setServerStatusChangeBillingCost(Money.of(EUR, 19))
.build());
persistDomain();
EppException thrown = assertThrows(CurrencyUnitMismatchException.class, this::runFlow);
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
void testFailure_feeGivenInWrongScale_v06() throws Exception {
setEppInput("domain_renew_fee_bad_scale.xml", FEE_06_MAP);
persistDomain();
EppException thrown = assertThrows(CurrencyValueScaleException.class, this::runFlow);
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
void testFailure_feeGivenInWrongScale_v11() throws Exception {
setEppInput("domain_renew_fee_bad_scale.xml", FEE_11_MAP);
persistDomain();
EppException thrown = assertThrows(CurrencyValueScaleException.class, this::runFlow);
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
void testFailure_feeGivenInWrongScale_v12() throws Exception {
setEppInput("domain_renew_fee_bad_scale.xml", FEE_12_MAP);
void testFailure_feeGivenInWrongScale_std_v1() throws Exception {
setEppInput("domain_renew_fee_bad_scale.xml", FEE_STD_1_0_MAP);
persistDomain();
EppException thrown = assertThrows(CurrencyValueScaleException.class, this::runFlow);
assertAboutEppExceptions().that(thrown).marshalsToXml();
@@ -1522,8 +1362,8 @@ class DomainRenewFlowTest extends ResourceFlowTestCase<DomainRenewFlow, Domain>
}
@Test
void testSuccess_doesNotApplyNonPremiumDefaultTokenToPremiumName() throws Exception {
ImmutableMap<String, String> customFeeMap = updateSubstitutions(FEE_06_MAP, "FEE", "500");
void testSuccess_doesNotApplyNonPremiumDefaultTokenToPremiumName_std_v1() throws Exception {
ImmutableMap<String, String> customFeeMap = updateSubstitutions(FEE_STD_1_0_MAP, "FEE", "500");
setEppInput("domain_renew_fee.xml", customFeeMap);
persistDomain();
AllocationToken defaultToken1 =
@@ -1556,9 +1396,9 @@ class DomainRenewFlowTest extends ResourceFlowTestCase<DomainRenewFlow, Domain>
"CURRENCY",
"USD",
"FEE_VERSION",
"0.6",
"epp:fee-1.0",
"FEE_NS",
"fee")));
"fee1_00")));
BillingEvent billingEvent =
Iterables.getOnlyElement(DatabaseHelper.loadAllOf(BillingEvent.class));
assertThat(billingEvent.getTargetId()).isEqualTo("example.tld");
@@ -1662,8 +1502,8 @@ class DomainRenewFlowTest extends ResourceFlowTestCase<DomainRenewFlow, Domain>
}
@Test
void testSuccess_wrongFeeAmountTooHigh_defaultToken_v06() throws Exception {
setEppInput("domain_renew_fee.xml", FEE_06_MAP);
void testSuccess_wrongFeeAmountTooHigh_defaultToken_std_v1() throws Exception {
setEppInput("domain_renew_fee.xml", FEE_STD_1_0_MAP);
persistDomain();
AllocationToken defaultToken1 =
persistResource(
@@ -1694,136 +1534,14 @@ class DomainRenewFlowTest extends ResourceFlowTestCase<DomainRenewFlow, Domain>
"CURRENCY",
"USD",
"FEE_VERSION",
"0.6",
"epp:fee-1.0",
"FEE_NS",
"fee")));
"fee1_00")));
}
@Test
void testFailure_wrongFeeAmountTooLow_defaultToken_v06() throws Exception {
setEppInput("domain_renew_fee.xml", FEE_06_MAP);
persistDomain();
AllocationToken defaultToken1 =
persistResource(
new AllocationToken.Builder()
.setToken("aaaaa")
.setTokenType(DEFAULT_PROMO)
.setDiscountFraction(0.5)
.setDiscountYears(1)
.setAllowedTlds(ImmutableSet.of("tld"))
.build());
persistResource(
Tld.get("tld")
.asBuilder()
.setDefaultPromoTokens(ImmutableList.of(defaultToken1.createVKey()))
.setRenewBillingCostTransitions(ImmutableSortedMap.of(START_OF_TIME, Money.of(USD, 20)))
.build());
EppException thrown = assertThrows(FeesMismatchException.class, this::runFlow);
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
void testSuccess_wrongFeeAmountTooHigh_defaultToken_v11() throws Exception {
setEppInput("domain_renew_fee.xml", FEE_11_MAP);
persistDomain();
AllocationToken defaultToken1 =
persistResource(
new AllocationToken.Builder()
.setToken("aaaaa")
.setTokenType(DEFAULT_PROMO)
.setDiscountFraction(0.5)
.setDiscountYears(1)
.setAllowedTlds(ImmutableSet.of("tld"))
.build());
persistResource(
Tld.get("tld")
.asBuilder()
.setDefaultPromoTokens(ImmutableList.of(defaultToken1.createVKey()))
.build());
runFlowAssertResponse(
loadFile(
"domain_renew_response_fee.xml",
ImmutableMap.of(
"NAME",
"example.tld",
"PERIOD",
"5",
"EX_DATE",
"2005-04-03T22:00:00.0Z",
"FEE",
"49.50",
"CURRENCY",
"USD",
"FEE_VERSION",
"0.11",
"FEE_NS",
"fee")));
}
@Test
void testFailure_wrongFeeAmountTooLow_defaultToken_v11() throws Exception {
setEppInput("domain_renew_fee.xml", FEE_11_MAP);
persistDomain();
AllocationToken defaultToken1 =
persistResource(
new AllocationToken.Builder()
.setToken("aaaaa")
.setTokenType(DEFAULT_PROMO)
.setDiscountFraction(0.5)
.setDiscountYears(1)
.setAllowedTlds(ImmutableSet.of("tld"))
.build());
persistResource(
Tld.get("tld")
.asBuilder()
.setDefaultPromoTokens(ImmutableList.of(defaultToken1.createVKey()))
.setRenewBillingCostTransitions(ImmutableSortedMap.of(START_OF_TIME, Money.of(USD, 20)))
.build());
EppException thrown = assertThrows(FeesMismatchException.class, this::runFlow);
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
void testSuccess_wrongFeeAmountTooHigh_defaultToken_v12() throws Exception {
setEppInput("domain_renew_fee.xml", FEE_12_MAP);
persistDomain();
AllocationToken defaultToken1 =
persistResource(
new AllocationToken.Builder()
.setToken("aaaaa")
.setTokenType(DEFAULT_PROMO)
.setDiscountFraction(0.5)
.setDiscountYears(1)
.setAllowedTlds(ImmutableSet.of("tld"))
.build());
persistResource(
Tld.get("tld")
.asBuilder()
.setDefaultPromoTokens(ImmutableList.of(defaultToken1.createVKey()))
.build());
runFlowAssertResponse(
loadFile(
"domain_renew_response_fee.xml",
ImmutableMap.of(
"NAME",
"example.tld",
"PERIOD",
"5",
"EX_DATE",
"2005-04-03T22:00:00.0Z",
"FEE",
"49.50",
"CURRENCY",
"USD",
"FEE_VERSION",
"0.12",
"FEE_NS",
"fee")));
}
@Test
void testFailure_wrongFeeAmountTooLow_defaultToken_v12() throws Exception {
setEppInput("domain_renew_fee.xml", FEE_06_MAP);
void testFailure_wrongFeeAmountTooLow_defaultToken_std_v1() throws Exception {
setEppInput("domain_renew_fee.xml", FEE_STD_1_0_MAP);
persistDomain();
AllocationToken defaultToken1 =
persistResource(

View File

@@ -0,0 +1,351 @@
// 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.flows.domain;
import static google.registry.testing.DatabaseHelper.createTld;
import static google.registry.testing.DatabaseHelper.loadRegistrar;
import static google.registry.testing.DatabaseHelper.persistResource;
import static google.registry.testing.EppExceptionSubject.assertAboutEppExceptions;
import static google.registry.util.DateTimeUtils.START_OF_TIME;
import static org.joda.money.CurrencyUnit.EUR;
import static org.joda.money.CurrencyUnit.USD;
import static org.junit.jupiter.api.Assertions.assertThrows;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.ImmutableSortedMap;
import google.registry.flows.EppException;
import google.registry.flows.FlowUtils.UnknownCurrencyEppException;
import google.registry.flows.domain.DomainFlowUtils.CurrencyUnitMismatchException;
import google.registry.flows.domain.DomainFlowUtils.CurrencyValueScaleException;
import google.registry.flows.domain.DomainFlowUtils.FeesMismatchException;
import google.registry.flows.domain.DomainFlowUtils.PremiumNameBlockedException;
import google.registry.flows.domain.DomainFlowUtils.UnsupportedFeeAttributeException;
import google.registry.model.domain.Domain;
import google.registry.model.domain.DomainHistory;
import google.registry.model.domain.GracePeriod;
import google.registry.model.domain.rgp.GracePeriodStatus;
import google.registry.model.eppcommon.StatusValue;
import google.registry.model.poll.PollMessage;
import google.registry.model.reporting.HistoryEntry;
import google.registry.model.tld.Tld;
import google.registry.testing.DatabaseHelper;
import java.util.Map;
import org.joda.money.Money;
import org.joda.time.DateTime;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
/** Tests for {@link DomainRestoreRequestFlow} that use the old fee extensions (0.6, 0.11, 0.12). */
public class DomainRestoreRequestFlowOldFeeExtensionsTest
extends ProductionSimulatingFeeExtensionsTest<DomainRestoreRequestFlow> {
private static final ImmutableMap<String, String> FEE_06_MAP =
ImmutableMap.of("FEE_VERSION", "fee-0.6", "FEE_NS", "fee", "CURRENCY", "USD");
private static final ImmutableMap<String, String> FEE_11_MAP =
ImmutableMap.of("FEE_VERSION", "fee-0.11", "FEE_NS", "fee11", "CURRENCY", "USD");
private static final ImmutableMap<String, String> FEE_12_MAP =
ImmutableMap.of("FEE_VERSION", "fee-0.12", "FEE_NS", "fee12", "CURRENCY", "USD");
@BeforeEach
void beforeEachDomainRestoreRequestFlowOldFeeExtensionsTest() {
createTld("tld");
persistResource(
loadRegistrar("TheRegistrar")
.asBuilder()
.setBillingAccountMap(ImmutableMap.of(USD, "123", EUR, "567"))
.build());
setEppInput("domain_update_restore_request.xml", ImmutableMap.of("DOMAIN", "example.tld"));
}
@Test
void testFailure_wrongFeeAmount_v06() throws Exception {
setEppInput("domain_update_restore_request_fee.xml", FEE_06_MAP);
persistPendingDeleteDomain();
persistResource(Tld.get("tld").asBuilder().setRestoreBillingCost(Money.of(USD, 100)).build());
EppException thrown = assertThrows(FeesMismatchException.class, this::runFlow);
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
void testFailure_wrongFeeAmount_v11() throws Exception {
setEppInput("domain_update_restore_request_fee.xml", FEE_11_MAP);
persistPendingDeleteDomain();
persistResource(Tld.get("tld").asBuilder().setRestoreBillingCost(Money.of(USD, 100)).build());
EppException thrown = assertThrows(FeesMismatchException.class, this::runFlow);
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
void testFailure_wrongFeeAmount_v12() throws Exception {
setEppInput("domain_update_restore_request_fee.xml", FEE_12_MAP);
persistPendingDeleteDomain();
persistResource(Tld.get("tld").asBuilder().setRestoreBillingCost(Money.of(USD, 100)).build());
EppException thrown = assertThrows(FeesMismatchException.class, this::runFlow);
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
void testFailure_feeGivenInWrongScale_v06() throws Exception {
setEppInput("domain_update_restore_request_fee_bad_scale.xml", FEE_06_MAP);
persistPendingDeleteDomain();
EppException thrown = assertThrows(CurrencyValueScaleException.class, this::runFlow);
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
void testFailure_feeGivenInWrongScale_v11() throws Exception {
setEppInput("domain_update_restore_request_fee_bad_scale.xml", FEE_11_MAP);
persistPendingDeleteDomain();
EppException thrown = assertThrows(CurrencyValueScaleException.class, this::runFlow);
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
void testFailure_feeGivenInWrongScale_v12() throws Exception {
setEppInput("domain_update_restore_request_fee_bad_scale.xml", FEE_12_MAP);
persistPendingDeleteDomain();
EppException thrown = assertThrows(CurrencyValueScaleException.class, this::runFlow);
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
void testFailure_appliedFee_v06() throws Exception {
setEppInput("domain_update_restore_request_fee_applied.xml", FEE_06_MAP);
persistPendingDeleteDomain();
EppException thrown = assertThrows(UnsupportedFeeAttributeException.class, this::runFlow);
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
void testFailure_appliedFee_v11() throws Exception {
setEppInput("domain_update_restore_request_fee_applied.xml", FEE_11_MAP);
persistPendingDeleteDomain();
EppException thrown = assertThrows(UnsupportedFeeAttributeException.class, this::runFlow);
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
void testFailure_appliedFee_v12() throws Exception {
setEppInput("domain_update_restore_request_fee_applied.xml", FEE_12_MAP);
persistPendingDeleteDomain();
EppException thrown = assertThrows(UnsupportedFeeAttributeException.class, this::runFlow);
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
void testSuccess_fee_withDefaultAttributes_v06() throws Exception {
setEppInput("domain_update_restore_request_fee_defaults.xml", FEE_06_MAP);
persistPendingDeleteDomain();
runFlowAssertResponse(loadFile("domain_update_restore_request_response_fee.xml", FEE_06_MAP));
}
@Test
void testSuccess_fee_withDefaultAttributes_v11() throws Exception {
setEppInput("domain_update_restore_request_fee_defaults.xml", FEE_11_MAP);
persistPendingDeleteDomain();
runFlowAssertResponse(loadFile("domain_update_restore_request_response_fee.xml", FEE_11_MAP));
}
@Test
void testSuccess_fee_withDefaultAttributes_v12() throws Exception {
setEppInput("domain_update_restore_request_fee_defaults.xml", FEE_12_MAP);
persistPendingDeleteDomain();
runFlowAssertResponse(loadFile("domain_update_restore_request_response_fee.xml", FEE_12_MAP));
}
@Test
void testSuccess_fee_v06() throws Exception {
setEppInput("domain_update_restore_request_fee.xml", FEE_06_MAP);
persistPendingDeleteDomain();
runFlowAssertResponse(loadFile("domain_update_restore_request_response_fee.xml", FEE_06_MAP));
}
@Test
void testSuccess_fee_v06_noRenewal() throws Exception {
setEppInput("domain_update_restore_request_fee_no_renewal.xml", FEE_06_MAP);
persistPendingDeleteDomain(clock.nowUtc().plusMonths(6));
runFlowAssertResponse(
loadFile("domain_update_restore_request_response_fee_no_renewal.xml", FEE_06_MAP));
}
@Test
void testSuccess_fee_v11() throws Exception {
setEppInput("domain_update_restore_request_fee.xml", FEE_11_MAP);
persistPendingDeleteDomain();
runFlowAssertResponse(loadFile("domain_update_restore_request_response_fee.xml", FEE_11_MAP));
}
@Test
void testSuccess_fee_v12() throws Exception {
setEppInput("domain_update_restore_request_fee.xml", FEE_12_MAP);
persistPendingDeleteDomain();
runFlowAssertResponse(loadFile("domain_update_restore_request_response_fee.xml", FEE_12_MAP));
}
@Test
void testFailure_refundableFee_v06() throws Exception {
setEppInput("domain_update_restore_request_fee_refundable.xml", FEE_06_MAP);
persistPendingDeleteDomain();
EppException thrown = assertThrows(UnsupportedFeeAttributeException.class, this::runFlow);
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
void testFailure_refundableFee_v11() throws Exception {
setEppInput("domain_update_restore_request_fee_refundable.xml", FEE_11_MAP);
persistPendingDeleteDomain();
EppException thrown = assertThrows(UnsupportedFeeAttributeException.class, this::runFlow);
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
void testFailure_refundableFee_v12() throws Exception {
setEppInput("domain_update_restore_request_fee_refundable.xml", FEE_12_MAP);
persistPendingDeleteDomain();
EppException thrown = assertThrows(UnsupportedFeeAttributeException.class, this::runFlow);
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
void testFailure_gracePeriodFee_v06() throws Exception {
setEppInput("domain_update_restore_request_fee_grace_period.xml", FEE_06_MAP);
persistPendingDeleteDomain();
EppException thrown = assertThrows(UnsupportedFeeAttributeException.class, this::runFlow);
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
void testFailure_gracePeriodFee_v11() throws Exception {
setEppInput("domain_update_restore_request_fee_grace_period.xml", FEE_11_MAP);
persistPendingDeleteDomain();
EppException thrown = assertThrows(UnsupportedFeeAttributeException.class, this::runFlow);
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
void testFailure_gracePeriodFee_v12() throws Exception {
setEppInput("domain_update_restore_request_fee_grace_period.xml", FEE_12_MAP);
persistPendingDeleteDomain();
EppException thrown = assertThrows(UnsupportedFeeAttributeException.class, this::runFlow);
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
private void runWrongCurrencyTest(Map<String, String> substitutions) throws Exception {
setEppInput("domain_update_restore_request_fee.xml", substitutions);
persistPendingDeleteDomain();
persistResource(
Tld.get("tld")
.asBuilder()
.setCurrency(EUR)
.setCreateBillingCostTransitions(
ImmutableSortedMap.of(START_OF_TIME, Money.of(EUR, 13)))
.setRestoreBillingCost(Money.of(EUR, 11))
.setRenewBillingCostTransitions(ImmutableSortedMap.of(START_OF_TIME, Money.of(EUR, 7)))
.setEapFeeSchedule(ImmutableSortedMap.of(START_OF_TIME, Money.zero(EUR)))
.setServerStatusChangeBillingCost(Money.of(EUR, 19))
.setRegistryLockOrUnlockBillingCost(Money.of(EUR, 0))
.build());
EppException thrown = assertThrows(CurrencyUnitMismatchException.class, this::runFlow);
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
void testFailure_wrongCurrency_v06() throws Exception {
runWrongCurrencyTest(FEE_06_MAP);
}
@Test
void testFailure_wrongCurrency_v11() throws Exception {
runWrongCurrencyTest(FEE_11_MAP);
}
@Test
void testFailure_wrongCurrency_v12() throws Exception {
runWrongCurrencyTest(FEE_12_MAP);
}
@Test
void testFailure_premiumBlocked_v12() throws Exception {
createTld("example");
setEppInput("domain_update_restore_request_premium.xml", FEE_12_MAP);
persistPendingDeleteDomain();
// Modify the Registrar to block premium names.
persistResource(loadRegistrar("TheRegistrar").asBuilder().setBlockPremiumNames(true).build());
EppException thrown = assertThrows(PremiumNameBlockedException.class, this::runFlow);
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
void testSuccess_premiumNotBlocked_andNoRenewal_v12() throws Exception {
createTld("example");
setEppInput("domain_update_restore_request_premium_no_renewal.xml", FEE_12_MAP);
persistPendingDeleteDomain(clock.nowUtc().plusYears(2));
runFlowAssertResponse(
loadFile("domain_update_restore_request_response_fee_no_renewal.xml", FEE_12_MAP));
}
@Test
void testFailure_fee_unknownCurrency_v12() {
ImmutableMap<String, String> substitutions =
ImmutableMap.of("FEE_VERSION", "fee-0.12", "FEE_NS", "fee12", "CURRENCY", "BAD");
setEppInput("domain_update_restore_request_fee.xml", substitutions);
EppException thrown =
assertThrows(UnknownCurrencyEppException.class, this::persistPendingDeleteDomain);
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
private Domain persistPendingDeleteDomain() throws Exception {
// The domain is now past what had been its expiration date at the time of deletion.
return persistPendingDeleteDomain(clock.nowUtc().minusDays(5));
}
private Domain persistPendingDeleteDomain(DateTime expirationTime) throws Exception {
Domain domain = persistResource(DatabaseHelper.newDomain(getUniqueIdFromCommand()));
HistoryEntry historyEntry =
persistResource(
new DomainHistory.Builder()
.setType(HistoryEntry.Type.DOMAIN_DELETE)
.setModificationTime(clock.nowUtc())
.setRegistrarId(domain.getCurrentSponsorRegistrarId())
.setDomain(domain)
.build());
domain =
persistResource(
domain
.asBuilder()
.setRegistrationExpirationTime(expirationTime)
.setDeletionTime(clock.nowUtc().plusDays(35))
.addGracePeriod(
GracePeriod.create(
GracePeriodStatus.REDEMPTION,
domain.getRepoId(),
clock.nowUtc().plusDays(1),
"TheRegistrar",
null))
.setStatusValues(ImmutableSet.of(StatusValue.PENDING_DELETE))
.setDeletePollMessage(
persistResource(
new PollMessage.OneTime.Builder()
.setRegistrarId("TheRegistrar")
.setEventTime(clock.nowUtc().plusDays(5))
.setHistoryEntry(historyEntry)
.build())
.createVKey())
.build());
clock.advanceOneMilli();
return domain;
}
}

View File

@@ -86,12 +86,8 @@ import org.junit.jupiter.api.Test;
/** Unit tests for {@link DomainRestoreRequestFlow}. */
class DomainRestoreRequestFlowTest extends ResourceFlowTestCase<DomainRestoreRequestFlow, Domain> {
private static final ImmutableMap<String, String> FEE_06_MAP =
ImmutableMap.of("FEE_VERSION", "0.6", "FEE_NS", "fee", "CURRENCY", "USD");
private static final ImmutableMap<String, String> FEE_11_MAP =
ImmutableMap.of("FEE_VERSION", "0.11", "FEE_NS", "fee11", "CURRENCY", "USD");
private static final ImmutableMap<String, String> FEE_12_MAP =
ImmutableMap.of("FEE_VERSION", "0.12", "FEE_NS", "fee12", "CURRENCY", "USD");
private static final ImmutableMap<String, String> FEE_STD_1_0_MAP =
ImmutableMap.of("FEE_VERSION", "epp:fee-1.0", "FEE_NS", "fee1_00", "CURRENCY", "USD");
@BeforeEach
void initDomainTest() {
@@ -308,7 +304,7 @@ class DomainRestoreRequestFlowTest extends ResourceFlowTestCase<DomainRestoreReq
@Test
void testSuccess_autorenewEndTimeIsCleared() throws Exception {
setEppInput("domain_update_restore_request_fee.xml", FEE_06_MAP);
setEppInput("domain_update_restore_request_fee.xml", FEE_STD_1_0_MAP);
persistPendingDeleteDomain();
persistResource(
reloadResourceByForeignKey()
@@ -316,64 +312,31 @@ class DomainRestoreRequestFlowTest extends ResourceFlowTestCase<DomainRestoreReq
.setAutorenewEndTime(Optional.of(clock.nowUtc().plusYears(2)))
.build());
assertThat(reloadResourceByForeignKey().getAutorenewEndTime()).isPresent();
runFlowAssertResponse(loadFile("domain_update_restore_request_response_fee.xml", FEE_06_MAP));
runFlowAssertResponse(
loadFile("domain_update_restore_request_response_fee.xml", FEE_STD_1_0_MAP));
assertThat(reloadResourceByForeignKey().getAutorenewEndTime()).isEmpty();
}
@Test
void testSuccess_fee_v06() throws Exception {
setEppInput("domain_update_restore_request_fee.xml", FEE_06_MAP);
void testSuccess_fee_std_v1() throws Exception {
setEppInput("domain_update_restore_request_fee.xml", FEE_STD_1_0_MAP);
persistPendingDeleteDomain();
runFlowAssertResponse(loadFile("domain_update_restore_request_response_fee.xml", FEE_06_MAP));
}
@Test
void testSuccess_fee_v06_noRenewal() throws Exception {
setEppInput("domain_update_restore_request_fee_no_renewal.xml", FEE_06_MAP);
persistPendingDeleteDomain(clock.nowUtc().plusMonths(6));
runFlowAssertResponse(
loadFile("domain_update_restore_request_response_fee_no_renewal.xml", FEE_06_MAP));
loadFile("domain_update_restore_request_response_fee.xml", FEE_STD_1_0_MAP));
}
@Test
void testSuccess_fee_v11() throws Exception {
setEppInput("domain_update_restore_request_fee.xml", FEE_11_MAP);
void testSuccess_fee_withDefaultAttributes_std_v1() throws Exception {
setEppInput("domain_update_restore_request_fee_defaults.xml", FEE_STD_1_0_MAP);
persistPendingDeleteDomain();
runFlowAssertResponse(loadFile("domain_update_restore_request_response_fee.xml", FEE_11_MAP));
runFlowAssertResponse(
loadFile("domain_update_restore_request_response_fee.xml", FEE_STD_1_0_MAP));
}
@Test
void testSuccess_fee_v12() throws Exception {
setEppInput("domain_update_restore_request_fee.xml", FEE_12_MAP);
persistPendingDeleteDomain();
runFlowAssertResponse(loadFile("domain_update_restore_request_response_fee.xml", FEE_12_MAP));
}
@Test
void testSuccess_fee_withDefaultAttributes_v06() throws Exception {
setEppInput("domain_update_restore_request_fee_defaults.xml", FEE_06_MAP);
persistPendingDeleteDomain();
runFlowAssertResponse(loadFile("domain_update_restore_request_response_fee.xml", FEE_06_MAP));
}
@Test
void testSuccess_fee_withDefaultAttributes_v11() throws Exception {
setEppInput("domain_update_restore_request_fee_defaults.xml", FEE_11_MAP);
persistPendingDeleteDomain();
runFlowAssertResponse(loadFile("domain_update_restore_request_response_fee.xml", FEE_11_MAP));
}
@Test
void testSuccess_fee_withDefaultAttributes_v12() throws Exception {
setEppInput("domain_update_restore_request_fee_defaults.xml", FEE_12_MAP);
persistPendingDeleteDomain();
runFlowAssertResponse(loadFile("domain_update_restore_request_response_fee.xml", FEE_12_MAP));
}
@Test
void testFailure_fee_unknownCurrency() {
void testFailure_fee_unknownCurrency_std_v1() {
ImmutableMap<String, String> substitutions =
ImmutableMap.of("FEE_VERSION", "0.12", "FEE_NS", "fee12", "CURRENCY", "BAD");
ImmutableMap.of("FEE_VERSION", "epp:fee-1.0", "FEE_NS", "fee1_00", "CURRENCY", "BAD");
setEppInput("domain_update_restore_request_fee.xml", substitutions);
EppException thrown =
assertThrows(UnknownCurrencyEppException.class, this::persistPendingDeleteDomain);
@@ -381,72 +344,24 @@ class DomainRestoreRequestFlowTest extends ResourceFlowTestCase<DomainRestoreReq
}
@Test
void testFailure_refundableFee_v06() throws Exception {
setEppInput("domain_update_restore_request_fee_refundable.xml", FEE_06_MAP);
void testFailure_refundableFee_std_v1() throws Exception {
setEppInput("domain_update_restore_request_fee_refundable.xml", FEE_STD_1_0_MAP);
persistPendingDeleteDomain();
EppException thrown = assertThrows(UnsupportedFeeAttributeException.class, this::runFlow);
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
void testFailure_refundableFee_v11() throws Exception {
setEppInput("domain_update_restore_request_fee_refundable.xml", FEE_11_MAP);
void testFailure_gracePeriodFee_std_v1() throws Exception {
setEppInput("domain_update_restore_request_fee_grace_period.xml", FEE_STD_1_0_MAP);
persistPendingDeleteDomain();
EppException thrown = assertThrows(UnsupportedFeeAttributeException.class, this::runFlow);
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
void testFailure_refundableFee_v12() throws Exception {
setEppInput("domain_update_restore_request_fee_refundable.xml", FEE_12_MAP);
persistPendingDeleteDomain();
EppException thrown = assertThrows(UnsupportedFeeAttributeException.class, this::runFlow);
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
void testFailure_gracePeriodFee_v06() throws Exception {
setEppInput("domain_update_restore_request_fee_grace_period.xml", FEE_06_MAP);
persistPendingDeleteDomain();
EppException thrown = assertThrows(UnsupportedFeeAttributeException.class, this::runFlow);
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
void testFailure_gracePeriodFee_v11() throws Exception {
setEppInput("domain_update_restore_request_fee_grace_period.xml", FEE_11_MAP);
persistPendingDeleteDomain();
EppException thrown = assertThrows(UnsupportedFeeAttributeException.class, this::runFlow);
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
void testFailure_gracePeriodFee_v12() throws Exception {
setEppInput("domain_update_restore_request_fee_grace_period.xml", FEE_12_MAP);
persistPendingDeleteDomain();
EppException thrown = assertThrows(UnsupportedFeeAttributeException.class, this::runFlow);
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
void testFailure_appliedFee_v06() throws Exception {
setEppInput("domain_update_restore_request_fee_applied.xml", FEE_06_MAP);
persistPendingDeleteDomain();
EppException thrown = assertThrows(UnsupportedFeeAttributeException.class, this::runFlow);
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
void testFailure_appliedFee_v11() throws Exception {
setEppInput("domain_update_restore_request_fee_applied.xml", FEE_11_MAP);
persistPendingDeleteDomain();
EppException thrown = assertThrows(UnsupportedFeeAttributeException.class, this::runFlow);
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
void testFailure_appliedFee_v12() throws Exception {
setEppInput("domain_update_restore_request_fee_applied.xml", FEE_12_MAP);
void testFailure_appliedFee_std_v1() throws Exception {
setEppInput("domain_update_restore_request_fee_applied.xml", FEE_STD_1_0_MAP);
persistPendingDeleteDomain();
EppException thrown = assertThrows(UnsupportedFeeAttributeException.class, this::runFlow);
assertAboutEppExceptions().that(thrown).marshalsToXml();
@@ -455,18 +370,19 @@ class DomainRestoreRequestFlowTest extends ResourceFlowTestCase<DomainRestoreReq
@Test
void testSuccess_premiumNotBlocked() throws Exception {
createTld("example");
setEppInput("domain_update_restore_request_premium.xml");
setEppInput("domain_update_restore_request_premium.xml", FEE_STD_1_0_MAP);
persistPendingDeleteDomain();
runFlowAssertResponse(loadFile("domain_update_restore_request_response_premium.xml"));
runFlowAssertResponse(
loadFile("domain_update_restore_request_response_premium.xml", FEE_STD_1_0_MAP));
}
@Test
void testSuccess_premiumNotBlocked_andNoRenewal() throws Exception {
void testSuccess_premiumNotBlocked_andNoRenewal_std_v1() throws Exception {
createTld("example");
setEppInput("domain_update_restore_request_premium_no_renewal.xml");
setEppInput("domain_update_restore_request_premium_no_renewal.xml", FEE_STD_1_0_MAP);
persistPendingDeleteDomain(clock.nowUtc().plusYears(2));
runFlowAssertResponse(
loadFile("domain_update_restore_request_response_fee_no_renewal.xml", FEE_12_MAP));
loadFile("domain_update_restore_request_response_fee_no_renewal.xml", FEE_STD_1_0_MAP));
}
@Test
@@ -484,14 +400,14 @@ class DomainRestoreRequestFlowTest extends ResourceFlowTestCase<DomainRestoreReq
@Test
void testSuccess_superuserOverridesPremiumNameBlock() throws Exception {
createTld("example");
setEppInput("domain_update_restore_request_premium.xml");
setEppInput("domain_update_restore_request_premium.xml", FEE_STD_1_0_MAP);
persistPendingDeleteDomain();
// Modify the Registrar to block premium names.
persistResource(loadRegistrar("TheRegistrar").asBuilder().setBlockPremiumNames(true).build());
runFlowAssertResponse(
CommitMode.LIVE,
UserPrivileges.SUPERUSER,
loadFile("domain_update_restore_request_response_premium.xml"));
loadFile("domain_update_restore_request_response_premium.xml", FEE_STD_1_0_MAP));
}
@Test
@@ -538,26 +454,8 @@ class DomainRestoreRequestFlowTest extends ResourceFlowTestCase<DomainRestoreReq
}
@Test
void testFailure_wrongFeeAmount_v06() throws Exception {
setEppInput("domain_update_restore_request_fee.xml", FEE_06_MAP);
persistPendingDeleteDomain();
persistResource(Tld.get("tld").asBuilder().setRestoreBillingCost(Money.of(USD, 100)).build());
EppException thrown = assertThrows(FeesMismatchException.class, this::runFlow);
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
void testFailure_wrongFeeAmount_v11() throws Exception {
setEppInput("domain_update_restore_request_fee.xml", FEE_11_MAP);
persistPendingDeleteDomain();
persistResource(Tld.get("tld").asBuilder().setRestoreBillingCost(Money.of(USD, 100)).build());
EppException thrown = assertThrows(FeesMismatchException.class, this::runFlow);
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
void testFailure_wrongFeeAmount_v12() throws Exception {
setEppInput("domain_update_restore_request_fee.xml", FEE_12_MAP);
void testFailure_wrongFeeAmount_std_v1() throws Exception {
setEppInput("domain_update_restore_request_fee.xml", FEE_STD_1_0_MAP);
persistPendingDeleteDomain();
persistResource(Tld.get("tld").asBuilder().setRestoreBillingCost(Money.of(USD, 100)).build());
EppException thrown = assertThrows(FeesMismatchException.class, this::runFlow);
@@ -584,39 +482,13 @@ class DomainRestoreRequestFlowTest extends ResourceFlowTestCase<DomainRestoreReq
}
@Test
void testFailure_wrongCurrency_v06() throws Exception {
runWrongCurrencyTest(FEE_06_MAP);
void testFailure_wrongCurrency_std_v1() throws Exception {
runWrongCurrencyTest(FEE_STD_1_0_MAP);
}
@Test
void testFailure_wrongCurrency_v11() throws Exception {
runWrongCurrencyTest(FEE_11_MAP);
}
@Test
void testFailure_wrongCurrency_v12() throws Exception {
runWrongCurrencyTest(FEE_12_MAP);
}
@Test
void testFailure_feeGivenInWrongScale_v06() throws Exception {
setEppInput("domain_update_restore_request_fee_bad_scale.xml", FEE_06_MAP);
persistPendingDeleteDomain();
EppException thrown = assertThrows(CurrencyValueScaleException.class, this::runFlow);
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
void testFailure_feeGivenInWrongScale_v11() throws Exception {
setEppInput("domain_update_restore_request_fee_bad_scale.xml", FEE_11_MAP);
persistPendingDeleteDomain();
EppException thrown = assertThrows(CurrencyValueScaleException.class, this::runFlow);
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
void testFailure_feeGivenInWrongScale_v12() throws Exception {
setEppInput("domain_update_restore_request_fee_bad_scale.xml", FEE_12_MAP);
void testFailure_feeGivenInWrongScale_std_v1() throws Exception {
setEppInput("domain_update_restore_request_fee_bad_scale.xml", FEE_STD_1_0_MAP);
persistPendingDeleteDomain();
EppException thrown = assertThrows(CurrencyValueScaleException.class, this::runFlow);
assertAboutEppExceptions().that(thrown).marshalsToXml();
@@ -738,9 +610,9 @@ class DomainRestoreRequestFlowTest extends ResourceFlowTestCase<DomainRestoreReq
}
@Test
void testFailure_premiumBlocked() throws Exception {
void testFailure_premiumBlocked_std_v1() throws Exception {
createTld("example");
setEppInput("domain_update_restore_request_premium.xml");
setEppInput("domain_update_restore_request_premium.xml", FEE_STD_1_0_MAP);
persistPendingDeleteDomain();
// Modify the Registrar to block premium names.
persistResource(loadRegistrar("TheRegistrar").asBuilder().setBlockPremiumNames(true).build());

View File

@@ -0,0 +1,897 @@
// 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.flows.domain;
import static com.google.common.collect.ImmutableSet.toImmutableSet;
import static com.google.common.collect.MoreCollectors.onlyElement;
import static com.google.common.truth.Truth.assertThat;
import static google.registry.batch.AsyncTaskEnqueuer.PARAM_REQUESTED_TIME;
import static google.registry.batch.AsyncTaskEnqueuer.PARAM_RESOURCE_KEY;
import static google.registry.batch.AsyncTaskEnqueuer.QUEUE_ASYNC_ACTIONS;
import static google.registry.model.reporting.HistoryEntry.Type.DOMAIN_CREATE;
import static google.registry.model.reporting.HistoryEntry.Type.DOMAIN_TRANSFER_REQUEST;
import static google.registry.testing.DatabaseHelper.assertBillingEvents;
import static google.registry.testing.DatabaseHelper.assertBillingEventsEqual;
import static google.registry.testing.DatabaseHelper.assertPollMessagesEqual;
import static google.registry.testing.DatabaseHelper.createTld;
import static google.registry.testing.DatabaseHelper.getOnlyHistoryEntryOfType;
import static google.registry.testing.DatabaseHelper.getOnlyPollMessage;
import static google.registry.testing.DatabaseHelper.getPollMessages;
import static google.registry.testing.DatabaseHelper.loadByKey;
import static google.registry.testing.DatabaseHelper.loadByKeys;
import static google.registry.testing.DatabaseHelper.loadRegistrar;
import static google.registry.testing.DatabaseHelper.persistActiveContact;
import static google.registry.testing.DatabaseHelper.persistDomainWithDependentResources;
import static google.registry.testing.DatabaseHelper.persistResource;
import static google.registry.testing.DomainSubject.assertAboutDomains;
import static google.registry.testing.EppExceptionSubject.assertAboutEppExceptions;
import static google.registry.testing.HistoryEntrySubject.assertAboutHistoryEntries;
import static google.registry.testing.HostSubject.assertAboutHosts;
import static google.registry.util.DateTimeUtils.END_OF_TIME;
import static google.registry.util.DateTimeUtils.START_OF_TIME;
import static org.joda.money.CurrencyUnit.USD;
import static org.junit.jupiter.api.Assertions.assertThrows;
import com.google.cloud.tasks.v2.HttpMethod;
import com.google.common.base.Ascii;
import com.google.common.base.Predicates;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.ImmutableSortedMap;
import com.google.common.collect.Iterables;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import com.google.common.collect.Streams;
import google.registry.batch.ResaveEntityAction;
import google.registry.flows.EppException;
import google.registry.flows.EppRequestSource;
import google.registry.flows.domain.DomainFlowUtils.CurrencyUnitMismatchException;
import google.registry.flows.domain.DomainFlowUtils.CurrencyValueScaleException;
import google.registry.flows.domain.DomainFlowUtils.FeesMismatchException;
import google.registry.flows.domain.DomainFlowUtils.UnsupportedFeeAttributeException;
import google.registry.flows.exceptions.TransferPeriodZeroAndFeeTransferExtensionException;
import google.registry.model.billing.BillingBase;
import google.registry.model.billing.BillingBase.Flag;
import google.registry.model.billing.BillingBase.Reason;
import google.registry.model.billing.BillingCancellation;
import google.registry.model.billing.BillingEvent;
import google.registry.model.billing.BillingRecurrence;
import google.registry.model.contact.Contact;
import google.registry.model.domain.Domain;
import google.registry.model.domain.DomainHistory;
import google.registry.model.domain.GracePeriod;
import google.registry.model.domain.Period;
import google.registry.model.domain.Period.Unit;
import google.registry.model.domain.rgp.GracePeriodStatus;
import google.registry.model.eppcommon.StatusValue;
import google.registry.model.eppcommon.Trid;
import google.registry.model.host.Host;
import google.registry.model.poll.PendingActionNotificationResponse;
import google.registry.model.poll.PollMessage;
import google.registry.model.reporting.HistoryEntry;
import google.registry.model.tld.Tld;
import google.registry.model.transfer.DomainTransferData;
import google.registry.model.transfer.TransferResponse;
import google.registry.model.transfer.TransferStatus;
import google.registry.testing.CloudTasksHelper.TaskMatcher;
import java.util.Map;
import java.util.Optional;
import java.util.stream.Stream;
import org.joda.money.Money;
import org.joda.time.DateTime;
import org.joda.time.Duration;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
/**
* Tests for {@link DomainTransferRequestFlow} that use the old fee extensions (0.6, 0.11, 0.12).
*/
public class DomainTransferRequestFlowOldFeeExtensionsTest
extends ProductionSimulatingFeeExtensionsTest<DomainTransferRequestFlow> {
private static final ImmutableMap<String, String> BASE_FEE_MAP =
new ImmutableMap.Builder<String, String>()
.put("DOMAIN", "example.tld")
.put("YEARS", "1")
.put("AMOUNT", "11.00")
.put("CURRENCY", "USD")
.build();
private static final ImmutableMap<String, String> FEE_06_MAP =
new ImmutableMap.Builder<String, String>()
.putAll(BASE_FEE_MAP)
.put("FEE_VERSION", "fee-0.6")
.put("FEE_NS", "fee")
.build();
private static final ImmutableMap<String, String> FEE_11_MAP =
new ImmutableMap.Builder<String, String>()
.putAll(BASE_FEE_MAP)
.put("FEE_VERSION", "fee-0.11")
.put("FEE_NS", "fee11")
.build();
private static final ImmutableMap<String, String> FEE_12_MAP =
new ImmutableMap.Builder<String, String>()
.putAll(BASE_FEE_MAP)
.put("FEE_VERSION", "fee-0.12")
.put("FEE_NS", "fee12")
.build();
private static final ImmutableMap<String, String> RICH_DOMAIN_MAP =
ImmutableMap.<String, String>builder()
.put("DOMAIN", "rich.example")
.put("YEARS", "1")
.put("AMOUNT", "100.00")
.put("CURRENCY", "USD")
.put("FEE_VERSION", "fee-0.12")
.put("FEE_NS", "fee12")
.build();
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_AUTOMATIC_TRANSFER_LENGTH);
private static final Duration TIME_SINCE_REQUEST = Duration.standardDays(3);
private static final int EXTENDED_REGISTRATION_YEARS = 1;
private static final DateTime REGISTRATION_EXPIRATION_TIME =
DateTime.parse("2001-09-08T22:00:00.0Z");
private static final DateTime EXTENDED_REGISTRATION_EXPIRATION_TIME =
REGISTRATION_EXPIRATION_TIME.plusYears(EXTENDED_REGISTRATION_YEARS);
private Contact contact;
private Domain domain;
private Host subordinateHost;
private DomainHistory historyEntryDomainCreate;
@BeforeEach
void beforeEachDomainTransferRequestFlowOldFeeExtensionsTest() {
setEppInput("domain_transfer_request.xml");
setRegistrarIdForFlow("NewRegistrar");
clock.setTo(TRANSFER_REQUEST_TIME.plus(TIME_SINCE_REQUEST));
}
@Test
void testFailure_wrongFeeAmount_v06() {
setupDomain("example", "tld");
runWrongFeeAmountTest(FEE_06_MAP);
}
@Test
void testFailure_wrongFeeAmount_v11() {
setupDomain("example", "tld");
runWrongFeeAmountTest(FEE_11_MAP);
}
@Test
void testFailure_wrongFeeAmount_v12() {
setupDomain("example", "tld");
runWrongFeeAmountTest(FEE_12_MAP);
}
@Test
void testFailure_appliedFee_v06() {
setupDomain("example", "tld");
EppException thrown =
assertThrows(
UnsupportedFeeAttributeException.class,
() -> doFailingTest("domain_transfer_request_fee_applied.xml", FEE_06_MAP));
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
void testFailure_appliedFee_v11() {
setupDomain("example", "tld");
EppException thrown =
assertThrows(
UnsupportedFeeAttributeException.class,
() -> doFailingTest("domain_transfer_request_fee_applied.xml", FEE_11_MAP));
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
void testFailure_appliedFee_v12() {
setupDomain("example", "tld");
EppException thrown =
assertThrows(
UnsupportedFeeAttributeException.class,
() -> doFailingTest("domain_transfer_request_fee_applied.xml", FEE_12_MAP));
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
void testFailure_gracePeriodFee_v06() {
setupDomain("example", "tld");
EppException thrown =
assertThrows(
UnsupportedFeeAttributeException.class,
() -> doFailingTest("domain_transfer_request_fee_grace_period.xml", FEE_06_MAP));
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
void testFailure_gracePeriodFee_v11() {
setupDomain("example", "tld");
EppException thrown =
assertThrows(
UnsupportedFeeAttributeException.class,
() -> doFailingTest("domain_transfer_request_fee_grace_period.xml", FEE_11_MAP));
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
void testFailure_gracePeriodFee_v12() {
setupDomain("example", "tld");
EppException thrown =
assertThrows(
UnsupportedFeeAttributeException.class,
() -> doFailingTest("domain_transfer_request_fee_grace_period.xml", FEE_12_MAP));
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
void testSuccess_fee_withDefaultAttributes_v06() throws Exception {
setupDomain("example", "tld");
doSuccessfulTest(
"domain_transfer_request_fee_defaults.xml",
"domain_transfer_request_response_fee.xml",
FEE_06_MAP);
}
@Test
void testSuccess_fee_withDefaultAttributes_v11() throws Exception {
setupDomain("example", "tld");
doSuccessfulTest(
"domain_transfer_request_fee_defaults.xml",
"domain_transfer_request_response_fee.xml",
FEE_11_MAP);
}
@Test
void testSuccess_fee_withDefaultAttributes_v12() throws Exception {
setupDomain("example", "tld");
doSuccessfulTest(
"domain_transfer_request_fee_defaults.xml",
"domain_transfer_request_response_fee.xml",
FEE_12_MAP);
}
@Test
void testFailure_refundableFee_v06() {
setupDomain("example", "tld");
EppException thrown =
assertThrows(
UnsupportedFeeAttributeException.class,
() -> doFailingTest("domain_transfer_request_fee_refundable.xml", FEE_06_MAP));
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
void testFailure_refundableFee_v11() {
setupDomain("example", "tld");
EppException thrown =
assertThrows(
UnsupportedFeeAttributeException.class,
() -> doFailingTest("domain_transfer_request_fee_refundable.xml", FEE_11_MAP));
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
void testFailure_refundableFee_v12() {
setupDomain("example", "tld");
EppException thrown =
assertThrows(
UnsupportedFeeAttributeException.class,
() -> doFailingTest("domain_transfer_request_fee_refundable.xml", FEE_12_MAP));
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
void testFailure_wrongCurrency_v06() {
runWrongCurrencyTest(FEE_06_MAP);
}
@Test
void testFailure_wrongCurrency_v11() {
runWrongCurrencyTest(FEE_11_MAP);
}
@Test
void testFailure_wrongCurrency_v12() {
runWrongCurrencyTest(FEE_12_MAP);
}
@Test
void testFailure_feeGivenInWrongScale_v06() {
setupDomain("example", "tld");
EppException thrown =
assertThrows(
CurrencyValueScaleException.class,
() -> doFailingTest("domain_transfer_request_fee_bad_scale.xml", FEE_06_MAP));
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
void testFailure_feeGivenInWrongScale_v11() {
setupDomain("example", "tld");
EppException thrown =
assertThrows(
CurrencyValueScaleException.class,
() -> doFailingTest("domain_transfer_request_fee_bad_scale.xml", FEE_11_MAP));
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
void testFailure_feeGivenInWrongScale_v12() {
setupDomain("example", "tld");
EppException thrown =
assertThrows(
CurrencyValueScaleException.class,
() -> doFailingTest("domain_transfer_request_fee_bad_scale.xml", FEE_12_MAP));
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
void testSuccess_fee_v06() throws Exception {
setupDomain("example", "tld");
doSuccessfulTest(
"domain_transfer_request_fee.xml", "domain_transfer_request_response_fee.xml", FEE_06_MAP);
}
@Test
void testSuccess_fee_v11() throws Exception {
setupDomain("example", "tld");
doSuccessfulTest(
"domain_transfer_request_fee.xml", "domain_transfer_request_response_fee.xml", FEE_11_MAP);
}
@Test
void testSuccess_fee_v12() throws Exception {
setupDomain("example", "tld");
doSuccessfulTest(
"domain_transfer_request_fee.xml", "domain_transfer_request_response_fee.xml", FEE_12_MAP);
}
@Test
void testSuccess_customLogicFee_v06() throws Exception {
setupDomain("expensive-domain", "foo");
clock.advanceOneMilli();
doSuccessfulTest(
"domain_transfer_request_separate_fees.xml",
"domain_transfer_request_response_fees.xml",
domain.getRegistrationExpirationTime().plusYears(1),
new ImmutableMap.Builder<String, String>()
.put("DOMAIN", "expensive-domain.foo")
.put("YEARS", "1")
.put("AMOUNT", "111.00")
.put("EXDATE", "2002-09-08T22:00:00.0Z")
.put("FEE_VERSION", "fee-0.6")
.put("FEE_NS", "fee")
.build(),
Optional.of(Money.of(USD, 111)));
}
@Test
void testSuccess_premiumNotBlocked_v12() throws Exception {
setupDomain("rich", "example");
clock.advanceOneMilli();
// We don't verify the results; just check that the flow doesn't fail.
runTest("domain_transfer_request_fee.xml", UserPrivileges.NORMAL, RICH_DOMAIN_MAP);
}
@Test
void testFailure_superuserExtension_zeroPeriod_feeTransferExtension_v12() {
setupDomain("example", "tld");
eppRequestSource = EppRequestSource.TOOL;
clock.advanceOneMilli();
assertThrows(
TransferPeriodZeroAndFeeTransferExtensionException.class,
() ->
runTest(
"domain_transfer_request_fee_and_superuser_extension.xml",
UserPrivileges.SUPERUSER,
new ImmutableMap.Builder<String, String>()
.putAll(FEE_12_MAP)
.put("PERIOD", "0")
.put("AUTOMATIC_TRANSFER_LENGTH", "5")
.build()));
}
@Test
void testSuccess_premiumNotBlockedInSuperuserMode_v12() throws Exception {
setupDomain("rich", "example");
clock.advanceOneMilli();
// Modify the Registrar to block premium names.
persistResource(loadRegistrar("NewRegistrar").asBuilder().setBlockPremiumNames(true).build());
// We don't verify the results; just check that the flow doesn't fail.
runTest("domain_transfer_request_fee.xml", UserPrivileges.SUPERUSER, RICH_DOMAIN_MAP);
}
private void runWrongCurrencyTest(Map<String, String> substitutions) {
Map<String, String> fullSubstitutions = Maps.newHashMap();
fullSubstitutions.putAll(substitutions);
fullSubstitutions.put("CURRENCY", "EUR");
setupDomain("example", "tld");
EppException thrown =
assertThrows(
CurrencyUnitMismatchException.class,
() -> doFailingTest("domain_transfer_request_fee.xml", fullSubstitutions));
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
/**
* Runs a successful test. The extraExpectedBillingEvents parameter consists of cancellation
* billing event builders that have had all of their attributes set except for the parent history
* entry, which is filled in during the execution of this method.
*/
private void doSuccessfulTest(
String commandFilename,
String expectedXmlFilename,
DateTime expectedExpirationTime,
Map<String, String> substitutions,
Optional<Money> transferCost,
BillingCancellation.Builder... extraExpectedBillingEvents)
throws Exception {
setEppInput(commandFilename, substitutions);
ImmutableSet<GracePeriod> originalGracePeriods = domain.getGracePeriods();
// Replace the ROID in the xml file with the one generated in our test.
eppLoader.replaceAll("JD1234-REP", contact.getRepoId());
// For all of the other transfer flow tests, 'now' corresponds to day 3 of the transfer, but
// for the request test we want that same 'now' to be the initial request time, so we shift
// the transfer timeline 3 days later by adjusting the implicit transfer time here.
Tld registry = Tld.get(domain.getTld());
DateTime implicitTransferTime = clock.nowUtc().plus(registry.getAutomaticTransferLength());
// Setup done; run the test.
assertMutatingFlow(true);
runFlowAssertResponse(loadFile(expectedXmlFilename, substitutions));
// Transfer should have been requested.
domain = reloadResourceByForeignKey();
// Verify that HistoryEntry was created.
assertAboutDomains()
.that(domain)
.hasOneHistoryEntryEachOfTypes(DOMAIN_CREATE, DOMAIN_TRANSFER_REQUEST);
assertLastHistoryContainsResource(domain);
final HistoryEntry historyEntryTransferRequest =
getOnlyHistoryEntryOfType(domain, DOMAIN_TRANSFER_REQUEST);
assertAboutHistoryEntries()
.that(historyEntryTransferRequest)
.hasPeriodYears(1)
.and()
.hasOtherRegistrarId("TheRegistrar");
// Verify correct fields were set.
assertTransferRequested(
domain, implicitTransferTime, Period.create(1, Unit.YEARS), expectedExpirationTime);
subordinateHost = reloadResourceAndCloneAtTime(subordinateHost, clock.nowUtc());
assertAboutHosts().that(subordinateHost).hasNoHistoryEntries();
assertHistoryEntriesContainBillingEventsAndGracePeriods(
expectedExpirationTime,
implicitTransferTime,
transferCost,
originalGracePeriods,
/* expectTransferBillingEvent= */ true,
extraExpectedBillingEvents);
assertPollMessagesEmitted(expectedExpirationTime, implicitTransferTime);
assertAboutDomainAfterAutomaticTransfer(
expectedExpirationTime, implicitTransferTime, Period.create(1, Unit.YEARS));
cloudTasksHelper.assertTasksEnqueued(
QUEUE_ASYNC_ACTIONS,
new TaskMatcher()
.path(ResaveEntityAction.PATH)
.method(HttpMethod.POST)
.service("backend")
.header("content-type", "application/x-www-form-urlencoded")
.param(PARAM_RESOURCE_KEY, domain.createVKey().stringify())
.param(PARAM_REQUESTED_TIME, clock.nowUtc().toString())
.scheduleTime(clock.nowUtc().plus(registry.getAutomaticTransferLength())));
}
private void doSuccessfulTest(
String commandFilename, String expectedXmlFilename, Map<String, String> substitutions)
throws Exception {
clock.advanceOneMilli();
doSuccessfulTest(
commandFilename,
expectedXmlFilename,
domain.getRegistrationExpirationTime().plusYears(1),
substitutions,
Optional.empty());
}
private void doSuccessfulTest(String commandFilename, String expectedXmlFilename)
throws Exception {
clock.advanceOneMilli();
doSuccessfulTest(
commandFilename, expectedXmlFilename, domain.getRegistrationExpirationTime().plusYears(1));
}
private void doSuccessfulTest(
String commandFilename,
String expectedXmlFilename,
DateTime expectedExpirationTime,
BillingCancellation.Builder... extraExpectedBillingEvents)
throws Exception {
doSuccessfulTest(
commandFilename,
expectedXmlFilename,
expectedExpirationTime,
ImmutableMap.of(),
Optional.empty(),
extraExpectedBillingEvents);
}
private void runWrongFeeAmountTest(Map<String, String> substitutions) {
persistResource(
Tld.get("tld")
.asBuilder()
.setRenewBillingCostTransitions(ImmutableSortedMap.of(START_OF_TIME, Money.of(USD, 20)))
.build());
EppException thrown =
assertThrows(
FeesMismatchException.class,
() -> doFailingTest("domain_transfer_request_fee.xml", substitutions));
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
private void runTest(
String commandFilename, UserPrivileges userPrivileges, Map<String, String> substitutions)
throws Exception {
setEppInput(commandFilename, substitutions);
// Replace the ROID in the xml file with the one generated in our test.
eppLoader.replaceAll("JD1234-REP", contact.getRepoId());
// Setup done; run the test.
assertMutatingFlow(true);
runFlow(CommitMode.LIVE, userPrivileges);
}
private void runTest(String commandFilename, UserPrivileges userPrivileges) throws Exception {
runTest(commandFilename, userPrivileges, ImmutableMap.of());
}
private void doFailingTest(String commandFilename, Map<String, String> substitutions)
throws Exception {
runTest(commandFilename, UserPrivileges.NORMAL, substitutions);
}
private void doFailingTest(String commandFilename) throws Exception {
runTest(commandFilename, UserPrivileges.NORMAL, ImmutableMap.of());
}
/** Adds a domain with no pending transfer on it. */
void setupDomain(String label, String tld) {
createTld(tld);
contact = persistActiveContact("jd1234");
domain =
persistDomainWithDependentResources(
label,
tld,
contact,
clock.nowUtc(),
DateTime.parse("1999-04-03T22:00:00.0Z"),
REGISTRATION_EXPIRATION_TIME);
subordinateHost =
persistResource(
new Host.Builder()
.setRepoId("2-".concat(Ascii.toUpperCase(tld)))
.setHostName("ns1." + label + "." + tld)
.setPersistedCurrentSponsorRegistrarId("TheRegistrar")
.setCreationRegistrarId("TheRegistrar")
.setCreationTimeForTest(DateTime.parse("1999-04-03T22:00:00.0Z"))
.setSuperordinateDomain(domain.createVKey())
.build());
domain =
persistResource(
domain.asBuilder().addSubordinateHost(subordinateHost.getHostName()).build());
historyEntryDomainCreate =
getOnlyHistoryEntryOfType(domain, DOMAIN_CREATE, DomainHistory.class);
}
private void assertPollMessagesEmitted(
DateTime expectedExpirationTime, DateTime implicitTransferTime) {
// Assert that there exists a poll message to notify the losing registrar that a transfer was
// requested. If the implicit transfer time is now (i.e. the automatic transfer length is zero)
// then also expect a server approved poll message.
assertThat(getPollMessages("TheRegistrar", clock.nowUtc()))
.hasSize(implicitTransferTime.equals(clock.nowUtc()) ? 2 : 1);
// Two poll messages on the gaining registrar's side at the expected expiration time: a
// (OneTime) transfer approved message, and an Autorenew poll message.
assertThat(getPollMessages("NewRegistrar", expectedExpirationTime)).hasSize(2);
PollMessage transferApprovedPollMessage =
getOnlyPollMessage("NewRegistrar", implicitTransferTime, PollMessage.OneTime.class);
PollMessage autorenewPollMessage =
getOnlyPollMessage("NewRegistrar", expectedExpirationTime, PollMessage.Autorenew.class);
assertThat(transferApprovedPollMessage.getEventTime()).isEqualTo(implicitTransferTime);
assertThat(autorenewPollMessage.getEventTime()).isEqualTo(expectedExpirationTime);
assertThat(
transferApprovedPollMessage.getResponseData().stream()
.filter(TransferResponse.class::isInstance)
.map(TransferResponse.class::cast)
.collect(onlyElement())
.getTransferStatus())
.isEqualTo(TransferStatus.SERVER_APPROVED);
PendingActionNotificationResponse panData =
transferApprovedPollMessage.getResponseData().stream()
.filter(PendingActionNotificationResponse.class::isInstance)
.map(PendingActionNotificationResponse.class::cast)
.collect(onlyElement());
assertThat(panData.getTrid().getClientTransactionId()).hasValue("ABC-12345");
assertThat(panData.getActionResult()).isTrue();
// Two poll messages on the losing registrar's side at the implicit transfer time: a
// transfer pending message, and a transfer approved message (both OneTime messages).
assertThat(getPollMessages("TheRegistrar", implicitTransferTime)).hasSize(2);
PollMessage losingTransferPendingPollMessage =
getPollMessages("TheRegistrar", clock.nowUtc()).stream()
.filter(pollMessage -> TransferStatus.PENDING.getMessage().equals(pollMessage.getMsg()))
.collect(onlyElement());
PollMessage losingTransferApprovedPollMessage =
getPollMessages("TheRegistrar", implicitTransferTime).stream()
.filter(Predicates.not(Predicates.equalTo(losingTransferPendingPollMessage)))
.collect(onlyElement());
assertThat(losingTransferPendingPollMessage.getEventTime()).isEqualTo(clock.nowUtc());
assertThat(losingTransferApprovedPollMessage.getEventTime()).isEqualTo(implicitTransferTime);
assertThat(
losingTransferPendingPollMessage.getResponseData().stream()
.filter(TransferResponse.class::isInstance)
.map(TransferResponse.class::cast)
.collect(onlyElement())
.getTransferStatus())
.isEqualTo(TransferStatus.PENDING);
assertThat(
losingTransferApprovedPollMessage.getResponseData().stream()
.filter(TransferResponse.class::isInstance)
.map(TransferResponse.class::cast)
.collect(onlyElement())
.getTransferStatus())
.isEqualTo(TransferStatus.SERVER_APPROVED);
// Assert that the poll messages show up in the TransferData server approve entities.
assertPollMessagesEqual(
loadByKey(domain.getTransferData().getServerApproveAutorenewPollMessage()),
autorenewPollMessage);
// Assert that the full set of server-approve poll messages is exactly the server approve
// OneTime messages to gaining and losing registrars plus the gaining client autorenew.
assertPollMessagesEqual(
Iterables.filter(
loadByKeys(domain.getTransferData().getServerApproveEntities()), PollMessage.class),
ImmutableList.of(
transferApprovedPollMessage, losingTransferApprovedPollMessage, autorenewPollMessage));
}
private void assertHistoryEntriesContainBillingEventsAndGracePeriods(
DateTime expectedExpirationTime,
DateTime implicitTransferTime,
Optional<Money> transferCost,
ImmutableSet<GracePeriod> originalGracePeriods,
boolean expectTransferBillingEvent,
BillingCancellation.Builder... extraExpectedBillingEvents) {
Tld registry = Tld.get(domain.getTld());
final DomainHistory historyEntryTransferRequest =
getOnlyHistoryEntryOfType(domain, DOMAIN_TRANSFER_REQUEST, DomainHistory.class);
// Construct the billing events we expect to exist, starting with the (optional) billing
// event for the transfer itself.
Optional<BillingEvent> optionalTransferBillingEvent;
if (expectTransferBillingEvent) {
// For normal transfers, a BillingEvent should be created AUTOMATIC_TRANSFER_DAYS in the
// future, for the case when the transfer is implicitly acked.
optionalTransferBillingEvent =
Optional.of(
new BillingEvent.Builder()
.setReason(Reason.TRANSFER)
.setTargetId(domain.getDomainName())
.setEventTime(implicitTransferTime)
.setBillingTime(
implicitTransferTime.plus(registry.getTransferGracePeriodLength()))
.setRegistrarId("NewRegistrar")
.setCost(transferCost.orElse(Money.of(USD, 11)))
.setPeriodYears(1)
.setDomainHistory(historyEntryTransferRequest)
.build());
} else {
// Superuser transfers with no bundled renewal have no transfer billing event.
optionalTransferBillingEvent = Optional.empty();
}
// Construct the autorenew events for the losing/existing client and the gaining one. Note that
// all of the other transfer flow tests happen on day 3 of the transfer, but the initial
// request by definition takes place on day 1, so we need to edit the times in the
// autorenew events from the base test case.
BillingRecurrence losingClientAutorenew =
getLosingClientAutorenewEvent()
.asBuilder()
.setRecurrenceEndTime(implicitTransferTime)
.build();
BillingRecurrence gainingClientAutorenew =
getGainingClientAutorenewEvent()
.asBuilder()
.setEventTime(expectedExpirationTime)
.setRecurrenceLastExpansion(expectedExpirationTime.minusYears(1))
.build();
// Construct extra billing events expected by the specific test.
ImmutableSet<BillingBase> extraBillingBases =
Stream.of(extraExpectedBillingEvents)
.map(builder -> builder.setDomainHistory(historyEntryTransferRequest).build())
.collect(toImmutableSet());
// Assert that the billing events we constructed above actually exist in the database.
ImmutableSet<BillingBase> expectedBillingBases =
Streams.concat(
Stream.of(losingClientAutorenew, gainingClientAutorenew),
optionalTransferBillingEvent.stream())
.collect(toImmutableSet());
assertBillingEvents(Sets.union(expectedBillingBases, extraBillingBases));
// Assert that the domain's TransferData server-approve billing events match the above.
if (expectTransferBillingEvent) {
assertBillingEventsEqual(
loadByKey(domain.getTransferData().getServerApproveBillingEvent()),
optionalTransferBillingEvent.get());
} else {
assertThat(domain.getTransferData().getServerApproveBillingEvent()).isNull();
}
assertBillingEventsEqual(
loadByKey(domain.getTransferData().getServerApproveAutorenewEvent()),
gainingClientAutorenew);
// Assert that the full set of server-approve billing events is exactly the extra ones plus
// the transfer billing event (if present) and the gaining client autorenew.
ImmutableSet<BillingBase> expectedServeApproveBillingBases =
Streams.concat(Stream.of(gainingClientAutorenew), optionalTransferBillingEvent.stream())
.collect(toImmutableSet());
assertBillingEventsEqual(
Iterables.filter(
loadByKeys(domain.getTransferData().getServerApproveEntities()), BillingBase.class),
Sets.union(expectedServeApproveBillingBases, extraBillingBases));
// The domain's autorenew billing event should still point to the losing client's event.
BillingRecurrence domainAutorenewEvent = loadByKey(domain.getAutorenewBillingEvent());
assertThat(domainAutorenewEvent.getRegistrarId()).isEqualTo("TheRegistrar");
assertThat(domainAutorenewEvent.getRecurrenceEndTime()).isEqualTo(implicitTransferTime);
// The original grace periods should remain untouched.
assertThat(domain.getGracePeriods()).containsExactlyElementsIn(originalGracePeriods);
// If we fast forward AUTOMATIC_TRANSFER_DAYS, the transfer should have cleared out all other
// grace periods, but expect a transfer grace period (if there was a transfer billing event).
Domain domainAfterAutomaticTransfer = domain.cloneProjectedAtTime(implicitTransferTime);
if (expectTransferBillingEvent) {
assertGracePeriods(
domainAfterAutomaticTransfer.getGracePeriods(),
ImmutableMap.of(
GracePeriod.create(
GracePeriodStatus.TRANSFER,
domain.getRepoId(),
implicitTransferTime.plus(registry.getTransferGracePeriodLength()),
"NewRegistrar",
null),
optionalTransferBillingEvent.get()));
} else {
assertGracePeriods(domainAfterAutomaticTransfer.getGracePeriods(), ImmutableMap.of());
}
}
private BillingRecurrence getGainingClientAutorenewEvent() {
return new BillingRecurrence.Builder()
.setReason(Reason.RENEW)
.setFlags(ImmutableSet.of(Flag.AUTO_RENEW))
.setTargetId(domain.getDomainName())
.setRegistrarId("NewRegistrar")
.setEventTime(EXTENDED_REGISTRATION_EXPIRATION_TIME)
.setRecurrenceEndTime(END_OF_TIME)
.setDomainHistory(
getOnlyHistoryEntryOfType(
domain, HistoryEntry.Type.DOMAIN_TRANSFER_REQUEST, DomainHistory.class))
.build();
}
/** Get the autorenew event that the losing client will have after a SERVER_APPROVED transfer. */
private BillingRecurrence getLosingClientAutorenewEvent() {
return new BillingRecurrence.Builder()
.setReason(Reason.RENEW)
.setFlags(ImmutableSet.of(Flag.AUTO_RENEW))
.setTargetId(domain.getDomainName())
.setRegistrarId("TheRegistrar")
.setEventTime(REGISTRATION_EXPIRATION_TIME)
.setRecurrenceEndTime(TRANSFER_EXPIRATION_TIME)
.setDomainHistory(historyEntryDomainCreate)
.build();
}
private void assertAboutDomainAfterAutomaticTransfer(
DateTime expectedExpirationTime, DateTime implicitTransferTime, Period expectedPeriod)
throws Exception {
Tld registry = Tld.get(domain.getTld());
Domain domainAfterAutomaticTransfer = domain.cloneProjectedAtTime(implicitTransferTime);
assertTransferApproved(domainAfterAutomaticTransfer, implicitTransferTime, expectedPeriod);
assertAboutDomains()
.that(domainAfterAutomaticTransfer)
.hasRegistrationExpirationTime(expectedExpirationTime)
.and()
.hasLastEppUpdateTime(implicitTransferTime)
.and()
.hasLastEppUpdateRegistrarId("NewRegistrar");
assertThat(loadByKey(domainAfterAutomaticTransfer.getAutorenewBillingEvent()).getEventTime())
.isEqualTo(expectedExpirationTime);
// And after the expected grace time, the grace period should be gone.
Domain afterGracePeriod =
domain.cloneProjectedAtTime(
clock
.nowUtc()
.plus(registry.getAutomaticTransferLength())
.plus(registry.getTransferGracePeriodLength()));
assertThat(afterGracePeriod.getGracePeriods()).isEmpty();
}
private void assertTransferApproved(
Domain domain, DateTime automaticTransferTime, Period expectedPeriod) throws Exception {
assertAboutDomains()
.that(domain)
.hasCurrentSponsorRegistrarId("NewRegistrar")
.and()
.hasLastTransferTime(automaticTransferTime)
.and()
.doesNotHaveStatusValue(StatusValue.PENDING_TRANSFER);
Trid expectedTrid =
Trid.create(
getClientTrid(),
domain.getTransferData().getTransferRequestTrid().getServerTransactionId());
assertThat(domain.getTransferData())
.isEqualTo(
new DomainTransferData.Builder()
.setGainingRegistrarId("NewRegistrar")
.setLosingRegistrarId("TheRegistrar")
.setTransferRequestTrid(expectedTrid)
.setTransferRequestTime(clock.nowUtc())
.setTransferPeriod(expectedPeriod)
.setTransferStatus(TransferStatus.SERVER_APPROVED)
.setPendingTransferExpirationTime(automaticTransferTime)
.setTransferredRegistrationExpirationTime(domain.getRegistrationExpirationTime())
// Server-approve entity fields should all be nulled out.
.build());
}
private void assertTransferRequested(
Domain domain,
DateTime automaticTransferTime,
Period expectedPeriod,
DateTime expectedExpirationTime)
throws Exception {
assertAboutDomains()
.that(domain)
.hasCurrentSponsorRegistrarId("TheRegistrar")
.and()
.hasStatusValue(StatusValue.PENDING_TRANSFER)
.and()
.hasLastEppUpdateTime(clock.nowUtc())
.and()
.hasLastEppUpdateRegistrarId("NewRegistrar");
Trid expectedTrid =
Trid.create(
getClientTrid(),
domain.getTransferData().getTransferRequestTrid().getServerTransactionId());
assertThat(domain.getTransferData())
.isEqualTo(
// Compare against only the following fields by rebuilding the existing TransferData.
// Equivalent to assertThat(transferData.getGainingClientId()).isEqualTo("NewReg")
// and similar individual assertions, but produces a nicer error message this way.
domain
.getTransferData()
.asBuilder()
.setGainingRegistrarId("NewRegistrar")
.setLosingRegistrarId("TheRegistrar")
.setTransferRequestTrid(expectedTrid)
.setTransferRequestTime(clock.nowUtc())
.setTransferPeriod(expectedPeriod)
.setTransferStatus(TransferStatus.PENDING)
.setPendingTransferExpirationTime(automaticTransferTime)
.setTransferredRegistrationExpirationTime(expectedExpirationTime)
// Don't compare the server-approve entity fields; they're hard to reconstruct
// and logic later will check them.
.build());
}
}

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