1
0
mirror of https://github.com/google/nomulus synced 2026-02-02 11:02:23 +00:00

Compare commits

..

7 Commits

Author SHA1 Message Date
gbrodman
3f8145b44f Remove various ContactHistory references (#2949)
This keeps the ContactHistory class and tests, to avoid changing any
database-related code in this PR.
2026-01-29 21:42:59 +00:00
gbrodman
1fdacf25dc Remove pipeline/action to wipe out contact data (#2948)
We've wiped it all out now, so it's moot
2026-01-29 19:38:29 +00:00
gbrodman
41d26d8385 Remove references to contacts in domain flows (#2944)
We've moved on from contacts entirely now so the only thing we really
need to do is make sure that people don't include contacts in domain
creates or updates. This also makes auth code checking easier too,
because now the only auth code that you're allowed to provide is the
domain auth code (not a contact auth code)
2026-01-29 19:30:41 +00:00
Nilay Shah
71c9407f07 Add MosApiMetrics exporter (#2931)
* Add MosApiMetrics exporter with status code mapping

Introduces the metrics exporter for the MoSAPI system.

- Implements `MosApiMetrics` to export TLD and service states to Cloud Monitoring.
- Maps ICANN status codes to numeric gauges: 1 (UP), 0 (DOWN), and 2 (DISABLED/INCONCLUSIVE).
- Sets `MAX_TIMESERIES_PER_REQUEST` to 195 to respect Cloud Monitoring API limits

* Automate metric descriptor creation on startup in Cloud Monitoring

* Refactor MoSAPI metrics for resilience and standards

* Refactor and nits

- Kept projectName as part constant instead of inside method signature
- Added Summary logs for metrics execution
- Metric Executor defaults to Single Threaded

* junit test refactoring

* Fix Metric kind to GAUGE for all metrics

* Refactor MosApiMetrics to remove async ExecutorService

* Add LockHandler for Metric Descriptor creation

* Update LockHandler lease time to one hour and refactoring
2026-01-29 14:53:05 +00:00
gbrodman
a138806199 Re-enable old fee extensions in sandbox (#2939)
Now that we've passed the RST testing (or at least the EPP portion of
it) we are no longer bound by the restriction to only use the fee
extension version 1.0 on sandbox.

For now, in order to avoid changing prod behavior, this does not enable
advertisement of the fee extension version 1.0 in production. We can
change this at any point in the future.
2026-01-21 21:49:29 +00:00
Juan Celhay
a5c1412aac Collect JVM memory metrics (#2937)
* add jvm metrics

* include all changes

* Fix tests and lint errors

* Fix formatting

* Instantiate jvmmetrics class in stackdriver module

* add metrics registration behaviour and explicit call

* redo tests

* fix formatting/variable name

* lint
2026-01-21 21:27:07 +00:00
Nilay Shah
41393e5f8d Revert "Remove contact as a supported object type in EPP (#2932)" (#2938)
This reverts commit d8e647316e.
2026-01-21 18:35:07 +00:00
101 changed files with 4604 additions and 7481 deletions

View File

@@ -569,11 +569,6 @@ if (environment == 'alpha') {
mainClass: 'google.registry.beam.resave.ResaveAllEppResourcesPipeline',
metaData: 'google/registry/beam/resave_all_epp_resources_pipeline_metadata.json'
],
wipeOutContactHistoryPii:
[
mainClass: 'google.registry.beam.wipeout.WipeOutContactHistoryPiiPipeline',
metaData: 'google/registry/beam/wipe_out_contact_history_pii_pipeline_metadata.json'
],
]
project.tasks.create("stageBeamPipelines") {
doLast {

View File

@@ -131,12 +131,6 @@ public class BatchModule {
return extractOptionalDatetimeParameter(req, ExpandBillingRecurrencesAction.PARAM_END_TIME);
}
@Provides
@Parameter(WipeOutContactHistoryPiiAction.PARAM_CUTOFF_TIME)
static Optional<DateTime> provideCutoffTime(HttpServletRequest req) {
return extractOptionalDatetimeParameter(req, WipeOutContactHistoryPiiAction.PARAM_CUTOFF_TIME);
}
@Provides
@Parameter(ExpandBillingRecurrencesAction.PARAM_ADVANCE_CURSOR)
static boolean provideAdvanceCursor(HttpServletRequest req) {

View File

@@ -1,238 +0,0 @@
// Copyright 2025 The Nomulus Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package google.registry.batch;
import static com.google.common.net.MediaType.PLAIN_TEXT_UTF_8;
import static google.registry.flows.FlowUtils.marshalWithLenientRetry;
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
import static google.registry.util.DateTimeUtils.END_OF_TIME;
import static google.registry.util.ResourceUtils.readResourceUtf8;
import static jakarta.servlet.http.HttpServletResponse.SC_INTERNAL_SERVER_ERROR;
import static jakarta.servlet.http.HttpServletResponse.SC_NO_CONTENT;
import static jakarta.servlet.http.HttpServletResponse.SC_OK;
import static java.nio.charset.StandardCharsets.US_ASCII;
import com.google.common.base.Ascii;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.flogger.FluentLogger;
import com.google.common.util.concurrent.RateLimiter;
import google.registry.config.RegistryConfig.Config;
import google.registry.flows.EppController;
import google.registry.flows.EppRequestSource;
import google.registry.flows.PasswordOnlyTransportCredentials;
import google.registry.flows.StatelessRequestSessionMetadata;
import google.registry.model.contact.Contact;
import google.registry.model.domain.DesignatedContact;
import google.registry.model.domain.Domain;
import google.registry.model.eppcommon.ProtocolDefinition;
import google.registry.model.eppoutput.EppOutput;
import google.registry.persistence.VKey;
import google.registry.request.Action;
import google.registry.request.Response;
import google.registry.request.auth.Auth;
import google.registry.request.lock.LockHandler;
import jakarta.inject.Inject;
import jakarta.inject.Named;
import java.util.List;
import java.util.concurrent.Callable;
import java.util.logging.Level;
import javax.annotation.Nullable;
import org.joda.time.Duration;
/**
* An action that removes all contacts from all active (non-deleted) domains.
*
* <p>This implements part 1 of phase 3 of the Minimum Dataset migration, wherein we remove all uses
* of contact objects in preparation for later removing all contact data from the system.
*
* <p>This runs as a singly threaded, resumable action that loads batches of domains still
* containing contacts, and runs a superuser domain update on each one to remove the contacts,
* leaving behind a record recording that update.
*/
@Action(
service = Action.Service.BACKEND,
path = RemoveAllDomainContactsAction.PATH,
method = Action.Method.POST,
auth = Auth.AUTH_ADMIN)
public class RemoveAllDomainContactsAction implements Runnable {
public static final String PATH = "/_dr/task/removeAllDomainContacts";
private static final String LOCK_NAME = "Remove all domain contacts";
private static final String CONTACT_FMT = "<domain:contact type=\"%s\">%s</domain:contact>";
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
private final EppController eppController;
private final String registryAdminClientId;
private final LockHandler lockHandler;
private final RateLimiter rateLimiter;
private final Response response;
private final String updateDomainXml;
private int successes = 0;
private int failures = 0;
private static final int BATCH_SIZE = 10000;
@Inject
RemoveAllDomainContactsAction(
EppController eppController,
@Config("registryAdminClientId") String registryAdminClientId,
LockHandler lockHandler,
@Named("standardRateLimiter") RateLimiter rateLimiter,
Response response) {
this.eppController = eppController;
this.registryAdminClientId = registryAdminClientId;
this.lockHandler = lockHandler;
this.rateLimiter = rateLimiter;
this.response = response;
this.updateDomainXml =
readResourceUtf8(RemoveAllDomainContactsAction.class, "domain_remove_contacts.xml");
}
@Override
public void run() {
response.setContentType(PLAIN_TEXT_UTF_8);
Callable<Void> runner =
() -> {
try {
runLocked();
response.setStatus(SC_OK);
} catch (Exception e) {
logger.atSevere().withCause(e).log("Errored out during execution.");
response.setStatus(SC_INTERNAL_SERVER_ERROR);
response.setPayload(String.format("Errored out with cause: %s", e));
}
return null;
};
if (!lockHandler.executeWithLocks(runner, null, Duration.standardHours(1), LOCK_NAME)) {
// Send a 200-series status code to prevent this conflicting action from retrying.
response.setStatus(SC_NO_CONTENT);
response.setPayload("Could not acquire lock; already running?");
}
}
private void runLocked() {
logger.atInfo().log("Removing contacts on all active domains.");
List<String> domainRepoIdsBatch;
do {
domainRepoIdsBatch =
tm().<List<String>>transact(
() ->
tm().getEntityManager()
.createQuery(
"""
SELECT repoId FROM Domain WHERE deletionTime = :end_of_time AND NOT (
adminContact IS NULL AND billingContact IS NULL
AND registrantContact IS NULL AND techContact IS NULL)
""")
.setParameter("end_of_time", END_OF_TIME)
.setMaxResults(BATCH_SIZE)
.getResultList());
for (String domainRepoId : domainRepoIdsBatch) {
rateLimiter.acquire();
runDomainUpdateFlow(domainRepoId);
}
} while (!domainRepoIdsBatch.isEmpty());
String msg =
String.format(
"Finished; %d domains were successfully updated and %d errored out.",
successes, failures);
logger.at(failures == 0 ? Level.INFO : Level.WARNING).log(msg);
response.setPayload(msg);
}
private void runDomainUpdateFlow(String repoId) {
// Create a new transaction that the flow's execution will be enlisted in that loads the domain
// transactionally. This way we can ensure that nothing else has modified the domain in question
// in the intervening period since the query above found it. If a single domain update fails
// permanently, log it and move on to not block processing all the other domains.
try {
boolean success = tm().transact(() -> runDomainUpdateFlowInner(repoId));
if (success) {
successes++;
} else {
failures++;
}
} catch (Throwable t) {
logger.atWarning().withCause(t).log(
"Failed updating domain with repoId %s; skipping.", repoId);
}
}
/**
* Runs the actual domain update flow and returns whether the contact removals were successful.
*/
private boolean runDomainUpdateFlowInner(String repoId) {
Domain domain = tm().loadByKey(VKey.create(Domain.class, repoId));
if (!domain.getDeletionTime().equals(END_OF_TIME)) {
// Domain has been deleted since the action began running; nothing further to be
// done here.
logger.atInfo().log("Nothing to process for deleted domain '%s'.", domain.getDomainName());
return false;
}
logger.atInfo().log("Attempting to remove contacts on domain '%s'.", domain.getDomainName());
StringBuilder sb = new StringBuilder();
ImmutableMap<VKey<? extends Contact>, Contact> contacts =
tm().loadByKeys(
domain.getContacts().stream()
.map(DesignatedContact::getContactKey)
.collect(ImmutableSet.toImmutableSet()));
// Collect all the (non-registrant) contacts referenced by the domain and compile an EPP XML
// string that removes each one.
for (DesignatedContact designatedContact : domain.getContacts()) {
@Nullable Contact contact = contacts.get(designatedContact.getContactKey());
if (contact == null) {
logger.atWarning().log(
"Domain '%s' referenced contact with repo ID '%s' that couldn't be" + " loaded.",
domain.getDomainName(), designatedContact.getContactKey().getKey());
continue;
}
sb.append(
String.format(
CONTACT_FMT,
Ascii.toLowerCase(designatedContact.getType().name()),
contact.getContactId()))
.append("\n");
}
String compiledXml =
updateDomainXml
.replace("%DOMAIN%", domain.getDomainName())
.replace("%CONTACTS%", sb.toString());
EppOutput output =
eppController.handleEppCommand(
new StatelessRequestSessionMetadata(
registryAdminClientId, ProtocolDefinition.getVisibleServiceExtensionUris()),
new PasswordOnlyTransportCredentials(),
EppRequestSource.BACKEND,
false,
true,
compiledXml.getBytes(US_ASCII));
if (output.isSuccess()) {
logger.atInfo().log(
"Successfully removed contacts from domain '%s'.", domain.getDomainName());
} else {
logger.atWarning().log(
"Failed removing contacts from domain '%s' with error %s.",
domain.getDomainName(), new String(marshalWithLenientRetry(output), US_ASCII));
}
return output.isSuccess();
}
}

View File

@@ -1,142 +0,0 @@
// Copyright 2021 The Nomulus Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package google.registry.batch;
import static google.registry.beam.BeamUtils.createJobName;
import static google.registry.request.RequestParameters.PARAM_DRY_RUN;
import static jakarta.servlet.http.HttpServletResponse.SC_INTERNAL_SERVER_ERROR;
import static jakarta.servlet.http.HttpServletResponse.SC_OK;
import com.google.api.services.dataflow.Dataflow;
import com.google.api.services.dataflow.model.LaunchFlexTemplateParameter;
import com.google.api.services.dataflow.model.LaunchFlexTemplateRequest;
import com.google.api.services.dataflow.model.LaunchFlexTemplateResponse;
import com.google.common.collect.ImmutableMap;
import com.google.common.flogger.FluentLogger;
import com.google.common.net.MediaType;
import google.registry.beam.wipeout.WipeOutContactHistoryPiiPipeline;
import google.registry.config.RegistryConfig.Config;
import google.registry.model.contact.ContactHistory;
import google.registry.request.Action;
import google.registry.request.Parameter;
import google.registry.request.Response;
import google.registry.request.auth.Auth;
import google.registry.util.Clock;
import google.registry.util.RegistryEnvironment;
import jakarta.inject.Inject;
import java.io.IOException;
import java.util.Optional;
import org.joda.time.DateTime;
/**
* An action that launches {@link WipeOutContactHistoryPiiPipeline} to wipe out Personal
* Identifiable Information (PII) fields of {@link ContactHistory} entities.
*
* <p>{@link ContactHistory} entities should be retained in the database for only certain amount of
* time.
*/
@Action(
service = Action.Service.BACKEND,
path = WipeOutContactHistoryPiiAction.PATH,
auth = Auth.AUTH_ADMIN)
public class WipeOutContactHistoryPiiAction implements Runnable {
public static final String PATH = "/_dr/task/wipeOutContactHistoryPii";
public static final String PARAM_CUTOFF_TIME = "wipeoutTime";
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
private static final String PIPELINE_NAME = "wipe_out_contact_history_pii_pipeline";
private final Clock clock;
private final boolean isDryRun;
private final Optional<DateTime> maybeCutoffTime;
private final int minMonthsBeforeWipeOut;
private final String stagingBucketUrl;
private final String projectId;
private final String jobRegion;
private final Dataflow dataflow;
private final Response response;
@Inject
public WipeOutContactHistoryPiiAction(
Clock clock,
@Parameter(PARAM_DRY_RUN) boolean isDryRun,
@Parameter(PARAM_CUTOFF_TIME) Optional<DateTime> maybeCutoffTime,
@Config("minMonthsBeforeWipeOut") int minMonthsBeforeWipeOut,
@Config("beamStagingBucketUrl") String stagingBucketUrl,
@Config("projectId") String projectId,
@Config("defaultJobRegion") String jobRegion,
Dataflow dataflow,
Response response) {
this.clock = clock;
this.isDryRun = isDryRun;
this.maybeCutoffTime = maybeCutoffTime;
this.minMonthsBeforeWipeOut = minMonthsBeforeWipeOut;
this.stagingBucketUrl = stagingBucketUrl;
this.projectId = projectId;
this.jobRegion = jobRegion;
this.dataflow = dataflow;
this.response = response;
}
@Override
public void run() {
response.setContentType(MediaType.PLAIN_TEXT_UTF_8);
DateTime cutoffTime =
maybeCutoffTime.orElse(clock.nowUtc().minusMonths(minMonthsBeforeWipeOut));
LaunchFlexTemplateParameter launchParameter =
new LaunchFlexTemplateParameter()
.setJobName(
createJobName(
String.format(
"contact-history-pii-wipeout-%s",
cutoffTime.toString("yyyy-MM-dd't'HH-mm-ss'z'")),
clock))
.setContainerSpecGcsPath(
String.format("%s/%s_metadata.json", stagingBucketUrl, PIPELINE_NAME))
.setParameters(
ImmutableMap.of(
"registryEnvironment",
RegistryEnvironment.get().name(),
"cutoffTime",
cutoffTime.toString("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'"),
"isDryRun",
Boolean.toString(isDryRun)));
logger.atInfo().log(
"Launching Beam pipeline to wipe out all PII of contact history entities prior to %s%s.",
cutoffTime, " in dry run mode");
try {
LaunchFlexTemplateResponse launchResponse =
dataflow
.projects()
.locations()
.flexTemplates()
.launch(
projectId,
jobRegion,
new LaunchFlexTemplateRequest().setLaunchParameter(launchParameter))
.execute();
logger.atInfo().log("Got response: %s", launchResponse.getJob().toPrettyString());
response.setStatus(SC_OK);
response.setPayload(
String.format(
"Launched contact history PII wipeout pipeline: %s",
launchResponse.getJob().getId()));
} catch (IOException e) {
logger.atWarning().withCause(e).log("Pipeline Launch failed");
response.setStatus(SC_INTERNAL_SERVER_ERROR);
response.setPayload(String.format("Pipeline launch failed: %s", e.getMessage()));
}
}
}

View File

@@ -1,166 +0,0 @@
// Copyright 2023 The Nomulus Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package google.registry.beam.wipeout;
import static com.google.common.collect.ImmutableList.toImmutableList;
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
import static org.apache.beam.sdk.values.TypeDescriptors.voids;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Streams;
import google.registry.beam.common.RegistryJpaIO;
import google.registry.model.contact.ContactHistory;
import google.registry.model.reporting.HistoryEntry.HistoryEntryId;
import google.registry.persistence.PersistenceModule.TransactionIsolationLevel;
import google.registry.persistence.VKey;
import java.io.Serializable;
import org.apache.beam.sdk.Pipeline;
import org.apache.beam.sdk.PipelineResult;
import org.apache.beam.sdk.coders.KvCoder;
import org.apache.beam.sdk.coders.StringUtf8Coder;
import org.apache.beam.sdk.coders.VarLongCoder;
import org.apache.beam.sdk.metrics.Counter;
import org.apache.beam.sdk.metrics.Metrics;
import org.apache.beam.sdk.options.PipelineOptionsFactory;
import org.apache.beam.sdk.transforms.MapElements;
import org.apache.beam.sdk.transforms.join.CoGroupByKey;
import org.apache.beam.sdk.transforms.join.KeyedPCollectionTuple;
import org.apache.beam.sdk.values.KV;
import org.apache.beam.sdk.values.PCollection;
import org.apache.beam.sdk.values.TupleTag;
import org.joda.time.DateTime;
/**
* Definition of a Dataflow Flex pipeline template, which finds out {@link ContactHistory} entries
* that are older than a given age (excluding the most recent one, even if it falls with the range)
* and wipe out PII information in them.
*
* <p>To stage this template locally, run {@code ./nom_build :core:sBP --environment=alpha \
* --pipeline=wipeOutContactHistoryPii}.
*
* <p>Then, you can run the staged template via the API client library, gCloud or a raw REST call.
*/
public class WipeOutContactHistoryPiiPipeline implements Serializable {
private static final long serialVersionUID = -4111052675715913820L;
private static final TupleTag<Long> REVISIONS_TO_WIPE = new TupleTag<>();
private static final TupleTag<Long> MOST_RECENT_REVISION = new TupleTag<>();
private final DateTime cutoffTime;
private final boolean dryRun;
private final Counter contactsInScope =
Metrics.counter("WipeOutContactHistoryPii", "contacts in scope");
private final Counter historiesToWipe =
Metrics.counter("WipeOutContactHistoryPii", "contact histories to wipe PII from");
private final Counter historiesWiped =
Metrics.counter("WipeOutContactHistoryPii", "contact histories actually updated");
WipeOutContactHistoryPiiPipeline(WipeOutContactHistoryPiiPipelineOptions options) {
dryRun = options.getIsDryRun();
cutoffTime = DateTime.parse(options.getCutoffTime());
}
void setup(Pipeline pipeline) {
KeyedPCollectionTuple.of(REVISIONS_TO_WIPE, getHistoryEntriesToWipe(pipeline))
.and(MOST_RECENT_REVISION, getMostRecentHistoryEntries(pipeline))
.apply("Group by contact", CoGroupByKey.create())
.apply(
"Wipe out PII",
MapElements.into(voids())
.via(
kv -> {
String repoId = kv.getKey();
long mostRecentRevision = kv.getValue().getOnly(MOST_RECENT_REVISION);
ImmutableList<Long> revisionsToWipe =
Streams.stream(kv.getValue().getAll(REVISIONS_TO_WIPE))
.filter(e -> e != mostRecentRevision)
.collect(toImmutableList());
if (revisionsToWipe.isEmpty()) {
return null;
}
contactsInScope.inc();
tm().transact(
() -> {
for (long revisionId : revisionsToWipe) {
historiesToWipe.inc();
ContactHistory history =
tm().loadByKey(
VKey.create(
ContactHistory.class,
new HistoryEntryId(repoId, revisionId)));
// In the unlikely case where multiple pipelines run at the
// same time, or where the runner decides to rerun a particular
// transform, we might have a history entry that has already been
// wiped at this point. There's no need to wipe it again.
if (!dryRun
&& history.getContactBase().isPresent()
&& history.getContactBase().get().getEmailAddress() != null) {
historiesWiped.inc();
tm().update(history.asBuilder().wipeOutPii().build());
}
}
});
return null;
}));
}
PCollection<KV<String, Long>> getHistoryEntriesToWipe(Pipeline pipeline) {
return pipeline.apply(
"Find contact histories to wipee",
// Email is one of the required fields in EPP, meaning it's initially not null when it
// is set by EPP flows (even though it is nullalbe in the SQL schema). Therefore,
// checking if it's null is one way to avoid processing contact history entities that
// have been processed previously. Refer to RFC 5733 for more information.
RegistryJpaIO.read(
"SELECT repoId, revisionId FROM ContactHistory WHERE resource.email IS NOT NULL"
+ " AND modificationTime < :cutoffTime",
ImmutableMap.of("cutoffTime", cutoffTime),
Object[].class,
row -> KV.of((String) row[0], (long) row[1]))
.withCoder(KvCoder.of(StringUtf8Coder.of(), VarLongCoder.of())));
}
PCollection<KV<String, Long>> getMostRecentHistoryEntries(Pipeline pipeline) {
return pipeline.apply(
"Find the most recent historiy entry for each contact",
RegistryJpaIO.read(
"SELECT repoId, revisionId FROM ContactHistory"
+ " WHERE (repoId, modificationTime) IN"
+ " (SELECT repoId, MAX(modificationTime) FROM ContactHistory GROUP BY repoId)",
ImmutableMap.of(),
Object[].class,
row -> KV.of((String) row[0], (long) row[1]))
.withCoder(KvCoder.of(StringUtf8Coder.of(), VarLongCoder.of())));
}
PipelineResult run(Pipeline pipeline) {
setup(pipeline);
return pipeline.run();
}
public static void main(String[] args) {
PipelineOptionsFactory.register(WipeOutContactHistoryPiiPipelineOptions.class);
WipeOutContactHistoryPiiPipelineOptions options =
PipelineOptionsFactory.fromArgs(args)
.withValidation()
.as(WipeOutContactHistoryPiiPipelineOptions.class);
// Repeatable read should be more than enough since we are dealing with old history entries that
// are otherwise immutable.
options.setIsolationOverride(TransactionIsolationLevel.TRANSACTION_REPEATABLE_READ);
Pipeline pipeline = Pipeline.create(options);
new WipeOutContactHistoryPiiPipeline(options).run(pipeline);
}
}

View File

@@ -1,37 +0,0 @@
// Copyright 2023 The Nomulus Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package google.registry.beam.wipeout;
import google.registry.beam.common.RegistryPipelineOptions;
import org.apache.beam.sdk.options.Default;
import org.apache.beam.sdk.options.Description;
public interface WipeOutContactHistoryPiiPipelineOptions extends RegistryPipelineOptions {
@Description(
"A contact history entry with a history modification time before this time will have its PII"
+ " wiped, unless it is the most entry for the contact.")
String getCutoffTime();
void setCutoffTime(String value);
@Description(
"If true, the wiped out billing events will not be saved but the pipeline metrics counter"
+ " will still be updated.")
@Default.Boolean(false)
boolean getIsDryRun();
void setIsDryRun(boolean value);
}

View File

@@ -1264,12 +1264,6 @@ public final class RegistryConfig {
return ImmutableSet.copyOf(config.sslCertificateValidation.allowedEcdsaCurves);
}
@Provides
@Config("minMonthsBeforeWipeOut")
public static int provideMinMonthsBeforeWipeOut(RegistryConfigSettings config) {
return config.contactHistory.minMonthsBeforeWipeOut;
}
@Provides
@Config("jdbcBatchSize")
public static int provideHibernateJdbcBatchSize(RegistryConfigSettings config) {
@@ -1463,9 +1457,9 @@ public final class RegistryConfig {
}
@Provides
@Config("mosapiTldThreadCnt")
@Config("mosapiTldThreadCount")
public static int provideMosapiTldThreads(RegistryConfigSettings config) {
return config.mosapi.tldThreadCnt;
return config.mosapi.tldThreadCount;
}
private static String formatComments(String text) {

View File

@@ -39,7 +39,6 @@ public class RegistryConfigSettings {
public Beam beam;
public RegistryTool registryTool;
public SslCertificateValidation sslCertificateValidation;
public ContactHistory contactHistory;
public DnsUpdate dnsUpdate;
public BulkPricingPackageMonitoring bulkPricingPackageMonitoring;
public Bsa bsa;
@@ -223,11 +222,6 @@ public class RegistryConfigSettings {
public String expirationWarningEmailSubjectText;
}
/** Configuration for contact history. */
public static class ContactHistory {
public int minMonthsBeforeWipeOut;
}
/** Configuration for dns update. */
public static class DnsUpdate {
public String dnsUpdateFailEmailSubjectText;
@@ -272,6 +266,6 @@ public class RegistryConfigSettings {
public String entityType;
public List<String> tlds;
public List<String> services;
public int tldThreadCnt;
public int tldThreadCount;
}
}

View File

@@ -450,11 +450,6 @@ registryTool:
# OAuth client secret used by the tool.
clientSecret: YOUR_CLIENT_SECRET
# Configuration options for handling contact history.
contactHistory:
# The number of months that a ContactHistory entity should be stored in the database.
minMonthsBeforeWipeOut: 18
# Configuration options relevant to the DNS update functionality.
dnsUpdate:
dnsUpdateFailRegistryName: Example name
@@ -645,5 +640,5 @@ mosapi:
# 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
tldThreadCount: 4

View File

@@ -22,7 +22,6 @@ import com.google.common.base.Strings;
import dagger.Module;
import dagger.Provides;
import google.registry.flows.picker.FlowPicker;
import google.registry.model.contact.ContactHistory;
import google.registry.model.domain.DomainHistory;
import google.registry.model.domain.metadata.MetadataExtension;
import google.registry.model.eppcommon.AuthInfo;
@@ -267,23 +266,6 @@ public class FlowModule {
return builder;
}
/**
* Provides a partially filled in {@link ContactHistory.Builder}
*
* <p>This is not marked with {@link FlowScope} so that each retry gets a fresh one. Otherwise,
* the fact that the builder is one-use would cause NPEs.
*/
@Provides
static ContactHistory.Builder provideContactHistoryBuilder(
Trid trid,
@InputXml byte[] inputXmlBytes,
@Superuser boolean isSuperuser,
@RegistrarId String registrarId,
EppInput eppInput) {
return makeHistoryEntryBuilder(
new ContactHistory.Builder(), trid, inputXmlBytes, isSuperuser, registrarId, eppInput);
}
/**
* Provides a partially filled in {@link HostHistory.Builder}
*

View File

@@ -16,7 +16,6 @@ package google.registry.flows;
import static com.google.common.collect.Sets.intersection;
import static google.registry.model.EppResourceUtils.isLinked;
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Sets;
@@ -37,7 +36,6 @@ import google.registry.model.EppResource;
import google.registry.model.EppResource.ForeignKeyedEppResource;
import google.registry.model.EppResource.ResourceWithTransferData;
import google.registry.model.ForeignKeyUtils;
import google.registry.model.contact.Contact;
import google.registry.model.domain.Domain;
import google.registry.model.domain.DomainBase;
import google.registry.model.domain.Period;
@@ -124,14 +122,6 @@ public final class ResourceFlowUtils {
}
}
/** Check that the given AuthInfo is either missing or else is valid for the given resource. */
public static void verifyOptionalAuthInfo(Optional<AuthInfo> authInfo, Contact contact)
throws EppException {
if (authInfo.isPresent()) {
verifyAuthInfo(authInfo.get(), contact);
}
}
/** Check that the given AuthInfo is either missing or else is valid for the given resource. */
public static void verifyOptionalAuthInfo(Optional<AuthInfo> authInfo, Domain domain)
throws EppException {
@@ -142,37 +132,14 @@ public final class ResourceFlowUtils {
/** Check that the given {@link AuthInfo} is valid for the given domain. */
public static void verifyAuthInfo(AuthInfo authInfo, Domain domain) throws EppException {
final String authRepoId = authInfo.getPw().getRepoId();
String authPassword = authInfo.getPw().getValue();
if (authRepoId == null) {
// If no roid is specified, check the password against the domain's password.
String domainPassword = domain.getAuthInfo().getPw().getValue();
if (!domainPassword.equals(authPassword)) {
throw new BadAuthInfoForResourceException();
}
return;
}
// The roid should match one of the contacts.
Optional<VKey<Contact>> foundContact =
domain.getReferencedContacts().stream()
.filter(key -> key.getKey().equals(authRepoId))
.findFirst();
if (foundContact.isEmpty()) {
String authRepoId = authInfo.getPw().getRepoId();
// Previously one could auth against a contact, but we no longer hold any contact info
if (authRepoId != null) {
throw new BadAuthInfoForResourceException();
}
// Check the authInfo against the contact.
verifyAuthInfo(authInfo, tm().loadByKey(foundContact.get()));
}
/** Check that the given {@link AuthInfo} is valid for the given contact. */
public static void verifyAuthInfo(AuthInfo authInfo, Contact contact) throws EppException {
String authRepoId = authInfo.getPw().getRepoId();
String authPassword = authInfo.getPw().getValue();
String contactPassword = contact.getAuthInfo().getPw().getValue();
if (!contactPassword.equals(authPassword)
// It's unnecessary to specify a repoId on a contact auth info, but if it's there validate
// it. The usual case of this is validating a domain's auth using this method.
|| (authRepoId != null && !authRepoId.equals(contact.getRepoId()))) {
String domainPassword = domain.getAuthInfo().getPw().getValue();
if (!domainPassword.equals(authPassword)) {
throw new BadAuthInfoForResourceException();
}
}

View File

@@ -167,7 +167,6 @@ import org.joda.time.Duration;
* @error {@link DomainFlowUtils.DomainLabelBlockedByBsaException}
* @error {@link DomainFlowUtils.DomainLabelTooLongException}
* @error {@link DomainFlowUtils.DomainReservedException}
* @error {@link DomainFlowUtils.DuplicateContactForRoleException}
* @error {@link DomainFlowUtils.EmptyDomainNamePartException}
* @error {@link DomainFlowUtils.ExceedsMaxRegistrationYearsException}
* @error {@link DomainFlowUtils.ExpiredClaimException}
@@ -188,7 +187,6 @@ import org.joda.time.Duration;
* @error {@link DomainFlowUtils.MaxSigLifeNotSupportedException}
* @error {@link DomainFlowUtils.MissingBillingAccountMapException}
* @error {@link DomainFlowUtils.MissingClaimsNoticeException}
* @error {@link DomainFlowUtils.MissingContactTypeException}
* @error {@link DomainFlowUtils.NameserversNotAllowedForTldException}
* @error {@link DomainFlowUtils.NameserversNotSpecifiedForTldWithNameserverAllowListException}
* @error {@link DomainFlowUtils.PremiumNameBlockedException}
@@ -221,7 +219,8 @@ public final class DomainCreateFlow implements MutatingFlow {
@Inject DomainPricingLogic pricingLogic;
@Inject DomainDeletionTimeCache domainDeletionTimeCache;
@Inject DomainCreateFlow() {}
@Inject
DomainCreateFlow() {}
@Override
public EppResponse run() throws EppException {
@@ -378,12 +377,10 @@ public final class DomainCreateFlow implements MutatingFlow {
.setLaunchNotice(hasClaimsNotice ? launchCreate.get().getNotice() : null)
.setSmdId(signedMarkId)
.setDsData(secDnsCreate.map(SecDnsCreateExtension::getDsData).orElse(null))
.setRegistrant(command.getRegistrant())
.setAuthInfo(command.getAuthInfo())
.setDomainName(targetId)
.setNameservers(command.getNameservers().stream().collect(toImmutableSet()))
.setStatusValues(statuses)
.setContacts(command.getContacts())
.addGracePeriod(
GracePeriod.forBillingEvent(GracePeriodStatus.ADD, repoId, createBillingEvent))
.setLordnPhase(

View File

@@ -157,7 +157,8 @@ public final class DomainFlowTmchUtils {
}
/** The provided mark does not match the desired domain label. */
static class NoMarksFoundMatchingDomainException extends RequiredParameterMissingException {
public static class NoMarksFoundMatchingDomainException
extends RequiredParameterMissingException {
public NoMarksFoundMatchingDomainException() {
super("The provided mark does not match the desired domain label");
}

View File

@@ -19,7 +19,6 @@ import static com.google.common.base.Preconditions.checkState;
import static com.google.common.base.Strings.emptyToNull;
import static com.google.common.collect.ImmutableList.toImmutableList;
import static com.google.common.collect.ImmutableMap.toImmutableMap;
import static com.google.common.collect.ImmutableSetMultimap.toImmutableSetMultimap;
import static com.google.common.collect.Sets.difference;
import static com.google.common.collect.Sets.intersection;
import static com.google.common.collect.Sets.union;
@@ -45,10 +44,8 @@ import static google.registry.util.DateTimeUtils.END_OF_TIME;
import static google.registry.util.DateTimeUtils.isAtOrAfter;
import static google.registry.util.DateTimeUtils.leapSafeAddYears;
import static google.registry.util.DomainNameUtils.ACE_PREFIX;
import static java.util.Comparator.comparing;
import static java.util.stream.Collectors.joining;
import com.google.common.base.Ascii;
import com.google.common.base.CharMatcher;
import com.google.common.base.Joiner;
import com.google.common.base.Splitter;
@@ -57,9 +54,6 @@ import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableMultimap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Maps;
import com.google.common.collect.Multimap;
import com.google.common.collect.Multimaps;
import com.google.common.collect.Sets;
import com.google.common.collect.Streams;
import com.google.common.net.InternetDomainName;
@@ -81,14 +75,12 @@ import google.registry.model.billing.BillingBase.Reason;
import google.registry.model.billing.BillingRecurrence;
import google.registry.model.contact.Contact;
import google.registry.model.domain.DesignatedContact;
import google.registry.model.domain.DesignatedContact.Type;
import google.registry.model.domain.Domain;
import google.registry.model.domain.DomainCommand.Create;
import google.registry.model.domain.DomainCommand.CreateOrUpdate;
import google.registry.model.domain.DomainCommand.InvalidReferencesException;
import google.registry.model.domain.DomainCommand.Update;
import google.registry.model.domain.DomainHistory;
import google.registry.model.domain.ForeignKeyedDesignatedContact;
import google.registry.model.domain.Period;
import google.registry.model.domain.Period.Unit;
import google.registry.model.domain.fee.BaseFee;
@@ -133,10 +125,8 @@ import google.registry.tldconfig.idn.IdnLabelValidator;
import google.registry.tools.DigestType;
import google.registry.util.Idn;
import java.math.BigDecimal;
import java.util.Collection;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Optional;
import java.util.Set;
@@ -405,22 +395,11 @@ public class DomainFlowUtils {
return period;
}
/** Verify that no linked resources have disallowed statuses. */
static void verifyNotInPendingDelete(
Set<DesignatedContact> contacts,
Optional<VKey<Contact>> registrant,
Set<VKey<Host>> nameservers)
throws EppException {
ImmutableList.Builder<VKey<? extends EppResource>> keysToLoad = new ImmutableList.Builder<>();
contacts.stream().map(DesignatedContact::getContactKey).forEach(keysToLoad::add);
registrant.ifPresent(keysToLoad::add);
keysToLoad.addAll(nameservers);
verifyNotInPendingDelete(EppResource.loadByCacheIfEnabled(keysToLoad.build()).values());
}
private static void verifyNotInPendingDelete(Iterable<EppResource> resources)
throws EppException {
for (EppResource resource : resources) {
/** Verify that no linked nameservers have disallowed statuses. */
static void verifyNotInPendingDelete(ImmutableSet<VKey<Host>> nameservers)
throws StatusProhibitsOperationException {
for (EppResource resource :
EppResource.loadByCacheIfEnabled(ImmutableSet.copyOf(nameservers)).values()) {
if (resource.getStatusValues().contains(StatusValue.PENDING_DELETE)) {
throw new LinkedResourceInPendingDeleteProhibitsOperationException(
resource.getForeignKey());
@@ -428,15 +407,6 @@ public class DomainFlowUtils {
}
}
static void validateContactsHaveTypes(Set<DesignatedContact> contacts)
throws ParameterValuePolicyErrorException {
for (DesignatedContact contact : contacts) {
if (contact.getType() == null) {
throw new MissingContactTypeException();
}
}
}
static void validateNameserversCountForTld(String tld, InternetDomainName domainName, int count)
throws EppException {
// For TLDs with a nameserver allow list, all domains must have at least 1 nameserver.
@@ -451,36 +421,22 @@ public class DomainFlowUtils {
}
}
static void validateNoDuplicateContacts(Set<DesignatedContact> contacts)
/** Enforces absence of contact data on creation as part of the Minimum Dataset requirements. */
static void enforceContactAbsencesOnCreate(Create create)
throws ParameterValuePolicyErrorException {
ImmutableMultimap<Type, VKey<Contact>> contactsByType =
contacts.stream()
.collect(
toImmutableSetMultimap(
DesignatedContact::getType, DesignatedContact::getContactKey));
// If any contact type has multiple contacts:
if (contactsByType.asMap().values().stream().anyMatch(v -> v.size() > 1)) {
// Find the duplicates.
Map<Type, Collection<VKey<Contact>>> dupeKeysMap =
Maps.filterEntries(contactsByType.asMap(), e -> e.getValue().size() > 1);
ImmutableList<VKey<Contact>> dupeKeys =
dupeKeysMap.values().stream().flatMap(Collection::stream).collect(toImmutableList());
// Load the duplicates in one batch.
Map<VKey<? extends Contact>, Contact> dupeContacts = tm().loadByKeys(dupeKeys);
ImmutableMultimap.Builder<Type, VKey<Contact>> typesMap = new ImmutableMultimap.Builder<>();
dupeKeysMap.forEach(typesMap::putAll);
// Create an error message showing the type and contact IDs of the duplicates.
throw new DuplicateContactForRoleException(
Multimaps.transformValues(typesMap.build(), key -> dupeContacts.get(key).getContactId()));
}
enforceContactAbsences(create.getRegistrant(), create.getContacts());
}
/**
* Enforces the presence/absence of contact data on domain creates depending on the minimum data
* set migration schedule.
*/
static void validateCreateContactData(
/** Enforces absence of contact data on update as part of the Minimum Dataset requirements. */
static void enforceContactAbsencesOnUpdate(Update update)
throws ParameterValuePolicyErrorException {
Set<DesignatedContact> allDesignatedContacts =
Sets.union(update.getInnerAdd().getContacts(), update.getInnerRemove().getContacts());
enforceContactAbsences(update.getInnerChange().getRegistrant(), allDesignatedContacts);
}
/** Enforces the absence of contact data as part of the Minimum Dataset requirements. */
static void enforceContactAbsences(
Optional<VKey<Contact>> registrant, Set<DesignatedContact> contacts)
throws ParameterValuePolicyErrorException {
if (registrant.isPresent()) {
@@ -491,25 +447,6 @@ public class DomainFlowUtils {
}
}
/**
* Enforces the presence/absence of contact data on domain updates depending on the minimum data
* set migration schedule.
*/
static void validateUpdateContactData(
Optional<VKey<Contact>> existingRegistrant,
Optional<VKey<Contact>> newRegistrant,
Set<DesignatedContact> existingContacts,
Set<DesignatedContact> newContacts)
throws ParameterValuePolicyErrorException {
// Throw if the update specifies a new registrant that is different from the existing one.
if (newRegistrant.isPresent() && !newRegistrant.equals(existingRegistrant)) {
throw new RegistrantProhibitedException();
}
// Throw if the update specifies any new contacts that weren't already present on the domain.
if (!Sets.difference(newContacts, existingContacts).isEmpty()) {
throw new ContactsProhibitedException();
}
}
static void validateNameserversAllowedOnTld(String tld, Set<String> fullyQualifiedHostNames)
throws EppException {
@@ -1032,12 +969,9 @@ public class DomainFlowUtils {
/** Validate the contacts and nameservers specified in a domain create command. */
static void validateCreateCommandContactsAndNameservers(
Create command, Tld tld, InternetDomainName domainName) throws EppException {
verifyNotInPendingDelete(
command.getContacts(), command.getRegistrant(), command.getNameservers());
validateContactsHaveTypes(command.getContacts());
verifyNotInPendingDelete(command.getNameservers());
String tldStr = tld.getTldStr();
validateNoDuplicateContacts(command.getContacts());
validateCreateContactData(command.getRegistrant(), command.getContacts());
enforceContactAbsencesOnCreate(command);
ImmutableSet<String> hostNames = command.getNameserverHostNames();
validateNameserversCountForTld(tldStr, domainName, hostNames.size());
validateNameserversAllowedOnTld(tldStr, hostNames);
@@ -1143,17 +1077,6 @@ public class DomainFlowUtils {
.build();
}
static ImmutableSet<ForeignKeyedDesignatedContact> loadForeignKeyedDesignatedContacts(
ImmutableSet<DesignatedContact> contacts) {
ImmutableSet.Builder<ForeignKeyedDesignatedContact> builder = new ImmutableSet.Builder<>();
for (DesignatedContact contact : contacts) {
builder.add(
ForeignKeyedDesignatedContact.create(
contact.getType(), tm().loadByKey(contact.getContactKey()).getContactId()));
}
return builder.build();
}
/**
* Returns a set of DomainTransactionRecords which negate the most recent HistoryEntry's records.
*
@@ -1293,32 +1216,6 @@ public class DomainFlowUtils {
}
}
/** Missing type attribute for contact. */
static class MissingContactTypeException extends ParameterValuePolicyErrorException {
public MissingContactTypeException() {
super("Missing type attribute for contact");
}
}
/** More than one contact for a given role is not allowed. */
static class DuplicateContactForRoleException extends ParameterValuePolicyErrorException {
public DuplicateContactForRoleException(Multimap<Type, String> dupeContactsByType) {
super(
String.format(
"More than one contact for a given role is not allowed: %s",
dupeContactsByType.asMap().entrySet().stream()
.sorted(comparing(e -> e.getKey().name()))
.map(
e ->
String.format(
"role [%s] has contacts [%s]",
Ascii.toLowerCase(e.getKey().name()),
e.getValue().stream().sorted().collect(joining(", "))))
.collect(joining(", "))));
}
}
/** Declared launch extension phase does not match the current registry phase. */
static class LaunchPhaseMismatchException extends ParameterValuePolicyErrorException {
public LaunchPhaseMismatchException() {

View File

@@ -19,7 +19,6 @@ import static google.registry.flows.ResourceFlowUtils.loadAndVerifyExistence;
import static google.registry.flows.ResourceFlowUtils.verifyOptionalAuthInfo;
import static google.registry.flows.domain.DomainFlowUtils.addSecDnsExtensionIfPresent;
import static google.registry.flows.domain.DomainFlowUtils.handleFeeRequest;
import static google.registry.flows.domain.DomainFlowUtils.loadForeignKeyedDesignatedContacts;
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
import com.google.common.collect.ImmutableList;
@@ -126,15 +125,11 @@ public final class DomainInfoFlow implements MutatingFlow {
.setLastEppUpdateTime(domain.getLastEppUpdateTime())
.setRegistrationExpirationTime(domain.getRegistrationExpirationTime())
.setLastTransferTime(domain.getLastTransferTime());
domain
.getRegistrant()
.ifPresent(r -> infoBuilder.setRegistrant(tm().loadByKey(r).getContactId()));
// If authInfo is non-null, then the caller is authorized to see the full information since we
// will have already verified the authInfo is valid.
if (registrarId.equals(domain.getCurrentSponsorRegistrarId()) || authInfo.isPresent()) {
infoBuilder
.setContacts(loadForeignKeyedDesignatedContacts(domain.getContacts()))
.setSubordinateHosts(
hostsRequest.requestSubordinate() ? domain.getSubordinateHosts() : null)
.setCreationRegistrarId(domain.getCreationRegistrarId())

View File

@@ -29,14 +29,12 @@ import static google.registry.flows.ResourceFlowUtils.verifyOptionalAuthInfo;
import static google.registry.flows.ResourceFlowUtils.verifyResourceOwnership;
import static google.registry.flows.domain.DomainFlowUtils.checkAllowedAccessToTld;
import static google.registry.flows.domain.DomainFlowUtils.cloneAndLinkReferences;
import static google.registry.flows.domain.DomainFlowUtils.enforceContactAbsencesOnUpdate;
import static google.registry.flows.domain.DomainFlowUtils.updateDsData;
import static google.registry.flows.domain.DomainFlowUtils.validateContactsHaveTypes;
import static google.registry.flows.domain.DomainFlowUtils.validateDsData;
import static google.registry.flows.domain.DomainFlowUtils.validateFeesAckedIfPresent;
import static google.registry.flows.domain.DomainFlowUtils.validateNameserversAllowedOnTld;
import static google.registry.flows.domain.DomainFlowUtils.validateNameserversCountForTld;
import static google.registry.flows.domain.DomainFlowUtils.validateNoDuplicateContacts;
import static google.registry.flows.domain.DomainFlowUtils.validateUpdateContactData;
import static google.registry.flows.domain.DomainFlowUtils.verifyClientUpdateNotProhibited;
import static google.registry.flows.domain.DomainFlowUtils.verifyNotInPendingDelete;
import static google.registry.model.reporting.HistoryEntry.Type.DOMAIN_UPDATE;
@@ -64,8 +62,6 @@ import google.registry.flows.domain.DomainFlowUtils.RegistrantProhibitedExceptio
import google.registry.model.ImmutableObject;
import google.registry.model.billing.BillingBase.Reason;
import google.registry.model.billing.BillingEvent;
import google.registry.model.contact.Contact;
import google.registry.model.domain.DesignatedContact;
import google.registry.model.domain.Domain;
import google.registry.model.domain.DomainCommand.Update;
import google.registry.model.domain.DomainCommand.Update.AddRemove;
@@ -88,7 +84,6 @@ import google.registry.model.poll.PendingActionNotificationResponse.DomainPendin
import google.registry.model.poll.PollMessage;
import google.registry.model.reporting.IcannReportingTypes.ActivityReportField;
import google.registry.model.tld.Tld;
import google.registry.persistence.VKey;
import jakarta.inject.Inject;
import java.util.Objects;
import java.util.Optional;
@@ -97,8 +92,8 @@ import org.joda.time.DateTime;
/**
* An EPP flow that updates a domain.
*
* <p>Updates can change contacts, nameservers and delegation signer data of a domain. Updates
* cannot change the domain's name.
* <p>Updates can change nameservers and delegation signer data of a domain. Updates cannot change
* the domain's name.
*
* <p>Some status values (those of the form "serverSomethingProhibited") can only be applied by the
* superuser. As such, adding or removing these statuses incurs a billing event. There will be only
@@ -113,7 +108,6 @@ import org.joda.time.DateTime;
* @error {@link google.registry.flows.exceptions.OnlyToolCanPassMetadataException}
* @error {@link google.registry.flows.exceptions.ResourceHasClientUpdateProhibitedException}
* @error {@link google.registry.flows.exceptions.ResourceStatusProhibitsOperationException}
* @error {@link DomainFlowUtils.DuplicateContactForRoleException}
* @error {@link DomainFlowUtils.EmptySecDnsUpdateException}
* @error {@link DomainFlowUtils.FeesMismatchException}
* @error {@link DomainFlowUtils.FeesRequiredForNonFreeOperationException}
@@ -121,7 +115,6 @@ import org.joda.time.DateTime;
* @error {@link DomainFlowUtils.LinkedResourcesDoNotExistException}
* @error {@link DomainFlowUtils.LinkedResourceInPendingDeleteProhibitsOperationException}
* @error {@link DomainFlowUtils.MaxSigLifeChangeNotSupportedException}
* @error {@link DomainFlowUtils.MissingContactTypeException}
* @error {@link DomainFlowUtils.NameserversNotAllowedForTldException}
* @error {@link NameserversNotSpecifiedForTldWithNameserverAllowListException}
* @error {@link DomainFlowUtils.NotAuthorizedForTldException}
@@ -158,7 +151,9 @@ public final class DomainUpdateFlow implements MutatingFlow {
@Inject EppResponse.Builder responseBuilder;
@Inject DomainUpdateFlowCustomLogic flowCustomLogic;
@Inject DomainPricingLogic pricingLogic;
@Inject DomainUpdateFlow() {}
@Inject
DomainUpdateFlow() {}
@Override
public EppResponse run() throws EppException {
@@ -179,7 +174,7 @@ public final class DomainUpdateFlow implements MutatingFlow {
Domain newDomain = performUpdate(command, existingDomain, now);
DomainHistory domainHistory =
historyBuilder.setType(DOMAIN_UPDATE).setDomain(newDomain).build();
validateNewState(existingDomain, newDomain);
validateNewState(newDomain);
if (requiresDnsUpdate(existingDomain, newDomain)) {
requestDomainDnsRefresh(targetId);
}
@@ -235,12 +230,7 @@ public final class DomainUpdateFlow implements MutatingFlow {
eppInput.getSingleExtension(FeeUpdateCommandExtension.class);
FeesAndCredits feesAndCredits = pricingLogic.getUpdatePrice(tld, targetId, now);
validateFeesAckedIfPresent(feeUpdate, feesAndCredits, false);
verifyNotInPendingDelete(
add.getContacts(),
command.getInnerChange().getRegistrant(),
add.getNameservers());
validateContactsHaveTypes(add.getContacts());
validateContactsHaveTypes(remove.getContacts());
verifyNotInPendingDelete(add.getNameservers());
validateNameserversAllowedOnTld(tldStr, add.getNameserverHostNames());
}
@@ -250,7 +240,6 @@ public final class DomainUpdateFlow implements MutatingFlow {
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();
@@ -260,12 +249,7 @@ public final class DomainUpdateFlow implements MutatingFlow {
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.
Sets.SetView<DesignatedContact> newContacts =
union(Sets.difference(domain.getContacts(), remove.getContacts()), add.getContacts());
validateNoDuplicateContacts(newContacts);
enforceContactAbsencesOnUpdate(command);
Domain.Builder domainBuilder =
domain
@@ -285,9 +269,6 @@ public final class DomainUpdateFlow implements MutatingFlow {
.setLastEppUpdateRegistrarId(registrarId)
.addStatusValues(add.getStatusValues())
.removeStatusValues(remove.getStatusValues())
.removeContacts(remove.getContacts())
.addContacts(add.getContacts())
.setRegistrant(determineUpdatedRegistrant(change, domain))
.setAuthInfo(Optional.ofNullable(change.getAuthInfo()).orElse(domain.getAuthInfo()));
if (!add.getNameservers().isEmpty()) {
@@ -309,15 +290,6 @@ public final class DomainUpdateFlow implements MutatingFlow {
return domainBuilder.build();
}
private Optional<VKey<Contact>> determineUpdatedRegistrant(Change change, Domain domain) {
// During or after the minimum dataset transition, allow registrant to be removed.
if (change.getRegistrantContactId().isPresent()
&& change.getRegistrantContactId().get().isEmpty()) {
return Optional.empty();
}
return change.getRegistrant().or(domain::getRegistrant);
}
/**
* Checks whether the new state of the domain is valid.
*
@@ -325,13 +297,7 @@ public final class DomainUpdateFlow implements MutatingFlow {
* compliant with the additions or amendments, otherwise existing data can become invalid and
* cause Domain update failure.
*/
private static void validateNewState(Domain existingDomain, Domain newDomain)
throws EppException {
validateUpdateContactData(
existingDomain.getRegistrant(),
newDomain.getRegistrant(),
existingDomain.getContacts(),
newDomain.getContacts());
private static void validateNewState(Domain newDomain) throws EppException {
validateDsData(newDomain.getDsData());
validateNameserversCountForTld(
newDomain.getTld(),
@@ -345,8 +311,8 @@ public final class DomainUpdateFlow implements MutatingFlow {
Optional<MetadataExtension> metadataExtension =
eppInput.getSingleExtension(MetadataExtension.class);
if (metadataExtension.isPresent() && metadataExtension.get().getRequestedByRegistrar()) {
for (StatusValue statusValue
: symmetricDifference(existingDomain.getStatusValues(), newDomain.getStatusValues())) {
for (StatusValue statusValue :
symmetricDifference(existingDomain.getStatusValues(), newDomain.getStatusValues())) {
if (statusValue.isChargedStatus()) {
// Only charge once.
return Optional.of(

View File

@@ -23,7 +23,6 @@ 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;
@@ -61,35 +60,20 @@ public class EppXmlTransformer {
// XML schemas that should not be used in production (yet)
private static final ImmutableSet<String> NON_PROD_SCHEMAS = ImmutableSet.of("fee-std-v1.xsd");
// 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 =
private static final 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 =
private static final 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);
if (RegistryEnvironment.get().equals(RegistryEnvironment.PRODUCTION)) {
return ALL_SCHEMAS.stream()
.filter(s -> !NON_PROD_SCHEMAS.contains(s))
.collect(toImmutableList());
}
return ALL_SCHEMAS;
}
public static void validateOutput(String xml) throws XmlException {

View File

@@ -46,12 +46,14 @@ 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");
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");
/** Enum representing which environments should have which service extensions enabled. */
private enum ServiceExtensionVisibility {
ALL,
ONLY_IN_PRODUCTION,
ONLY_IN_NON_PRODUCTION,
NONE
}
@@ -64,15 +66,15 @@ public class ProtocolDefinition {
FEE_0_6(
FeeCheckCommandExtensionV06.class,
FeeCheckResponseExtensionV06.class,
ServiceExtensionVisibility.ONLY_IN_PRODUCTION),
ServiceExtensionVisibility.ALL),
FEE_0_11(
FeeCheckCommandExtensionV11.class,
FeeCheckResponseExtensionV11.class,
ServiceExtensionVisibility.ONLY_IN_PRODUCTION),
ServiceExtensionVisibility.ALL),
FEE_0_12(
FeeCheckCommandExtensionV12.class,
FeeCheckResponseExtensionV12.class,
ServiceExtensionVisibility.ONLY_IN_PRODUCTION),
ServiceExtensionVisibility.ALL),
FEE_1_00(
FeeCheckCommandExtensionStdV1.class,
FeeCheckResponseExtensionStdV1.class,
@@ -114,7 +116,6 @@ public class ProtocolDefinition {
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;

View File

@@ -27,7 +27,6 @@ import google.registry.model.UnsafeSerializable;
import google.registry.model.annotations.ExternalMessagingName;
import google.registry.model.annotations.IdAllocation;
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.domain.DomainRenewData;
@@ -272,12 +271,6 @@ public abstract class PollMessage extends ImmutableObject
return thisCastToDerived();
}
public B setContactHistoryId(HistoryEntryId historyId) {
getInstance().contactRepoId = historyId.getRepoId();
getInstance().contactHistoryRevisionId = historyId.getRevisionId();
return thisCastToDerived();
}
public B setHostHistoryId(HistoryEntryId historyId) {
getInstance().hostRepoId = historyId.getRepoId();
getInstance().hostHistoryRevisionId = historyId.getRevisionId();
@@ -290,9 +283,6 @@ public abstract class PollMessage extends ImmutableObject
if (history instanceof DomainHistory) {
return setDomainHistoryId(historyId);
}
if (history instanceof ContactHistory) {
return setContactHistoryId(historyId);
}
if (history instanceof HostHistory) {
return setHostHistoryId(historyId);
}

View File

@@ -23,8 +23,6 @@ import google.registry.model.EppResource;
import google.registry.model.ImmutableObject;
import google.registry.model.UnsafeSerializable;
import google.registry.model.annotations.IdAllocation;
import google.registry.model.contact.ContactBase;
import google.registry.model.contact.ContactHistory;
import google.registry.model.domain.DomainBase;
import google.registry.model.domain.DomainHistory;
import google.registry.model.eppcommon.Trid;
@@ -348,8 +346,6 @@ public abstract class HistoryEntry extends ImmutableObject
HistoryEntry.Builder<? extends HistoryEntry, ?> createBuilderForResource(E parent) {
if (parent instanceof DomainBase) {
return new DomainHistory.Builder().setDomain((DomainBase) parent);
} else if (parent instanceof ContactBase) {
return new ContactHistory.Builder().setContact((ContactBase) parent);
} else if (parent instanceof HostBase) {
return new HostHistory.Builder().setHost((HostBase) parent);
} else {

View File

@@ -25,8 +25,6 @@ import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Streams;
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;
@@ -46,8 +44,6 @@ public class HistoryEntryDao {
public static ImmutableMap<Class<? extends EppResource>, Class<? extends HistoryEntry>>
RESOURCE_TYPES_TO_HISTORY_TYPES =
ImmutableMap.of(
Contact.class,
ContactHistory.class,
Domain.class,
DomainHistory.class,
Host.class,
@@ -59,7 +55,6 @@ public class HistoryEntryDao {
return tm().transact(
() ->
new ImmutableList.Builder<HistoryEntry>()
.addAll(loadAllHistoryObjects(ContactHistory.class, afterTime, beforeTime))
.addAll(loadAllHistoryObjects(DomainHistory.class, afterTime, beforeTime))
.addAll(loadAllHistoryObjects(HostHistory.class, afterTime, beforeTime))
.build());
@@ -121,7 +116,6 @@ public class HistoryEntryDao {
return tm().reTransact(
() ->
Streams.concat(
loadHistoryObjectByRegistrarsInternal(ContactHistory.class, registrarIds),
loadHistoryObjectByRegistrarsInternal(DomainHistory.class, registrarIds),
loadHistoryObjectByRegistrarsInternal(HostHistory.class, registrarIds))
.sorted(Comparator.comparing(HistoryEntry::getModificationTime))

View File

@@ -24,11 +24,9 @@ import google.registry.batch.DeleteLoadTestDataAction;
import google.registry.batch.DeleteProberDataAction;
import google.registry.batch.ExpandBillingRecurrencesAction;
import google.registry.batch.RelockDomainAction;
import google.registry.batch.RemoveAllDomainContactsAction;
import google.registry.batch.ResaveAllEppResourcesPipelineAction;
import google.registry.batch.ResaveEntityAction;
import google.registry.batch.SendExpiringCertificateNotificationEmailAction;
import google.registry.batch.WipeOutContactHistoryPiiAction;
import google.registry.bsa.BsaDownloadAction;
import google.registry.bsa.BsaRefreshAction;
import google.registry.bsa.BsaValidateAction;
@@ -276,8 +274,6 @@ interface RequestComponent {
ReadinessProbeActionFrontend readinessProbeActionFrontend();
RemoveAllDomainContactsAction removeAllDomainContactsAction();
RdapAutnumAction rdapAutnumAction();
RdapDomainAction rdapDomainAction();
@@ -350,8 +346,6 @@ interface RequestComponent {
VerifyOteAction verifyOteAction();
WipeOutContactHistoryPiiAction wipeOutContactHistoryPiiAction();
@Subcomponent.Builder
abstract class Builder implements RequestComponentBuilder<RequestComponent> {
@Override

View File

@@ -0,0 +1,103 @@
// 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.monitoring.whitebox;
import com.google.common.base.Supplier;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.monitoring.metrics.LabelDescriptor;
import com.google.monitoring.metrics.MetricRegistry;
import com.google.monitoring.metrics.MetricRegistryImpl;
import jakarta.inject.Inject;
import jakarta.inject.Singleton;
import java.lang.management.ManagementFactory;
import java.lang.management.MemoryMXBean;
import java.lang.management.MemoryUsage;
/** Exposes JVM metrics. */
@Singleton
class JvmMetrics {
private static final ImmutableSet<LabelDescriptor> TYPE_LABEL_SET =
ImmutableSet.of(LabelDescriptor.create("type", "Memory type (e.g., heap, non_heap)"));
private final MemoryMXBean memoryMxBean;
@Inject
JvmMetrics() {
this(ManagementFactory.getMemoryMXBean());
}
/** Constructor for testing. */
JvmMetrics(MemoryMXBean memoryMxBean) {
this.memoryMxBean = memoryMxBean;
}
/** Registers JVM gauges with the default registry. */
void register() {
MetricRegistry registry = MetricRegistryImpl.getDefault();
registry.newGauge(
"/jvm/memory/used",
"Current memory usage in bytes",
"bytes",
TYPE_LABEL_SET,
(Supplier<ImmutableMap<ImmutableList<String>, Long>>) this::getUsedMemory,
Long.class);
registry.newGauge(
"/jvm/memory/committed",
"Committed memory in bytes",
"bytes",
TYPE_LABEL_SET,
(Supplier<ImmutableMap<ImmutableList<String>, Long>>) this::getCommittedMemory,
Long.class);
registry.newGauge(
"/jvm/memory/max",
"Maximum memory in bytes",
"bytes",
TYPE_LABEL_SET,
(Supplier<ImmutableMap<ImmutableList<String>, Long>>) this::getMaxMemory,
Long.class);
}
ImmutableMap<ImmutableList<String>, Long> getUsedMemory() {
MemoryUsage heapUsage = memoryMxBean.getHeapMemoryUsage();
MemoryUsage nonHeapUsage = memoryMxBean.getNonHeapMemoryUsage();
return ImmutableMap.of(
ImmutableList.of("heap"), heapUsage.getUsed(),
ImmutableList.of("non_heap"), nonHeapUsage.getUsed());
}
ImmutableMap<ImmutableList<String>, Long> getCommittedMemory() {
MemoryUsage heapUsage = memoryMxBean.getHeapMemoryUsage();
MemoryUsage nonHeapUsage = memoryMxBean.getNonHeapMemoryUsage();
return ImmutableMap.of(
ImmutableList.of("heap"), heapUsage.getCommitted(),
ImmutableList.of("non_heap"), nonHeapUsage.getCommitted());
}
ImmutableMap<ImmutableList<String>, Long> getMaxMemory() {
MemoryUsage heapUsage = memoryMxBean.getHeapMemoryUsage();
MemoryUsage nonHeapUsage = memoryMxBean.getNonHeapMemoryUsage();
return ImmutableMap.of(
ImmutableList.of("heap"), heapUsage.getMax(),
ImmutableList.of("non_heap"), nonHeapUsage.getMax());
}
}

View File

@@ -32,7 +32,7 @@ import jakarta.inject.Named;
import jakarta.inject.Singleton;
import org.joda.time.Duration;
/** Dagger module for Google Stackdriver service connection objects. */
/** Dagger module for monitoring and Google Stackdriver service connection objects. */
@Module
public final class StackdriverModule {
@@ -77,7 +77,11 @@ public final class StackdriverModule {
@Provides
static MetricReporter provideMetricReporter(
MetricWriter metricWriter, @Config("metricsWriteInterval") Duration writeInterval) {
MetricWriter metricWriter,
@Config("metricsWriteInterval") Duration writeInterval,
JvmMetrics jvmMetrics) {
jvmMetrics.register();
return new MetricReporter(
metricWriter,
writeInterval.getStandardSeconds(),

View File

@@ -14,21 +14,346 @@
package google.registry.mosapi;
import static com.google.common.collect.ImmutableList.toImmutableList;
import com.google.api.client.googleapis.json.GoogleJsonResponseException;
import com.google.api.services.monitoring.v3.Monitoring;
import com.google.api.services.monitoring.v3.model.CreateTimeSeriesRequest;
import com.google.api.services.monitoring.v3.model.LabelDescriptor;
import com.google.api.services.monitoring.v3.model.Metric;
import com.google.api.services.monitoring.v3.model.MetricDescriptor;
import com.google.api.services.monitoring.v3.model.MonitoredResource;
import com.google.api.services.monitoring.v3.model.Point;
import com.google.api.services.monitoring.v3.model.TimeInterval;
import com.google.api.services.monitoring.v3.model.TimeSeries;
import com.google.api.services.monitoring.v3.model.TypedValue;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Ascii;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Iterators;
import com.google.common.flogger.FluentLogger;
import google.registry.config.RegistryConfig.Config;
import google.registry.mosapi.MosApiModels.ServiceStatus;
import google.registry.mosapi.MosApiModels.TldServiceState;
import google.registry.request.lock.LockHandler;
import google.registry.util.Clock;
import jakarta.inject.Inject;
import java.io.IOException;
import java.time.Instant;
import java.util.Iterator;
import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.stream.Stream;
import org.joda.time.Duration;
/** Metrics Exporter for MoSAPI. */
public class MosApiMetrics {
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
@Inject
public MosApiMetrics() {}
// Google Cloud Monitoring Limit: Max 200 TimeSeries per request
private static final int MAX_TIMESERIES_PER_REQUEST = 195;
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");
private static final int METRICS_ALREADY_EXIST = 409;
// Magic String Constants
private static final String METRIC_DOMAIN = "custom.googleapis.com/mosapi/";
private static final String PROJECT_RESOURCE_PREFIX = "projects/";
private static final String RESOURCE_TYPE_GLOBAL = "global";
private static final String LABEL_PROJECT_ID = "project_id";
private static final String LABEL_TLD = "tld";
private static final String LABEL_SERVICE_TYPE = "service_type";
// Lock Constants
private static final String LOCK_NAME = "MosApiMetricCreation";
private static final Duration LOCK_LEASE_TIME = Duration.standardHours(1);
// Metric Names
private static final String METRIC_TLD_STATUS = "tld_status";
private static final String METRIC_SERVICE_STATUS = "service_status";
private static final String METRIC_EMERGENCY_USAGE = "emergency_usage";
private static final String GAUGE_METRIC_KIND = "GAUGE";
// Metric Display Names & Descriptions
private static final String DISPLAY_NAME_TLD_STATUS =
"Health of TLDs. 1 = UP, 0 = DOWN, 2= DISABLED/NOT_MONITORED";
private static final String DESC_TLD_STATUS = "Overall Health of TLDs reported from ICANN";
private static final String DISPLAY_NAME_SERVICE_STATUS =
"Health of Services. 1 = UP, 0 = DOWN, 2= DISABLED/NOT_MONITORED";
private static final String DESC_SERVICE_STATUS =
"Overall Health of Services reported from ICANN";
private static final String DISPLAY_NAME_EMERGENCY_USAGE =
"Percentage of Emergency Threshold Consumed";
private static final String DESC_EMERGENCY_USAGE =
"Downtime threshold that if reached by any of the monitored Services may cause the TLDs"
+ " Services emergency transition to an interim Registry Operator";
// MoSAPI Status Constants
private static final String STATUS_UP_INCONCLUSIVE = "UP-INCONCLUSIVE";
private static final String STATUS_DOWN = "DOWN";
private static final String STATUS_DISABLED = "DISABLED";
private final Monitoring monitoringClient;
private final String projectId;
private final String projectName;
private final Clock clock;
private final MonitoredResource monitoredResource;
private final LockHandler lockHandler;
// Flag to ensure we only create descriptors once, lazily
@VisibleForTesting static final AtomicBoolean isDescriptorInitialized = new AtomicBoolean(false);
@Inject
public MosApiMetrics(
Monitoring monitoringClient,
@Config("projectId") String projectId,
Clock clock,
LockHandler lockHandler) {
this.monitoringClient = monitoringClient;
this.projectId = projectId;
this.clock = clock;
this.projectName = PROJECT_RESOURCE_PREFIX + projectId;
this.lockHandler = lockHandler;
this.monitoredResource =
new MonitoredResource()
.setType(RESOURCE_TYPE_GLOBAL)
.setLabels(ImmutableMap.of(LABEL_PROJECT_ID, projectId));
}
/** Accepts a list of states and processes them in a single async batch task. */
public void recordStates(ImmutableList<TldServiceState> states) {
// If this is the first time we are recording, ensure descriptors exist.
ensureMetricDescriptorsWithLock();
pushBatchMetrics(states);
}
/**
* Attempts to create metric descriptors using a distributed lock.
*
* <p>If the lock is acquired, this instance creates the descriptors and marks itself initialized.
* If the lock is busy, it implies another instance is handling it, so we skip and proceed.
*/
private void ensureMetricDescriptorsWithLock() {
lockHandler.executeWithLocks(
() -> {
if (!isDescriptorInitialized.get()) {
createCustomMetricDescriptors();
isDescriptorInitialized.set(true);
}
return null;
},
null,
LOCK_LEASE_TIME,
LOCK_NAME);
}
// Defines the custom metrics in Cloud Monitoring
private void createCustomMetricDescriptors() {
// 1. TLD Status Descriptor
createMetricDescriptor(
METRIC_TLD_STATUS,
DISPLAY_NAME_TLD_STATUS,
DESC_TLD_STATUS,
"INT64",
ImmutableList.of(LABEL_TLD));
// 2. Service Status Descriptor
createMetricDescriptor(
METRIC_SERVICE_STATUS,
DISPLAY_NAME_SERVICE_STATUS,
DESC_SERVICE_STATUS,
"INT64",
ImmutableList.of(LABEL_TLD, LABEL_SERVICE_TYPE));
// 3. Emergency Usage Descriptor
createMetricDescriptor(
METRIC_EMERGENCY_USAGE,
DISPLAY_NAME_EMERGENCY_USAGE,
DESC_EMERGENCY_USAGE,
"DOUBLE",
ImmutableList.of(LABEL_TLD, LABEL_SERVICE_TYPE));
logger.atInfo().log("Metric descriptors ensured for project %s", projectId);
}
private void createMetricDescriptor(
String metricTypeSuffix,
String displayName,
String description,
String valueType,
ImmutableList<String> labelKeys) {
ImmutableList<LabelDescriptor> labelDescriptors =
labelKeys.stream()
.map(
key ->
new LabelDescriptor()
.setKey(key)
.setValueType("STRING")
.setDescription(
key.equals(LABEL_TLD)
? "The TLD being monitored"
: "The type of service"))
.collect(toImmutableList());
MetricDescriptor descriptor =
new MetricDescriptor()
.setType(METRIC_DOMAIN + metricTypeSuffix)
.setMetricKind(GAUGE_METRIC_KIND)
.setValueType(valueType)
.setDisplayName(displayName)
.setDescription(description)
.setLabels(labelDescriptors);
try {
monitoringClient
.projects()
.metricDescriptors()
.create(this.projectName, descriptor)
.execute();
} catch (GoogleJsonResponseException e) {
if (e.getStatusCode() == METRICS_ALREADY_EXIST) {
// the metric already exists. This is expected.
logger.atFine().log("Metric descriptor %s already exists.", metricTypeSuffix);
} else {
logger.atWarning().withCause(e).log(
"Failed to create metric descriptor %s. Status: %d",
metricTypeSuffix, e.getStatusCode());
}
} catch (Exception e) {
logger.atWarning().withCause(e).log(
"Unexpected error creating metric descriptor %s.", metricTypeSuffix);
}
}
private void pushBatchMetrics(ImmutableList<TldServiceState> states) {
Instant now = Instant.ofEpochMilli(clock.nowUtc().getMillis());
TimeInterval interval = new TimeInterval().setEndTime(now.toString());
Stream<TimeSeries> allTimeSeriesStream =
states.stream().flatMap(state -> createMetricsForState(state, interval));
Iterator<List<TimeSeries>> batchIterator =
Iterators.partition(allTimeSeriesStream.iterator(), MAX_TIMESERIES_PER_REQUEST);
int successCount = 0;
int failureCount = 0;
// Iterate and count
while (batchIterator.hasNext()) {
List<TimeSeries> batch = batchIterator.next();
try {
CreateTimeSeriesRequest request = new CreateTimeSeriesRequest().setTimeSeries(batch);
monitoringClient.projects().timeSeries().create(this.projectName, request).execute();
successCount++;
} catch (IOException e) {
failureCount++;
// Log individual batch failures, so we have the stack trace for debugging
logger.atWarning().withCause(e).log(
"Failed to push batch of %d time series.", batch.size());
}
}
// 4. Log the final summary
if (failureCount > 0) {
logger.atWarning().log(
"Metric push finished with errors. Batches Succeeded: %d, Failed: %d",
successCount, failureCount);
} else {
logger.atInfo().log("Metric push finished successfully. Batches Succeeded: %d", successCount);
}
}
/** Generates all TimeSeries (TLD + Services) for a single state object. */
private Stream<TimeSeries> createMetricsForState(TldServiceState state, TimeInterval interval) {
// 1. TLD Status
Stream<TimeSeries> tldStream = Stream.of(createTldStatusTimeSeries(state, interval));
// 2. Service Metrics (if any)
Stream<TimeSeries> serviceStream =
state.serviceStatuses().entrySet().stream()
.flatMap(
entry ->
createServiceMetricsStream(
state.tld(), entry.getKey(), entry.getValue(), interval));
return Stream.concat(tldStream, serviceStream);
}
private Stream<TimeSeries> createServiceMetricsStream(
String tld, String serviceType, ServiceStatus statusObj, TimeInterval interval) {
ImmutableMap<String, String> labels =
ImmutableMap.of(LABEL_TLD, tld, LABEL_SERVICE_TYPE, serviceType);
return Stream.of(
createTimeSeries(
METRIC_SERVICE_STATUS, labels, parseServiceStatus(statusObj.status()), interval),
createTimeSeries(METRIC_EMERGENCY_USAGE, labels, statusObj.emergencyThreshold(), interval));
}
private TimeSeries createTldStatusTimeSeries(TldServiceState state, TimeInterval interval) {
return createTimeSeries(
METRIC_TLD_STATUS,
ImmutableMap.of(LABEL_TLD, state.tld()),
parseTldStatus(state.status()),
interval);
}
private TimeSeries createTimeSeries(
String suffix, ImmutableMap<String, String> labels, Number val, TimeInterval interval) {
Metric metric = new Metric().setType(METRIC_DOMAIN + suffix).setLabels(labels);
TypedValue tv = new TypedValue();
if (val instanceof Double) {
tv.setDoubleValue((Double) val);
} else {
tv.setInt64Value(val.longValue());
}
return new TimeSeries()
.setMetric(metric)
.setResource(this.monitoredResource)
.setPoints(ImmutableList.of(new Point().setInterval(interval).setValue(tv)));
}
/**
* Translates MoSAPI status to a numeric metric.
*
* <p>Mappings: 1 (UP) = Healthy; 0 (DOWN) = Critical failure; 2 (UP-INCONCLUSIVE) = Disabled/Not
* Monitored/In Maintenance.
*
* <p>A status of 2 indicates the SLA monitoring system is under maintenance. The TLD is
* considered "UP" by default, but individual service checks are disabled. This distinguishes
* maintenance windows from actual availability or outages.
*
* @see <a href="https://www.icann.org/mosapi-specification.pdf">ICANN MoSAPI Spec Sec 5.1</a>
*/
private long parseTldStatus(String status) {
return switch (Ascii.toUpperCase(status)) {
case STATUS_DOWN -> 0;
case STATUS_UP_INCONCLUSIVE -> 2;
default -> 1; // status is up
};
}
/**
* Translates MoSAPI service status to a numeric metric.
*
* <p>Mappings: 1 (UP) = Healthy; 0 (DOWN) = Critical failure; 2 (DISABLED/UP-INCONCLUSIVE*) =
* Disabled/Not Monitored/In Maintenance.
*
* @see <a href="https://www.icann.org/mosapi-specification.pdf">ICANN MoSAPI Spec Sec 5.1</a>
*/
private long parseServiceStatus(String status) {
String serviceStatus = Ascii.toUpperCase(status);
if (serviceStatus.startsWith(STATUS_UP_INCONCLUSIVE)) {
return 2;
}
return switch (serviceStatus) {
case STATUS_DOWN -> 0;
case STATUS_DISABLED -> 2;
default -> 1; // status is Up
};
}
}

View File

@@ -26,11 +26,9 @@ 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 {
@@ -135,11 +133,12 @@ public class MosApiStateService {
tldExecutor))
.collect(toImmutableList());
List<TldServiceState> allStates =
ImmutableList<TldServiceState> allStates =
futures.stream()
.map(CompletableFuture::join)
.filter(Objects::nonNull)
.collect(Collectors.toList());
.filter(this::isValidForMetrics)
.collect(toImmutableList());
if (!allStates.isEmpty()) {
try {
@@ -152,4 +151,14 @@ public class MosApiStateService {
logger.atWarning().log("No successful TLD states fetched; skipping metrics push.");
}
}
private boolean isValidForMetrics(TldServiceState state) {
if (state.tld() == null || state.status() == null) {
logger.atSevere().log(
"Contract Violation: Received invalid state (TLD=%s, Status=%s). Skipping.",
state.tld(), state.status());
return false;
}
return true;
}
}

View File

@@ -200,7 +200,7 @@ public final class MosApiModule {
@Singleton
@Named("mosapiTldExecutor")
static ExecutorService provideMosapiTldExecutor(
@Config("mosapiTldThreadCnt") int threadPoolSize) {
@Config("mosapiTldThreadCount") int threadPoolSize) {
return Executors.newFixedThreadPool(threadPoolSize);
}
}

View File

@@ -1,39 +0,0 @@
{
"name": "Wipe Out PII From Old Contact History Entries",
"description": "An Apache Beam batch pipeline that finds old contact history entries and remove PII information from them.",
"parameters": [
{
"name": "registryEnvironment",
"label": "The Registry environment.",
"helpText": "The Registry environment.",
"is_optional": false,
"regexes": [
"^PRODUCTION|SANDBOX|CRASH|QA|ALPHA$"
]
},
{
"name": "isolationOverride",
"label": "The desired SQL transaction isolation level.",
"helpText": "The desired SQL transaction isolation level.",
"is_optional": true,
"regexes": [
"^[0-9A-Z_]+$"
]
},
{
"name": "cutoffTime",
"label": "The maximum history modification time of a contact history entry eligible for wipe out.",
"helpText": "If the history modificaiton time of contact history entry is older than this, and it is not the most recent entry of a contact, it will have its PII wiped out.",
"is_optional": true
},
{
"name": "isDryRun",
"label": "Whether this job is a dry run.",
"helpText": "If true, no changes will be saved to the database.",
"is_optional": true,
"regexes": [
"^true|false$"
]
}
]
}

View File

@@ -1,119 +0,0 @@
// Copyright 2025 The Nomulus Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package google.registry.batch;
import static com.google.common.truth.Truth.assertThat;
import static google.registry.model.common.FeatureFlag.FeatureName.MINIMUM_DATASET_CONTACTS_PROHIBITED;
import static google.registry.model.common.FeatureFlag.FeatureStatus.ACTIVE;
import static google.registry.testing.DatabaseHelper.createTld;
import static google.registry.testing.DatabaseHelper.loadByEntity;
import static google.registry.testing.DatabaseHelper.newDomain;
import static google.registry.testing.DatabaseHelper.persistActiveContact;
import static google.registry.testing.DatabaseHelper.persistResource;
import static google.registry.util.DateTimeUtils.START_OF_TIME;
import static org.mockito.Mockito.mock;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.ImmutableSortedMap;
import com.google.common.util.concurrent.RateLimiter;
import google.registry.flows.DaggerEppTestComponent;
import google.registry.flows.EppController;
import google.registry.flows.EppTestComponent.FakesAndMocksModule;
import google.registry.model.common.FeatureFlag;
import google.registry.model.contact.Contact;
import google.registry.model.domain.Domain;
import google.registry.persistence.transaction.JpaTestExtensions;
import google.registry.persistence.transaction.JpaTestExtensions.JpaIntegrationTestExtension;
import google.registry.testing.FakeClock;
import google.registry.testing.FakeLockHandler;
import google.registry.testing.FakeResponse;
import java.util.Optional;
import org.joda.time.DateTime;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;
/** Unit tests for {@link RemoveAllDomainContactsAction}. */
class RemoveAllDomainContactsActionTest {
@RegisterExtension
final JpaIntegrationTestExtension jpa =
new JpaTestExtensions.Builder().buildIntegrationTestExtension();
private final FakeResponse response = new FakeResponse();
private final RateLimiter rateLimiter = mock(RateLimiter.class);
private RemoveAllDomainContactsAction action;
@BeforeEach
void beforeEach() {
createTld("tld");
persistResource(
new FeatureFlag.Builder()
.setFeatureName(MINIMUM_DATASET_CONTACTS_PROHIBITED)
.setStatusMap(ImmutableSortedMap.of(START_OF_TIME, ACTIVE))
.build());
EppController eppController =
DaggerEppTestComponent.builder()
.fakesAndMocksModule(FakesAndMocksModule.create(new FakeClock()))
.build()
.startRequest()
.eppController();
action =
new RemoveAllDomainContactsAction(
eppController, "NewRegistrar", new FakeLockHandler(true), rateLimiter, response);
}
@Test
void test_removesAllContactsFromMultipleDomains_andDoesntModifyDomainThatHasNoContacts() {
Contact c1 = persistActiveContact("contact12345");
Domain d1 = persistResource(newDomain("foo.tld", c1));
assertThat(d1.getAllContacts()).hasSize(3);
Contact c2 = persistActiveContact("contact23456");
Domain d2 = persistResource(newDomain("bar.tld", c2));
assertThat(d2.getAllContacts()).hasSize(3);
Domain d3 =
persistResource(
newDomain("baz.tld")
.asBuilder()
.setRegistrant(Optional.empty())
.setContacts(ImmutableSet.of())
.build());
assertThat(d3.getAllContacts()).isEmpty();
DateTime lastUpdate = d3.getUpdateTimestamp().getTimestamp();
action.run();
assertThat(loadByEntity(d1).getAllContacts()).isEmpty();
assertThat(loadByEntity(d2).getAllContacts()).isEmpty();
assertThat(loadByEntity(d3).getUpdateTimestamp().getTimestamp()).isEqualTo(lastUpdate);
}
@Test
void test_removesContacts_onDomainsThatOnlyPartiallyHaveContacts() {
Contact c1 = persistActiveContact("contact12345");
Domain d1 =
persistResource(
newDomain("foo.tld", c1).asBuilder().setContacts(ImmutableSet.of()).build());
assertThat(d1.getAllContacts()).hasSize(1);
Contact c2 = persistActiveContact("contact23456");
Domain d2 =
persistResource(
newDomain("bar.tld", c2).asBuilder().setRegistrant(Optional.empty()).build());
assertThat(d2.getAllContacts()).hasSize(2);
action.run();
assertThat(loadByEntity(d1).getAllContacts()).isEmpty();
assertThat(loadByEntity(d2).getAllContacts()).isEmpty();
}
}

View File

@@ -1,133 +0,0 @@
// Copyright 2021 The Nomulus Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package google.registry.batch;
import static com.google.common.truth.Truth.assertThat;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import com.google.api.services.dataflow.model.LaunchFlexTemplateRequest;
import google.registry.beam.BeamActionTestBase;
import google.registry.testing.FakeClock;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import org.joda.time.DateTime;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;
/** Unit tests for {@link WipeOutContactHistoryPiiAction}. */
class WipeOutContactHistoryPiiActionTest extends BeamActionTestBase {
private final DateTime now = DateTime.parse("2019-01-19T01:02:03Z");
private final FakeClock clock = new FakeClock(now);
private final Map<String, String> expectedParameters = new HashMap<>();
private final ArgumentCaptor<LaunchFlexTemplateRequest> launchRequest =
ArgumentCaptor.forClass(LaunchFlexTemplateRequest.class);
private WipeOutContactHistoryPiiAction action =
new WipeOutContactHistoryPiiAction(
clock,
false,
Optional.empty(),
8,
"tucketBucket",
"testProject",
"testRegion",
dataflow,
response);
@BeforeEach
void before() {
expectedParameters.put("registryEnvironment", "UNITTEST");
expectedParameters.put("isDryRun", "false");
expectedParameters.put("cutoffTime", "2018-05-19T01:02:03.000Z");
}
@Test
void testSuccess() throws Exception {
action.run();
assertThat(response.getStatus()).isEqualTo(200);
assertThat(response.getPayload())
.isEqualTo("Launched contact history PII wipeout pipeline: jobid");
verify(templates, times(1))
.launch(eq("testProject"), eq("testRegion"), launchRequest.capture());
assertThat(launchRequest.getValue().getLaunchParameter().getParameters())
.containsExactlyEntriesIn(expectedParameters);
}
@Test
void testSuccess_providedCutoffTime() throws Exception {
action =
new WipeOutContactHistoryPiiAction(
clock,
false,
Optional.of(now.minusYears(1)),
8,
"tucketBucket",
"testProject",
"testRegion",
dataflow,
response);
action.run();
expectedParameters.put("cutoffTime", "2018-01-19T01:02:03.000Z");
assertThat(response.getStatus()).isEqualTo(200);
assertThat(response.getPayload())
.isEqualTo("Launched contact history PII wipeout pipeline: jobid");
verify(templates, times(1))
.launch(eq("testProject"), eq("testRegion"), launchRequest.capture());
assertThat(launchRequest.getValue().getLaunchParameter().getParameters())
.containsExactlyEntriesIn(expectedParameters);
}
@Test
void testSuccess_dryRun() throws Exception {
action =
new WipeOutContactHistoryPiiAction(
clock,
true,
Optional.empty(),
8,
"tucketBucket",
"testProject",
"testRegion",
dataflow,
response);
action.run();
expectedParameters.put("isDryRun", "true");
assertThat(response.getStatus()).isEqualTo(200);
assertThat(response.getPayload())
.isEqualTo("Launched contact history PII wipeout pipeline: jobid");
verify(templates, times(1))
.launch(eq("testProject"), eq("testRegion"), launchRequest.capture());
assertThat(launchRequest.getValue().getLaunchParameter().getParameters())
.containsExactlyEntriesIn(expectedParameters);
}
@Test
void testFailure_launchError() throws Exception {
when(launch.execute()).thenThrow(new IOException("cannot launch"));
action.run();
assertThat(response.getStatus()).isEqualTo(500);
assertThat(response.getPayload()).isEqualTo("Pipeline launch failed: cannot launch");
verify(templates, times(1))
.launch(eq("testProject"), eq("testRegion"), launchRequest.capture());
assertThat(launchRequest.getValue().getLaunchParameter().getParameters())
.containsExactlyEntriesIn(expectedParameters);
}
}

View File

@@ -22,7 +22,6 @@ import static google.registry.testing.DatabaseHelper.loadAllOf;
import static google.registry.testing.DatabaseHelper.loadByEntity;
import static google.registry.testing.DatabaseHelper.persistActiveContact;
import static google.registry.testing.DatabaseHelper.persistActiveDomain;
import static google.registry.testing.DatabaseHelper.persistContactWithPendingTransfer;
import static google.registry.testing.DatabaseHelper.persistDomainWithDependentResources;
import static google.registry.testing.DatabaseHelper.persistDomainWithPendingTransfer;
import static google.registry.testing.DatabaseHelper.persistNewRegistrars;
@@ -89,18 +88,6 @@ public class ResaveAllEppResourcesPipelineTest {
assertThat(loadByEntity(contact)).isEqualTo(contact);
}
@Test
void testPipeline_fulfilledContactTransfer() {
Contact contact = persistActiveContact("test123");
DateTime now = fakeClock.nowUtc();
contact = persistContactWithPendingTransfer(contact, now, now.plusDays(5), now);
fakeClock.advanceBy(Duration.standardDays(10));
assertThat(loadByEntity(contact).getStatusValues()).contains(StatusValue.PENDING_TRANSFER);
runPipeline();
assertThat(loadByEntity(contact).getStatusValues())
.doesNotContain(StatusValue.PENDING_TRANSFER);
}
@Test
void testPipeline_fulfilledDomainTransfer() {
options.setFast(true);

View File

@@ -1,196 +0,0 @@
// Copyright 2023 The Nomulus Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package google.registry.beam.wipeout;
import static com.google.common.collect.ImmutableList.toImmutableList;
import static com.google.common.truth.Truth.assertThat;
import static google.registry.model.reporting.HistoryEntry.Type.CONTACT_CREATE;
import static google.registry.persistence.PersistenceModule.TransactionIsolationLevel.TRANSACTION_REPEATABLE_READ;
import static google.registry.testing.DatabaseHelper.loadAllOf;
import static google.registry.testing.DatabaseHelper.newContact;
import static google.registry.testing.DatabaseHelper.persistResource;
import static org.hibernate.cfg.AvailableSettings.ISOLATION;
import com.google.common.collect.ImmutableList;
import google.registry.beam.TestPipelineExtension;
import google.registry.model.contact.Contact;
import google.registry.model.contact.ContactHistory;
import google.registry.model.contact.ContactPhoneNumber;
import google.registry.model.reporting.HistoryEntryDao;
import google.registry.persistence.transaction.JpaTestExtensions;
import google.registry.persistence.transaction.JpaTestExtensions.JpaIntegrationTestExtension;
import google.registry.testing.FakeClock;
import org.apache.beam.sdk.options.PipelineOptionsFactory;
import org.joda.time.DateTime;
import org.joda.time.Duration;
import org.joda.time.format.DateTimeFormat;
import org.joda.time.format.DateTimeFormatter;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;
/** Unit tests for {@link WipeOutContactHistoryPiiPipeline}. */
public class WipeOutContactHistoryPiiPipelineTest {
private static final int MIN_AGE_IN_MONTHS = 18;
private static final DateTimeFormatter DATE_TIME_FORMATTER =
DateTimeFormat.forPattern("yyyy-MM-dd'T'HH:mm:ss.SSSZ");
private final FakeClock clock = new FakeClock(DateTime.parse("2020-02-02T12:34:56Z"));
private final WipeOutContactHistoryPiiPipelineOptions options =
PipelineOptionsFactory.create().as(WipeOutContactHistoryPiiPipelineOptions.class);
private Contact contact1;
private Contact contact2;
@RegisterExtension
final JpaIntegrationTestExtension jpa =
new JpaTestExtensions.Builder()
.withClock(clock)
.withProperty(ISOLATION, TRANSACTION_REPEATABLE_READ.name())
.buildIntegrationTestExtension();
@RegisterExtension
final TestPipelineExtension pipeline =
TestPipelineExtension.create().enableAbandonedNodeEnforcement(true);
@BeforeEach
void beforeEach() {
contact1 =
persistResource(
newContact("my-contact1")
.asBuilder()
.setEmailAddress("test@example.com")
.setFaxNumber(
new ContactPhoneNumber.Builder().setPhoneNumber("+12122122122").build())
.build());
contact2 =
persistResource(
newContact("my-contact2")
.asBuilder()
.setEmailAddress("test@example.tld")
.setVoiceNumber(
new ContactPhoneNumber.Builder().setPhoneNumber("+19177199177").build())
.build());
// T = 0 month;
persistResource(createHistory(contact1));
// T = 5 months;
advanceMonths(5);
persistResource(createHistory(contact2));
// T = 10 months;
advanceMonths(5);
persistResource(createHistory(contact1));
persistResource(createHistory(contact2));
// T = 20 months;
advanceMonths(10);
persistResource(createHistory(contact2));
// T = 30 months;
advanceMonths(10);
options.setCutoffTime(DATE_TIME_FORMATTER.print(clock.nowUtc().minusMonths(MIN_AGE_IN_MONTHS)));
}
@Test
void testSuccess() {
// Before the pipeline runs, every history entry should have an emali address.
assertThat(
loadAllOf(ContactHistory.class).stream()
.filter(e -> e.getContactBase().get().getEmailAddress() != null)
.count())
.isEqualTo(5);
// Before the pipeline runs, contact history for contact1 should have fax numbers.
ImmutableList<ContactHistory> histories =
HistoryEntryDao.loadHistoryObjectsForResource(contact1.createVKey(), ContactHistory.class);
assertThat(
histories.stream().filter(e -> e.getContactBase().get().getFaxNumber() != null).count())
.isEqualTo(2);
// Before the pipeline runs, contact history for contact2 should have voice numbers.
histories =
HistoryEntryDao.loadHistoryObjectsForResource(contact2.createVKey(), ContactHistory.class);
assertThat(
histories.stream()
.filter(e -> e.getContactBase().get().getVoiceNumber() != null)
.count())
.isEqualTo(3);
WipeOutContactHistoryPiiPipeline wipeOutContactHistoryPiiPipeline =
new WipeOutContactHistoryPiiPipeline(options);
wipeOutContactHistoryPiiPipeline.run(pipeline).waitUntilFinish();
histories =
HistoryEntryDao.loadHistoryObjectsForResource(contact1.createVKey(), ContactHistory.class);
assertThat(histories.size()).isEqualTo(2);
ImmutableList<ContactHistory> wipedEntries =
histories.stream()
.filter(e -> e.getContactBase().get().getEmailAddress() == null)
.collect(toImmutableList());
// Only the history entry at T = 10 is wiped. The one at T = 10 is over 18 months old, but it
// is the most recent entry, so it is kept.
assertThat(wipedEntries.size()).isEqualTo(1);
assertThat(wipedEntries.get(0).getContactBase().get().getFaxNumber()).isNull();
// With a new history entry at T = 30, the one at T = 10 is eligible for wipe out. Note the
// current time itself (therefore the cutoff time) has not changed.
persistResource(createHistory(contact1));
wipeOutContactHistoryPiiPipeline.run(pipeline).waitUntilFinish();
histories =
HistoryEntryDao.loadHistoryObjectsForResource(contact1.createVKey(), ContactHistory.class);
assertThat(histories.size()).isEqualTo(3);
wipedEntries =
histories.stream()
.filter(e -> e.getContactBase().get().getEmailAddress() == null)
.collect(toImmutableList());
assertThat(wipedEntries.size()).isEqualTo(2);
// Check that the pipeline deals with multiple contacts correctly.
histories =
HistoryEntryDao.loadHistoryObjectsForResource(contact2.createVKey(), ContactHistory.class);
assertThat(histories.size()).isEqualTo(3);
wipedEntries =
histories.stream()
.filter(e -> e.getContactBase().get().getEmailAddress() == null)
.collect(toImmutableList());
// Only the history entry at T = 10 is wiped. The one at T = 10 is over 18 months old, but it
// is the most recent entry, so it is kept.
assertThat(wipedEntries.size()).isEqualTo(2);
assertThat(wipedEntries.get(0).getContactBase().get().getVoiceNumber()).isNull();
assertThat(wipedEntries.get(1).getContactBase().get().getVoiceNumber()).isNull();
}
@Test
void testSuccess_dryRun() {
options.setIsDryRun(true);
WipeOutContactHistoryPiiPipeline wipeOutContactHistoryPiiPipeline =
new WipeOutContactHistoryPiiPipeline(options);
wipeOutContactHistoryPiiPipeline.run(pipeline).waitUntilFinish();
ImmutableList<ContactHistory> histories =
HistoryEntryDao.loadHistoryObjectsForResource(contact1.createVKey(), ContactHistory.class);
assertThat(histories.size()).isEqualTo(2);
assertThat(
histories.stream()
.filter(e -> e.getContactBase().get().getEmailAddress() == null)
.collect(toImmutableList()))
.isEmpty();
}
private ContactHistory createHistory(Contact contact) {
return new ContactHistory.Builder()
.setContact(contact)
.setType(CONTACT_CREATE)
.setRegistrarId("TheRegistrar")
.setModificationTime(clock.nowUtc())
.build();
}
private void advanceMonths(int months) {
DateTime now = clock.nowUtc();
DateTime next = now.plusMonths(months);
clock.advanceBy(new Duration(now, next));
}
}

View File

@@ -24,13 +24,10 @@ import com.google.common.collect.Iterables;
import com.google.common.testing.TestLogHandler;
import google.registry.model.EppResource;
import google.registry.model.ForeignKeyUtils;
import google.registry.model.contact.ContactBase;
import google.registry.model.contact.ContactHistory;
import google.registry.model.domain.DomainBase;
import google.registry.model.domain.DomainHistory;
import google.registry.model.eppinput.EppInput.ResourceCommandWrapper;
import google.registry.model.eppinput.ResourceCommand;
import google.registry.model.host.HostBase;
import google.registry.model.host.HostHistory;
import google.registry.model.reporting.HistoryEntry;
import google.registry.model.tmch.ClaimsList;
@@ -131,18 +128,12 @@ public abstract class ResourceFlowTestCase<F extends Flow, R extends EppResource
protected void assertLastHistoryContainsResource(EppResource resource) {
HistoryEntry historyEntry = Iterables.getLast(DatabaseHelper.getHistoryEntries(resource));
if (resource instanceof ContactBase) {
ContactHistory contactHistory = (ContactHistory) historyEntry;
// Don't use direct equals comparison since one might be a subclass of the other
assertAboutImmutableObjects()
.that(contactHistory.getContactBase().get())
.hasFieldsEqualTo(resource);
} else if (resource instanceof DomainBase) {
if (resource instanceof DomainBase) {
DomainHistory domainHistory = (DomainHistory) historyEntry;
assertAboutImmutableObjects()
.that(domainHistory.getDomainBase().get())
.isEqualExceptFields(resource, "gracePeriods", "dsData", "nsHosts");
} else if (resource instanceof HostBase) {
} else {
HostHistory hostHistory = (HostHistory) historyEntry;
// Don't use direct equals comparison since one might be a subclass of the other
assertAboutImmutableObjects()

View File

@@ -119,7 +119,6 @@ import google.registry.flows.domain.DomainFlowUtils.MalformedTcnIdException;
import google.registry.flows.domain.DomainFlowUtils.MaxSigLifeNotSupportedException;
import google.registry.flows.domain.DomainFlowUtils.MissingBillingAccountMapException;
import google.registry.flows.domain.DomainFlowUtils.MissingClaimsNoticeException;
import google.registry.flows.domain.DomainFlowUtils.MissingContactTypeException;
import google.registry.flows.domain.DomainFlowUtils.NameserversNotAllowedForTldException;
import google.registry.flows.domain.DomainFlowUtils.NameserversNotSpecifiedForTldWithNameserverAllowListException;
import google.registry.flows.domain.DomainFlowUtils.NotAuthorizedForTldException;
@@ -209,6 +208,14 @@ 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_06_MAP =
ImmutableMap.of("FEE_VERSION", "fee-0.6", "FEE_NS", "fee", "CURRENCY", "USD", "FEE", "15.00");
private static final ImmutableMap<String, String> FEE_11_MAP =
ImmutableMap.of(
"FEE_VERSION", "fee-0.11", "FEE_NS", "fee", "CURRENCY", "USD", "FEE", "15.00");
private static final ImmutableMap<String, String> FEE_12_MAP =
ImmutableMap.of(
"FEE_VERSION", "fee-0.12", "FEE_NS", "fee", "CURRENCY", "USD", "FEE", "15.00");
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");
@@ -1866,15 +1873,6 @@ class DomainCreateFlowTest extends ResourceFlowTestCase<DomainCreateFlow, Domain
.build());
}
@Test
void testFailure_missingContactType() {
// We need to test for missing type, but not for invalid - the schema enforces that for us.
setEppInput("domain_create_missing_contact_type.xml");
persistContactsAndHosts();
EppException thrown = assertThrows(MissingContactTypeException.class, this::runFlow);
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
void testFailure_minimumDataset_noRegistrantButSomeOtherContactTypes() throws Exception {
setEppInput("domain_create_other_contact_types.xml");
@@ -3305,6 +3303,791 @@ class DomainCreateFlowTest extends ResourceFlowTestCase<DomainCreateFlow, Domain
.isEqualTo(Money.of(USD, 24));
}
@Test
void testFailure_wrongFeeAmount_v06() {
setEppInput("domain_create_fee.xml", FEE_06_MAP);
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 testFailure_wrongFeeAmount_v11() {
setEppInput("domain_create_fee.xml", FEE_11_MAP);
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 testFailure_wrongFeeAmount_v12() {
setEppInput("domain_create_fee.xml", FEE_12_MAP);
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_v06() 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", FEE_06_MAP);
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", FEE_06_MAP));
}
@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", FEE_11_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", FEE_11_MAP));
}
@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", FEE_12_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", FEE_12_MAP));
}
@Test
void testFailure_omitFeeExtensionOnLogin_v06() {
for (String uri : FEE_EXTENSION_URIS) {
removeServiceExtensionUri(uri);
}
createTld("net");
setEppInput("domain_create_fee.xml", FEE_06_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", FEE_11_MAP);
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", FEE_12_MAP);
persistContactsAndHosts();
EppException thrown = assertThrows(UndeclaredServiceExtensionException.class, this::runFlow);
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
void testSuccess_eapFeeApplied_v06() throws Exception {
setEppInput(
"domain_create_eap_fee.xml",
new ImmutableMap.Builder<String, String>()
.putAll(FEE_06_MAP)
.put("DESCRIPTION_1", "create")
.put("DESCRIPTION_2", "Early Access Period")
.build());
persistContactsAndHosts();
setEapForTld("tld");
doSuccessfulTest("tld", "domain_create_response_eap_fee.xml", FEE_06_MAP);
}
@Test
void testSuccess_eapFeeApplied_v11() throws Exception {
setEppInput(
"domain_create_eap_fee.xml",
new ImmutableMap.Builder<String, String>()
.putAll(FEE_11_MAP)
.put("DESCRIPTION_1", "create")
.put("DESCRIPTION_2", "Early Access Period")
.build());
persistContactsAndHosts();
setEapForTld("tld");
doSuccessfulTest("tld", "domain_create_response_eap_fee.xml", FEE_11_MAP);
}
@Test
void testSuccess_eapFeeApplied_v12() throws Exception {
setEppInput(
"domain_create_eap_fee.xml",
new ImmutableMap.Builder<String, String>()
.putAll(FEE_12_MAP)
.put("DESCRIPTION_1", "create")
.put("DESCRIPTION_2", "Early Access Period")
.build());
persistContactsAndHosts();
setEapForTld("tld");
doSuccessfulTest("tld", "domain_create_response_eap_fee.xml", FEE_12_MAP);
}
@Test
void testFailure_feeGivenInWrongScale_v06() {
setEppInput("domain_create_fee_bad_scale.xml", FEE_06_MAP);
persistContactsAndHosts();
EppException thrown = assertThrows(CurrencyValueScaleException.class, this::runFlow);
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
void testFailure_feeGivenInWrongScale_v11() {
setEppInput("domain_create_fee_bad_scale.xml", FEE_11_MAP);
persistContactsAndHosts();
EppException thrown = assertThrows(CurrencyValueScaleException.class, this::runFlow);
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
void testFailure_feeGivenInWrongScale_v12() {
setEppInput("domain_create_fee_bad_scale.xml", FEE_12_MAP);
persistContactsAndHosts();
EppException thrown = assertThrows(CurrencyValueScaleException.class, this::runFlow);
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
void testFailure_appliedFee_v06() {
setEppInput("domain_create_fee_applied.xml", FEE_06_MAP);
persistContactsAndHosts();
EppException thrown = assertThrows(UnsupportedFeeAttributeException.class, this::runFlow);
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
void testFailure_appliedFee_v11() {
setEppInput("domain_create_fee_applied.xml", FEE_11_MAP);
persistContactsAndHosts();
EppException thrown = assertThrows(UnsupportedFeeAttributeException.class, this::runFlow);
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
void testFailure_appliedFee_v12() {
setEppInput("domain_create_fee_applied.xml", FEE_12_MAP);
persistContactsAndHosts();
EppException thrown = assertThrows(UnsupportedFeeAttributeException.class, this::runFlow);
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@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", FEE_06_MAP);
persistContactsAndHosts();
EppException thrown = assertThrows(FeesMismatchException.class, this::runFlow);
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
void testFailure_wrongFeeAmountTooLow_defaultToken_v11() 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", FEE_11_MAP);
persistContactsAndHosts();
EppException thrown = assertThrows(FeesMismatchException.class, this::runFlow);
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@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", FEE_12_MAP);
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", "fee-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", "fee-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", "fee-0.12", "CURRENCY", "EUR"));
persistContactsAndHosts();
EppException thrown = assertThrows(CurrencyUnitMismatchException.class, this::runFlow);
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
void testFailure_gracePeriodFee_v06() {
setEppInput("domain_create_fee_grace_period.xml", FEE_06_MAP);
persistContactsAndHosts();
EppException thrown = assertThrows(UnsupportedFeeAttributeException.class, this::runFlow);
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
void testFailure_gracePeriodFee_v11() {
setEppInput("domain_create_fee_grace_period.xml", FEE_11_MAP);
persistContactsAndHosts();
EppException thrown = assertThrows(UnsupportedFeeAttributeException.class, this::runFlow);
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
void testFailure_gracePeriodFee_v12() {
setEppInput("domain_create_fee_grace_period.xml", FEE_12_MAP);
persistContactsAndHosts();
EppException thrown = assertThrows(UnsupportedFeeAttributeException.class, this::runFlow);
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
void testSuccess_fee_withDefaultAttributes_v06() throws Exception {
setEppInput("domain_create_fee_defaults.xml", FEE_06_MAP);
persistContactsAndHosts();
doSuccessfulTest(
"tld",
"domain_create_response_fee.xml",
ImmutableMap.of("FEE_VERSION", "fee-0.6", "FEE", "24.00"));
}
@Test
void testSuccess_fee_withDefaultAttributes_v11() throws Exception {
setEppInput("domain_create_fee_defaults.xml", FEE_11_MAP);
persistContactsAndHosts();
doSuccessfulTest(
"tld",
"domain_create_response_fee.xml",
ImmutableMap.of("FEE_VERSION", "fee-0.11", "FEE", "24.00"));
}
@Test
void testSuccess_fee_withDefaultAttributes_v12() throws Exception {
setEppInput("domain_create_fee_defaults.xml", FEE_12_MAP);
persistContactsAndHosts();
doSuccessfulTest(
"tld",
"domain_create_response_fee.xml",
ImmutableMap.of("FEE_VERSION", "fee-0.12", "FEE", "24.00"));
}
@Test
void testFailure_refundableFee_v06() {
setEppInput("domain_create_fee_refundable.xml", FEE_06_MAP);
persistContactsAndHosts();
EppException thrown = assertThrows(UnsupportedFeeAttributeException.class, this::runFlow);
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
void testFailure_refundableFee_v11() {
setEppInput("domain_create_fee_refundable.xml", FEE_11_MAP);
persistContactsAndHosts();
EppException thrown = assertThrows(UnsupportedFeeAttributeException.class, this::runFlow);
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
void testFailure_refundableFee_v12() {
setEppInput("domain_create_fee_refundable.xml", FEE_12_MAP);
persistContactsAndHosts();
EppException thrown = assertThrows(UnsupportedFeeAttributeException.class, this::runFlow);
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
void testSuccess_fee_v06() throws Exception {
setEppInput("domain_create_fee.xml", FEE_06_MAP);
persistContactsAndHosts();
doSuccessfulTest(
"tld",
"domain_create_response_fee.xml",
ImmutableMap.of("FEE_VERSION", "fee-0.6", "FEE", "24.00"));
}
@Test
void testSuccess_fee_v11() throws Exception {
setEppInput("domain_create_fee.xml", FEE_11_MAP);
persistContactsAndHosts();
doSuccessfulTest(
"tld",
"domain_create_response_fee.xml",
ImmutableMap.of("FEE_VERSION", "fee-0.11", "FEE", "24.00"));
}
@Test
void testSuccess_fee_v12() throws Exception {
setEppInput("domain_create_fee.xml", FEE_12_MAP);
persistContactsAndHosts();
doSuccessfulTest(
"tld",
"domain_create_response_fee.xml",
ImmutableMap.of("FEE_VERSION", "fee-0.12", "FEE", "24.00"));
}
@Test
void testFailure_eapFee_description_multipleMatch_v06() {
setEppInput(
"domain_create_eap_fee.xml",
ImmutableMap.of(
"FEE_VERSION",
"fee-0.6",
"DESCRIPTION_1",
"create",
"DESCRIPTION_2",
"renew transfer"));
persistContactsAndHosts();
setEapForTld("tld");
EppException thrown = assertThrows(FeeDescriptionMultipleMatchesException.class, this::runFlow);
assertThat(thrown).hasMessageThat().contains("RENEW, TRANSFER");
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
void testFailure_unknownCurrency_v12() {
setEppInput(
"domain_create_fee.xml", ImmutableMap.of("FEE_VERSION", "fee-0.12", "CURRENCY", "BAD"));
persistContactsAndHosts();
EppException thrown = assertThrows(UnknownCurrencyEppException.class, this::runFlow);
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
void testTieredPricingPromoResponse_v12() throws Exception {
sessionMetadata.setRegistrarId("NewRegistrar");
setupDefaultTokenWithDiscount("NewRegistrar");
setEppInput("domain_create_fee.xml", FEE_12_MAP);
persistContactsAndHosts();
// Fee in the result should be 24 (create cost of 13 plus renew cost of 11) even though the
// actual cost is lower (due to the tiered pricing promo)
runFlowAssertResponse(
loadFile(
"domain_create_response_fee.xml",
ImmutableMap.of("FEE_VERSION", "fee-0.12", "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 testSuccess_eapFee_multipleEAPfees_doNotAddToExpectedValue_v06() {
setEppInput(
"domain_create_extra_fees.xml",
new ImmutableMap.Builder<String, String>()
.put("FEE_VERSION", "fee-0.6")
.put("DESCRIPTION_1", "create")
.put("FEE_1", "24")
.put("DESCRIPTION_2", "Early Access Period")
.put("FEE_2", "55")
.put("DESCRIPTION_3", "Early Access Period")
.put("FEE_3", "55")
.build());
persistContactsAndHosts();
setEapForTld("tld");
EppException thrown = assertThrows(FeesMismatchException.class, this::runFlow);
assertThat(thrown).hasMessageThat().contains("expected fee of USD 100.00");
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
void testFailure_eapFee_description_swapped_v06() {
setEppInput(
"domain_create_eap_fee.xml",
ImmutableMap.of(
"FEE_VERSION",
"fee-0.6",
"DESCRIPTION_1",
"Early Access Period",
"DESCRIPTION_2",
"create"));
persistContactsAndHosts();
setEapForTld("tld");
EppException thrown = assertThrows(FeesMismatchException.class, this::runFlow);
assertThat(thrown).hasMessageThat().contains("CREATE");
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
void testSuccess_doesNotApplyNonPremiumDefaultTokenToPremiumName_v12() throws Exception {
persistContactsAndHosts();
createTld("example");
persistResource(
setupDefaultTokenWithDiscount()
.asBuilder()
.setAllowedTlds(ImmutableSet.of("example"))
.build());
setEppInput("domain_create_premium.xml", FEE_12_MAP);
runFlowAssertResponse(
loadFile(
"domain_create_response_premium.xml",
ImmutableMap.of(
"FEE_VERSION", "fee-0.12", "EXDATE", "2001-04-03T22:00:00.0Z", "FEE", "200.00")));
assertSuccessfulCreate("example", ImmutableSet.of(), 200);
}
@Test
void testSuccess_superuserOverridesPremiumNameBlock_v12() throws Exception {
createTld("example");
setEppInput("domain_create_premium.xml", FEE_12_MAP);
persistContactsAndHosts("net");
// Modify the Registrar to block premium names.
persistResource(loadRegistrar("TheRegistrar").asBuilder().setBlockPremiumNames(true).build());
runFlowAssertResponse(
CommitMode.LIVE,
SUPERUSER,
loadFile(
"domain_create_response_premium.xml",
ImmutableMap.of(
"FEE_VERSION", "fee-0.12", "EXDATE", "2001-04-03T22:00:00.0Z", "FEE", "200.00")));
assertSuccessfulCreate("example", ImmutableSet.of(), 200);
}
@Test
void testFailure_eapFee_combined_v06() {
setEppInput("domain_create_eap_combined_fee.xml", FEE_06_MAP);
persistContactsAndHosts();
setEapForTld("tld");
EppException thrown = assertThrows(FeeDescriptionParseException.class, this::runFlow);
assertThat(thrown).hasMessageThat().contains("No fee description");
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
void testSuccess_nonpremiumCreateToken_v06() throws Exception {
createTld("example");
persistContactsAndHosts();
persistResource(
new AllocationToken.Builder()
.setToken("abc123")
.setTokenType(SINGLE_USE)
.setRegistrationBehavior(RegistrationBehavior.NONPREMIUM_CREATE)
.setDomainName("rich.example")
.build());
setEppInput(
"domain_create_premium_allocationtoken.xml",
ImmutableMap.of("FEE_VERSION", "fee-0.6", "YEARS", "1", "FEE", "13.00"));
runFlowAssertResponse(loadFile("domain_create_nonpremium_token_response.xml", FEE_06_MAP));
}
@Test
void testSuccess_eapFee_fullDescription_includingArbitraryExpiryTime_v06() throws Exception {
setEppInput(
"domain_create_eap_fee.xml",
ImmutableMap.of(
"FEE_VERSION",
"fee-0.6",
"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", FEE_06_MAP);
}
@Test
void testSuccess_allocationToken_multiYearDiscount_worksForPremiums_v06() throws Exception {
createTld("example");
persistContactsAndHosts();
persistResource(
new AllocationToken.Builder()
.setToken("abc123")
.setTokenType(SINGLE_USE)
.setDomainName("rich.example")
.setDiscountFraction(0.98)
.setDiscountYears(2)
.setDiscountPremiums(true)
.setTokenStatusTransitions(
ImmutableSortedMap.<DateTime, TokenStatus>naturalOrder()
.put(START_OF_TIME, TokenStatus.NOT_STARTED)
.put(clock.nowUtc().plusMillis(1), TokenStatus.VALID)
.put(clock.nowUtc().plusSeconds(1), TokenStatus.ENDED)
.build())
.build());
clock.advanceOneMilli();
setEppInput(
"domain_create_premium_allocationtoken.xml",
ImmutableMap.of("FEE_VERSION", "fee-0.6", "YEARS", "3", "FEE", "104.00"));
runFlowAssertResponse(
loadFile(
"domain_create_response_premium.xml",
ImmutableMap.of(
"FEE_VERSION", "fee-0.6", "EXDATE", "2002-04-03T22:00:00.0Z", "FEE", "104.00")));
BillingEvent billingEvent =
Iterables.getOnlyElement(DatabaseHelper.loadAllOf(BillingEvent.class));
assertThat(billingEvent.getTargetId()).isEqualTo("rich.example");
// 1yr @ $100 + 2yrs @ $100 * (1 - 0.98) = $104
assertThat(billingEvent.getCost()).isEqualTo(Money.of(USD, 104.00));
}
@Test
void testSuccess_eapFee_multipleEAPfees_addToExpectedValue_v06() throws Exception {
setEppInput(
"domain_create_extra_fees.xml",
new ImmutableMap.Builder<String, String>()
.put("FEE_VERSION", "fee-0.6")
.put("DESCRIPTION_1", "create")
.put("FEE_1", "24")
.put("DESCRIPTION_2", "Early Access Period")
.put("FEE_2", "55")
.put("DESCRIPTION_3", "Early Access Period")
.put("FEE_3", "45")
.build());
persistContactsAndHosts();
setEapForTld("tld");
doSuccessfulTest("tld", "domain_create_response_eap_fee.xml", FEE_06_MAP);
}
@Test
void testFailure_eapFee_totalAmountNotMatched_v06() {
setEppInput(
"domain_create_extra_fees.xml",
new ImmutableMap.Builder<String, String>()
.put("FEE_VERSION", "fee-0.6")
.put("DESCRIPTION_1", "create")
.put("FEE_1", "24")
.put("DESCRIPTION_2", "Early Access Period")
.put("FEE_2", "100")
.put("DESCRIPTION_3", "renew")
.put("FEE_3", "55")
.build());
persistContactsAndHosts();
setEapForTld("tld");
EppException thrown = assertThrows(FeesMismatchException.class, this::runFlow);
assertThat(thrown).hasMessageThat().contains("expected total of USD 124.00");
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
void testSuccess_premiumAndEap_v06() throws Exception {
createTld("example");
setEppInput("domain_create_premium_eap.xml", FEE_06_MAP);
persistContactsAndHosts("net");
persistResource(
Tld.get("example")
.asBuilder()
.setEapFeeSchedule(
ImmutableSortedMap.of(
START_OF_TIME,
Money.of(USD, 0),
clock.nowUtc().minusDays(1),
Money.of(USD, 100),
clock.nowUtc().plusDays(1),
Money.of(USD, 0)))
.build());
assertMutatingFlow(true);
runFlowAssertResponse(
CommitMode.LIVE,
UserPrivileges.NORMAL,
loadFile("domain_create_response_premium_eap.xml", FEE_06_MAP));
assertSuccessfulCreate("example", ImmutableSet.of(), 200);
assertNoLordn();
}
@Test
void testFailure_premiumBlocked_v06() {
createTld("example");
setEppInput("domain_create_premium.xml", FEE_06_MAP);
persistContactsAndHosts("net");
// 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_allocationToken_singleYearDiscount_worksForPremiums_v06() throws Exception {
createTld("example");
persistContactsAndHosts();
persistResource(
new AllocationToken.Builder()
.setToken("abc123")
.setTokenType(SINGLE_USE)
.setDomainName("rich.example")
.setDiscountFraction(0.95555)
.setDiscountPremiums(true)
.setTokenStatusTransitions(
ImmutableSortedMap.<DateTime, TokenStatus>naturalOrder()
.put(START_OF_TIME, TokenStatus.NOT_STARTED)
.put(clock.nowUtc().plusMillis(1), TokenStatus.VALID)
.put(clock.nowUtc().plusSeconds(1), TokenStatus.ENDED)
.build())
.build());
clock.advanceOneMilli();
setEppInput(
"domain_create_premium_allocationtoken.xml",
ImmutableMap.of("FEE_VERSION", "fee-0.6", "YEARS", "3", "FEE", "204.44"));
runFlowAssertResponse(
loadFile(
"domain_create_response_premium.xml",
ImmutableMap.of(
"FEE_VERSION", "fee-0.6", "EXDATE", "2002-04-03T22:00:00.0Z", "FEE", "204.44")));
BillingEvent billingEvent =
Iterables.getOnlyElement(DatabaseHelper.loadAllOf(BillingEvent.class));
assertThat(billingEvent.getTargetId()).isEqualTo("rich.example");
// 2yrs @ $100 + 1yr @ $100 * (1 - 0.95555) = $204.44
assertThat(billingEvent.getCost()).isEqualTo(Money.of(USD, 204.44));
}
@Test
void testTieredPricingPromo_registrarIncluded_noTokenActive_v12() throws Exception {
sessionMetadata.setRegistrarId("NewRegistrar");
persistActiveDomain("example1.tld");
persistResource(
setupDefaultTokenWithDiscount("NewRegistrar")
.asBuilder()
.setTokenStatusTransitions(
ImmutableSortedMap.of(
START_OF_TIME,
TokenStatus.NOT_STARTED,
clock.nowUtc().plusDays(1),
TokenStatus.VALID))
.build());
setEppInput("domain_create_fee.xml", FEE_12_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", "fee-0.12", "FEE", "24.00")));
assertThat(Iterables.getOnlyElement(loadAllOf(BillingEvent.class)).getCost())
.isEqualTo(Money.of(USD, 24));
}
@Test
void testTieredPricingPromo_registrarNotIncluded_standardResponse_v12() throws Exception {
setupDefaultTokenWithDiscount("NewRegistrar");
setEppInput("domain_create_fee.xml", FEE_12_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", "fee-0.12", "FEE", "24.00")));
assertThat(Iterables.getOnlyElement(loadAllOf(BillingEvent.class)).getCost())
.isEqualTo(Money.of(USD, 24));
}
@Test
void testSuccess_nonAnchorTenant_nonPremiumRenewal_v06() throws Exception {
createTld("example");
AllocationToken token =
persistResource(
new AllocationToken.Builder()
.setToken("abc123")
.setTokenType(SINGLE_USE)
.setDomainName("rich.example")
.setRenewalPriceBehavior(NONPREMIUM)
.build());
persistContactsAndHosts();
// Creation is still $100 but it'll create a NONPREMIUM renewal
setEppInput(
"domain_create_premium_allocationtoken.xml",
ImmutableMap.of("FEE_VERSION", "fee-0.6", "YEARS", "2", "FEE", "111.00"));
runFlow();
assertSuccessfulCreate("example", ImmutableSet.of(), token, 111);
}
@Test
void testSuccess_specifiedRenewalPriceToken_specifiedRecurrencePrice_v06() throws Exception {
createTld("example");
AllocationToken token =
persistResource(
new AllocationToken.Builder()
.setToken("abc123")
.setTokenType(SINGLE_USE)
.setDomainName("rich.example")
.setRenewalPriceBehavior(SPECIFIED)
.setRenewalPrice(Money.of(USD, 1))
.build());
persistContactsAndHosts();
// Creation is still $100 but it'll create a $1 renewal
setEppInput(
"domain_create_premium_allocationtoken.xml",
ImmutableMap.of("FEE_VERSION", "fee-0.6", "YEARS", "2", "FEE", "101.00"));
runFlow();
assertSuccessfulCreate("example", ImmutableSet.of(), token, 101, 1);
}
private AllocationToken setupDefaultTokenWithDiscount() {
return setupDefaultTokenWithDiscount("TheRegistrar");
}

View File

@@ -1,440 +0,0 @@
// 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,6 +86,7 @@ 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;
@@ -121,6 +122,12 @@ 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", "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 static final ImmutableMap<String, String> FEE_STD_1_0_MAP =
ImmutableMap.of("FEE_VERSION", "epp:fee-1.0", "FEE_NS", "fee1_00");
@@ -1233,4 +1240,144 @@ class DomainDeleteFlowTest extends ResourceFlowTestCase<DomainDeleteFlow, Domain
+ "history_registrar_id,history_modification_time,history_other_registrar_id,"
+ "history_period_unit,history_period_value,history_reason,history");
}
@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 {
setEppInput("domain_delete.xml", FEE_11_MAP);
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);
}
@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);
}
@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());
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());
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());
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());
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));
}
@Test
void testSuccess_freeCreation_deletionDuringGracePeriod_v12() 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 =
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"));
}
}

View File

@@ -1,523 +0,0 @@
// 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,12 +17,15 @@ 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;
@@ -38,9 +41,15 @@ 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;
@@ -663,4 +672,353 @@ class DomainInfoFlowTest extends ResourceFlowTestCase<DomainInfoFlow, Domain> {
setEppInput("domain_info_bulk.xml");
doSuccessfulTest("domain_info_response_unauthorized.xml", false);
}
// The fee extension is no longer supported in domain:info commands as of version 1.0.
// For now, we still support old versions.
@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

@@ -1,827 +0,0 @@
// 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,6 +119,12 @@ 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", "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 static final ImmutableMap<String, String> FEE_STD_1_0_MAP =
updateSubstitutions(FEE_BASE_MAP, "FEE_VERSION", "epp:fee-1.0", "FEE_NS", "fee1_00");
@@ -1561,4 +1567,538 @@ class DomainRenewFlowTest extends ResourceFlowTestCase<DomainRenewFlow, Domain>
EppException thrown = assertThrows(FeesMismatchException.class, this::runFlow);
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@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));
}
}

View File

@@ -1,351 +0,0 @@
// 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,6 +86,12 @@ 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", "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");
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");
@@ -677,4 +683,222 @@ class DomainRestoreRequestFlowTest extends ResourceFlowTestCase<DomainRestoreReq
assertThat(thrown).hasMessageThat().contains("domain restore reports are not supported");
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@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();
}
@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();
}
}

View File

@@ -64,7 +64,6 @@ import google.registry.model.billing.BillingBase.RenewalPriceBehavior;
import google.registry.model.billing.BillingCancellation;
import google.registry.model.billing.BillingEvent;
import google.registry.model.billing.BillingRecurrence;
import google.registry.model.contact.ContactAuthInfo;
import google.registry.model.domain.Domain;
import google.registry.model.domain.DomainAuthInfo;
import google.registry.model.domain.DomainHistory;
@@ -153,12 +152,6 @@ class DomainTransferApproveFlowTest
.build());
}
private void setEppLoader(String commandFilename) {
setEppInput(commandFilename);
// Replace the ROID in the xml file with the one generated in our test.
eppLoader.replaceAll("JD1234-REP", contact.getRepoId());
}
/**
* Runs a successful test, with the expectedCancellationBillingEvents parameter containing a list
* of billing event builders that will be filled out with the correct HistoryEntry parent as it is
@@ -184,7 +177,7 @@ class DomainTransferApproveFlowTest
String expectedXmlFilename,
DateTime expectedExpirationTime)
throws Exception {
setEppLoader(commandFilename);
setEppInput(commandFilename);
Tld registry = Tld.get(tld);
domain = reloadResourceByForeignKey();
// Make sure the implicit billing event is there; it will be deleted by the flow.
@@ -361,7 +354,7 @@ class DomainTransferApproveFlowTest
}
private void doFailingTest(String commandFilename) throws Exception {
setEppLoader(commandFilename);
setEppInput(commandFilename);
// Setup done; run the test.
assertMutatingFlow(true);
runFlow();
@@ -376,7 +369,7 @@ class DomainTransferApproveFlowTest
@Test
void testDryRun() throws Exception {
setEppLoader("domain_transfer_approve.xml");
setEppInput("domain_transfer_approve.xml");
dryRunFlowAssertResponse(loadFile("domain_transfer_approve_response.xml"));
}
@@ -492,14 +485,6 @@ class DomainTransferApproveFlowTest
"domain_transfer_approve_response.xml");
}
@Test
void testSuccess_contactAuthInfo() throws Exception {
doSuccessfulTest(
"tld",
"domain_transfer_approve_contact_authinfo.xml",
"domain_transfer_approve_response.xml");
}
@Test
void testSuccess_autorenewBeforeTransfer() throws Exception {
domain = reloadResourceByForeignKey();
@@ -619,14 +604,8 @@ class DomainTransferApproveFlowTest
}
@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());
void testFailure_contactPassword() {
// Contact passwords cannot be provided because we don't store contacts
EppException thrown =
assertThrows(
BadAuthInfoForResourceException.class,

View File

@@ -43,7 +43,6 @@ import google.registry.flows.ResourceFlowUtils.ResourceDoesNotExistException;
import google.registry.flows.domain.DomainFlowUtils.NotAuthorizedForTldException;
import google.registry.flows.exceptions.NotPendingTransferException;
import google.registry.flows.exceptions.NotTransferInitiatorException;
import google.registry.model.contact.ContactAuthInfo;
import google.registry.model.domain.Domain;
import google.registry.model.domain.DomainAuthInfo;
import google.registry.model.domain.DomainHistory;
@@ -75,8 +74,6 @@ class DomainTransferCancelFlowTest
private void doSuccessfulTest(String commandFilename) throws Exception {
setEppInput(commandFilename);
// Replace the ROID in the xml file with the one generated in our test.
eppLoader.replaceAll("JD1234-REP", contact.getRepoId());
// Make sure the implicit billing event is there; it will be deleted by the flow.
// We also expect to see autorenew events for the gaining and losing registrars.
assertBillingEvents(
@@ -187,8 +184,6 @@ class DomainTransferCancelFlowTest
private void doFailingTest(String commandFilename) throws Exception {
setEppInput(commandFilename);
// 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();
@@ -204,7 +199,6 @@ class DomainTransferCancelFlowTest
@Test
void testDryRun() throws Exception {
setEppInput("domain_transfer_cancel.xml");
eppLoader.replaceAll("JD1234-REP", contact.getRepoId());
dryRunFlowAssertResponse(loadFile("domain_transfer_cancel_response.xml"));
}
@@ -219,19 +213,8 @@ class DomainTransferCancelFlowTest
}
@Test
void testSuccess_contactAuthInfo() throws Exception {
doSuccessfulTest("domain_transfer_cancel_contact_authinfo.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());
void testFailure_contactPassword() {
// Contact passwords cannot be provided because we don't store contacts
EppException thrown =
assertThrows(
BadAuthInfoForResourceException.class,

View File

@@ -20,7 +20,6 @@ import static google.registry.model.reporting.HistoryEntry.Type.DOMAIN_CREATE;
import static google.registry.testing.DatabaseHelper.createBillingEventForTransfer;
import static google.registry.testing.DatabaseHelper.createTld;
import static google.registry.testing.DatabaseHelper.getOnlyHistoryEntryOfType;
import static google.registry.testing.DatabaseHelper.persistActiveContact;
import static google.registry.testing.DatabaseHelper.persistDomainWithDependentResources;
import static google.registry.testing.DatabaseHelper.persistDomainWithPendingTransfer;
import static google.registry.testing.DatabaseHelper.persistResource;
@@ -36,7 +35,6 @@ import google.registry.model.billing.BillingBase.Flag;
import google.registry.model.billing.BillingBase.Reason;
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.eppcommon.StatusValue;
@@ -72,7 +70,6 @@ abstract class DomainTransferFlowTestCase<F extends Flow, R extends EppResource>
static final DateTime EXTENDED_REGISTRATION_EXPIRATION_TIME =
REGISTRATION_EXPIRATION_TIME.plusYears(EXTENDED_REGISTRATION_YEARS);
protected Contact contact;
protected Domain domain;
Host subordinateHost;
private DomainHistory historyEntryDomainCreate;
@@ -104,12 +101,11 @@ abstract class DomainTransferFlowTestCase<F extends Flow, R extends EppResource>
/** 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,
null,
clock.nowUtc(),
DateTime.parse("1999-04-03T22:00:00.0Z"),
REGISTRATION_EXPIRATION_TIME);

View File

@@ -29,7 +29,6 @@ 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.ContactAuthInfo;
import google.registry.model.domain.Domain;
import google.registry.model.domain.DomainAuthInfo;
import google.registry.model.eppcommon.AuthInfo.PasswordAuth;
@@ -52,8 +51,6 @@ class DomainTransferQueryFlowTest
private void doSuccessfulTest(
String commandFilename, String expectedXmlFilename, int numPollMessages) throws Exception {
setEppInput(commandFilename);
// 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(false);
runFlowAssertResponse(loadFile(expectedXmlFilename));
@@ -73,8 +70,6 @@ class DomainTransferQueryFlowTest
private void doFailingTest(String commandFilename) throws Exception {
setEppInput(commandFilename);
// 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(false);
runFlow();
@@ -105,13 +100,6 @@ class DomainTransferQueryFlowTest
"domain_transfer_query_domain_authinfo.xml", "domain_transfer_query_response.xml", 1);
}
@Test
void testSuccess_contactAuthInfo() throws Exception {
setRegistrarIdForFlow("ClientZ");
doSuccessfulTest(
"domain_transfer_query_contact_authinfo.xml", "domain_transfer_query_response.xml", 1);
}
@Test
void testSuccess_clientApproved() throws Exception {
changeTransferStatus(TransferStatus.CLIENT_APPROVED);
@@ -170,14 +158,7 @@ class DomainTransferQueryFlowTest
}
@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());
void testFailure_contactPasswordNotAllowed() {
EppException thrown =
assertThrows(
BadAuthInfoForResourceException.class,

View File

@@ -43,7 +43,6 @@ import google.registry.flows.ResourceFlowUtils.ResourceDoesNotExistException;
import google.registry.flows.ResourceFlowUtils.ResourceNotOwnedException;
import google.registry.flows.domain.DomainFlowUtils.NotAuthorizedForTldException;
import google.registry.flows.exceptions.NotPendingTransferException;
import google.registry.model.contact.ContactAuthInfo;
import google.registry.model.domain.Domain;
import google.registry.model.domain.DomainAuthInfo;
import google.registry.model.domain.DomainHistory;
@@ -78,7 +77,6 @@ class DomainTransferRejectFlowTest
private void doSuccessfulTest(String commandFilename, String expectedXmlFilename)
throws Exception {
setEppInput(commandFilename);
eppLoader.replaceAll("JD1234-REP", contact.getRepoId());
// Make sure the implicit billing event is there; it will be deleted by the flow.
// We also expect to see autorenew events for the gaining and losing registrars.
assertBillingEvents(
@@ -149,8 +147,6 @@ class DomainTransferRejectFlowTest
private void doFailingTest(String commandFilename) throws Exception {
setEppInput(commandFilename);
// 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();
@@ -171,7 +167,6 @@ class DomainTransferRejectFlowTest
@Test
void testDryRun() throws Exception {
setEppInput("domain_transfer_reject.xml");
eppLoader.replaceAll("JD1234-REP", contact.getRepoId());
dryRunFlowAssertResponse(loadFile("domain_transfer_reject_response.xml"));
}
@@ -181,12 +176,6 @@ class DomainTransferRejectFlowTest
"domain_transfer_reject_domain_authinfo.xml", "domain_transfer_reject_response.xml");
}
@Test
void testSuccess_contactAuthInfo() throws Exception {
doSuccessfulTest(
"domain_transfer_reject_contact_authinfo.xml", "domain_transfer_reject_response.xml");
}
@Test
void testFailure_notAuthorizedForTld() {
persistResource(
@@ -209,14 +198,8 @@ class DomainTransferRejectFlowTest
}
@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());
void testFailure_contactPassword() {
// Contact passwords cannot be provided because we don't store contacts
EppException thrown =
assertThrows(
BadAuthInfoForResourceException.class,

View File

@@ -1,897 +0,0 @@
// 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());
}
}

View File

@@ -40,7 +40,6 @@ import static google.registry.testing.DatabaseHelper.loadByEntity;
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.persistResource;
import static google.registry.testing.DomainSubject.assertAboutDomains;
import static google.registry.testing.EppExceptionSubject.assertAboutEppExceptions;
@@ -96,7 +95,6 @@ import google.registry.model.billing.BillingBase.RenewalPriceBehavior;
import google.registry.model.billing.BillingCancellation;
import google.registry.model.billing.BillingEvent;
import google.registry.model.billing.BillingRecurrence;
import google.registry.model.contact.ContactAuthInfo;
import google.registry.model.domain.Domain;
import google.registry.model.domain.DomainAuthInfo;
import google.registry.model.domain.DomainHistory;
@@ -147,6 +145,24 @@ class DomainTransferRequestFlowTest
.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> FEE_STD_1_0_MAP =
updateSubstitutions(BASE_FEE_MAP, "FEE_VERSION", "epp:fee-1.0", "FEE_NS", "fee1_00");
private static final ImmutableMap<String, String> RICH_DOMAIN_MAP =
@@ -452,8 +468,6 @@ class DomainTransferRequestFlowTest
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.
@@ -553,8 +567,6 @@ class DomainTransferRequestFlowTest
eppRequestSource = EppRequestSource.TOOL;
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.
@@ -608,8 +620,6 @@ class DomainTransferRequestFlowTest
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);
@@ -639,7 +649,6 @@ class DomainTransferRequestFlowTest
void testDryRun() throws Exception {
setupDomain("example", "tld");
setEppInput("domain_transfer_request.xml");
eppLoader.replaceAll("JD1234-REP", contact.getRepoId());
dryRunFlowAssertResponse(loadFile("domain_transfer_request_response.xml"));
}
@@ -1166,7 +1175,6 @@ class DomainTransferRequestFlowTest
// This ensures that the transfer has non-premium cost, as otherwise, the fee extension would be
// required to ack the premium price.
setEppInput("domain_transfer_request.xml");
eppLoader.replaceAll("JD1234-REP", contact.getRepoId());
runFlowAssertResponse(loadFile("domain_transfer_request_response.xml"));
domain = loadByEntity(domain);
@@ -1220,7 +1228,6 @@ class DomainTransferRequestFlowTest
DateTime now = clock.nowUtc();
setEppInput("domain_transfer_request.xml");
eppLoader.replaceAll("JD1234-REP", contact.getRepoId());
runFlowAssertResponse(loadFile("domain_transfer_request_response.xml"));
domain = loadByEntity(domain);
@@ -1282,7 +1289,6 @@ class DomainTransferRequestFlowTest
DateTime now = clock.nowUtc();
setEppInput("domain_transfer_request.xml");
eppLoader.replaceAll("JD1234-REP", contact.getRepoId());
runFlowAssertResponse(loadFile("domain_transfer_request_response.xml"));
domain = loadByEntity(domain);
@@ -1343,7 +1349,6 @@ class DomainTransferRequestFlowTest
DateTime now = clock.nowUtc();
setEppInput("domain_transfer_request.xml");
eppLoader.replaceAll("JD1234-REP", contact.getRepoId());
runFlowAssertResponse(loadFile("domain_transfer_request_response.xml"));
domain = loadByEntity(domain);
@@ -1500,36 +1505,6 @@ class DomainTransferRequestFlowTest
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
void testFailure_badContactPassword() {
setupDomain("example", "tld");
// 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("domain_transfer_request.xml"));
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
void testFailure_badContactRepoId() {
setupDomain("example", "tld");
// 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("domain_transfer_request.xml"));
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
void testSuccess_clientApproved() throws Exception {
setupDomain("example", "tld");
@@ -1633,7 +1608,6 @@ class DomainTransferRequestFlowTest
setEppInput(
"domain_transfer_request_wildcard.xml",
ImmutableMap.of("YEARS", "1", "DOMAIN", "--invalid", "EXDATE", "2002-09-08T22:00:00.0Z"));
eppLoader.replaceAll("JD1234-REP", contact.getRepoId());
assertMutatingFlow(true);
ResourceDoesNotExistException thrown =
assertThrows(
@@ -1645,7 +1619,6 @@ class DomainTransferRequestFlowTest
@Test
void testFailure_nonexistentDomain() {
createTld("tld");
contact = persistActiveContact("jd1234");
ResourceDoesNotExistException thrown =
assertThrows(
ResourceDoesNotExistException.class,
@@ -1653,6 +1626,22 @@ class DomainTransferRequestFlowTest
assertThat(thrown).hasMessageThat().contains(String.format("(%s)", "example.tld"));
}
@Test
void testFailure_cannotUseContactAuthInfo() {
// RFC 5731: "An OPTIONAL "roid" attribute MUST be used to identify the registrant or contact
// object if and only if the given authInfo is associated with a registrant or contact object,
// and not the domain object itself."
//
// We have no contacts, so it cannot be valid to specify a roid
setupDomain("example", "tld");
assertAboutEppExceptions()
.that(
assertThrows(
BadAuthInfoForResourceException.class,
() -> doFailingTest("domain_transfer_request_contact_auth_info_failure.xml")))
.marshalsToXml();
}
@Test
void testFailure_periodInMonths() {
setupDomain("example", "tld");
@@ -1826,4 +1815,250 @@ class DomainTransferRequestFlowTest
assertThrows(AlreadyRedeemedAllocationTokenException.class, this::runFlow);
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@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()));
}
}

View File

@@ -14,43 +14,62 @@
package google.registry.flows.domain;
import google.registry.flows.Flow;
import google.registry.flows.ResourceFlowTestCase;
import google.registry.model.domain.Domain;
import google.registry.model.eppcommon.EppXmlTransformer;
import static com.google.common.truth.Truth.assertThat;
import google.registry.model.eppcommon.ProtocolDefinition;
import google.registry.util.RegistryEnvironment;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
/**
* Abstract class for tests that require old versions of the fee extension (0.6, 0.11, 0.12).
*
* <p>These are enabled only in the production environment so in order to test them, we need to
* simulate the production environment for each test (and reset it to the test environment
* afterward).
*/
public abstract class ProductionSimulatingFeeExtensionsTest<F extends Flow>
extends ResourceFlowTestCase<F, Domain> {
/** Class for testing the XML extension definitions loaded in the prod environment. */
public class ProductionSimulatingFeeExtensionsTest {
private RegistryEnvironment previousEnvironment;
@BeforeEach
void beforeEach() {
previousEnvironment = RegistryEnvironment.get();
RegistryEnvironment.PRODUCTION.setup();
reloadServiceExtensionUris();
}
@AfterEach
void afterEach() {
previousEnvironment.setup();
reloadServiceExtensionUris();
ProtocolDefinition.reloadServiceExtensionUris();
}
void reloadServiceExtensionUris() {
@Test
void testNonProdEnvironments() {
for (RegistryEnvironment env : RegistryEnvironment.values()) {
if (env.equals(RegistryEnvironment.PRODUCTION)) {
continue;
}
env.setup();
ProtocolDefinition.reloadServiceExtensionUris();
assertThat(ProtocolDefinition.getVisibleServiceExtensionUris())
.containsExactly(
"urn:ietf:params:xml:ns:launch-1.0",
"urn:ietf:params:xml:ns:rgp-1.0",
"urn:ietf:params:xml:ns:secDNS-1.1",
"urn:ietf:params:xml:ns:fee-0.6",
"urn:ietf:params:xml:ns:fee-0.11",
"urn:ietf:params:xml:ns:fee-0.12",
"urn:ietf:params:xml:ns:epp:fee-1.0");
}
}
@Test
void testProdEnvironment() {
RegistryEnvironment.PRODUCTION.setup();
ProtocolDefinition.reloadServiceExtensionUris();
sessionMetadata.setServiceExtensionUris(ProtocolDefinition.getVisibleServiceExtensionUris());
EppXmlTransformer.reloadTransformers();
// prod shouldn't have the fee extension version 1.0
assertThat(ProtocolDefinition.getVisibleServiceExtensionUris())
.containsExactly(
"urn:ietf:params:xml:ns:launch-1.0",
"urn:ietf:params:xml:ns:rgp-1.0",
"urn:ietf:params:xml:ns:secDNS-1.1",
"urn:ietf:params:xml:ns:fee-0.6",
"urn:ietf:params:xml:ns:fee-0.11",
"urn:ietf:params:xml:ns:fee-0.12");
}
}

View File

@@ -17,7 +17,6 @@ package google.registry.flows.poll;
import static com.google.common.truth.Truth.assertThat;
import static google.registry.testing.DatabaseHelper.createHistoryEntryForEppResource;
import static google.registry.testing.DatabaseHelper.createTld;
import static google.registry.testing.DatabaseHelper.persistActiveContact;
import static google.registry.testing.DatabaseHelper.persistResource;
import static google.registry.util.DateTimeUtils.END_OF_TIME;
import static org.junit.jupiter.api.Assertions.assertThrows;
@@ -28,7 +27,6 @@ import google.registry.flows.poll.PollAckFlow.InvalidMessageIdException;
import google.registry.flows.poll.PollAckFlow.MessageDoesNotExistException;
import google.registry.flows.poll.PollAckFlow.MissingMessageIdException;
import google.registry.flows.poll.PollAckFlow.NotAuthorizedToAckMessageException;
import google.registry.model.contact.Contact;
import google.registry.model.domain.Domain;
import google.registry.model.poll.PollMessage;
import google.registry.testing.DatabaseHelper;
@@ -43,7 +41,6 @@ class PollAckFlowTest extends FlowTestCase<PollAckFlow> {
private static final long MESSAGE_ID = 3;
private Domain domain;
private Contact contact;
@BeforeEach
void setUp() {
@@ -51,8 +48,7 @@ class PollAckFlowTest extends FlowTestCase<PollAckFlow> {
clock.setTo(DateTime.parse("2011-01-02T01:01:01Z"));
setRegistrarIdForFlow("NewRegistrar");
createTld("example");
contact = persistActiveContact("jd1234");
domain = persistResource(DatabaseHelper.newDomain("test.example", contact));
domain = persistResource(DatabaseHelper.newDomain("test.example"));
}
private void persistOneTimePollMessage(long messageId) {
@@ -85,43 +81,6 @@ class PollAckFlowTest extends FlowTestCase<PollAckFlow> {
dryRunFlowAssertResponse(loadFile("poll_ack_response_empty.xml"));
}
@Test
void testSuccess_contactPollMessage() throws Exception {
setEppInput("poll_ack.xml", ImmutableMap.of("MSGID", "3-2011"));
persistResource(
new PollMessage.OneTime.Builder()
.setId(MESSAGE_ID)
.setRegistrarId(getRegistrarIdForFlow())
.setEventTime(clock.nowUtc().minusDays(1))
.setMsg("Some poll message.")
.setHistoryEntry(createHistoryEntryForEppResource(contact))
.build());
assertMutatingFlow(true);
runFlowAssertResponse(loadFile("poll_ack_response_empty.xml"));
}
@Test
void testFailure_contactPollMessage_withIncorrectYearField() throws Exception {
setEppInput("poll_ack.xml", ImmutableMap.of("MSGID", "3-1999"));
persistResource(
new PollMessage.OneTime.Builder()
.setId(MESSAGE_ID)
.setRegistrarId(getRegistrarIdForFlow())
.setEventTime(clock.nowUtc().minusDays(1))
.setMsg("Some poll message.")
.setHistoryEntry(createHistoryEntryForEppResource(contact))
.build());
assertMutatingFlow(true);
assertThrows(MessageDoesNotExistException.class, this::runFlow);
}
@Test
void testSuccess_messageOnContact() throws Exception {
persistOneTimePollMessage(MESSAGE_ID);
assertMutatingFlow(true);
runFlowAssertResponse(loadFile("poll_ack_response_empty.xml"));
}
@Test
void testSuccess_recentActiveAutorenew() throws Exception {
setEppInput("poll_ack.xml", ImmutableMap.of("MSGID", "3-2010"));
@@ -184,21 +143,6 @@ class PollAckFlowTest extends FlowTestCase<PollAckFlow> {
assertThrows(InvalidMessageIdException.class, this::runFlow);
}
@Test
void testFailure_contactPollMessage_withMissingYearField() throws Exception {
setEppInput("poll_ack.xml", ImmutableMap.of("MSGID", "3"));
persistResource(
new PollMessage.OneTime.Builder()
.setId(MESSAGE_ID)
.setRegistrarId(getRegistrarIdForFlow())
.setEventTime(clock.nowUtc().minusDays(1))
.setMsg("Some poll message.")
.setHistoryEntry(createHistoryEntryForEppResource(contact))
.build());
assertMutatingFlow(true);
assertThrows(InvalidMessageIdException.class, this::runFlow);
}
@Test
void testFailure_invalidId_stringInsteadOfNumeric() throws Exception {
setEppInput("poll_ack.xml", ImmutableMap.of("MSGID", "ABC-12345"));

View File

@@ -16,7 +16,6 @@ package google.registry.flows.poll;
import static google.registry.testing.DatabaseHelper.createHistoryEntryForEppResource;
import static google.registry.testing.DatabaseHelper.createTld;
import static google.registry.testing.DatabaseHelper.persistActiveContact;
import static google.registry.testing.DatabaseHelper.persistActiveHost;
import static google.registry.testing.DatabaseHelper.persistNewRegistrar;
import static google.registry.testing.DatabaseHelper.persistResource;
@@ -27,8 +26,6 @@ import com.google.common.collect.ImmutableList;
import google.registry.flows.EppException;
import google.registry.flows.FlowTestCase;
import google.registry.flows.poll.PollRequestFlow.UnexpectedMessageIdException;
import google.registry.model.contact.Contact;
import google.registry.model.contact.ContactHistory;
import google.registry.model.domain.Domain;
import google.registry.model.eppcommon.Trid;
import google.registry.model.host.Host;
@@ -36,7 +33,6 @@ import google.registry.model.host.HostHistory;
import google.registry.model.poll.PendingActionNotificationResponse.DomainPendingActionNotificationResponse;
import google.registry.model.poll.PollMessage;
import google.registry.model.reporting.HistoryEntry;
import google.registry.model.transfer.TransferResponse.ContactTransferResponse;
import google.registry.model.transfer.TransferResponse.DomainTransferResponse;
import google.registry.model.transfer.TransferStatus;
import google.registry.testing.DatabaseHelper;
@@ -48,7 +44,6 @@ import org.junit.jupiter.api.Test;
class PollRequestFlowTest extends FlowTestCase<PollRequestFlow> {
private Domain domain;
private Contact contact;
private Host host;
@BeforeEach
@@ -58,8 +53,7 @@ class PollRequestFlowTest extends FlowTestCase<PollRequestFlow> {
setRegistrarIdForFlow("NewRegistrar");
createTld("example");
persistNewRegistrar("BadRegistrar");
contact = persistActiveContact("jd1234");
domain = persistResource(DatabaseHelper.newDomain("test.example", contact));
domain = persistResource(DatabaseHelper.newDomain("test.example"));
host = persistActiveHost("ns1.test.example");
}
@@ -99,31 +93,6 @@ class PollRequestFlowTest extends FlowTestCase<PollRequestFlow> {
runFlowAssertResponse(loadFile("poll_response_domain_transfer_no_cltrid.xml"));
}
@Test
void testSuccess_contactTransferPending() throws Exception {
setRegistrarIdForFlow("TheRegistrar");
persistResource(
new PollMessage.OneTime.Builder()
.setId(3L)
.setRegistrarId(getRegistrarIdForFlow())
.setEventTime(clock.nowUtc().minusDays(5))
.setMsg("Transfer requested.")
.setResponseData(
ImmutableList.of(
new ContactTransferResponse.Builder()
.setContactId("sh8013")
.setTransferStatus(TransferStatus.PENDING)
.setGainingRegistrarId(getRegistrarIdForFlow())
.setTransferRequestTime(clock.nowUtc().minusDays(5))
.setLosingRegistrarId("NewRegistrar")
.setPendingTransferExpirationTime(clock.nowUtc())
.build()))
.setHistoryEntry(createHistoryEntryForEppResource(contact))
.build());
assertMutatingFlow(false);
runFlowAssertResponse(loadFile("poll_response_contact_transfer.xml"));
}
@Test
void testSuccess_domainPendingActionComplete() throws Exception {
persistResource(
@@ -225,29 +194,6 @@ class PollRequestFlowTest extends FlowTestCase<PollRequestFlow> {
runFlowAssertResponse(loadFile("poll_response_empty.xml"));
}
@Test
void testSuccess_contactDelete() throws Exception {
// Contact delete poll messages do not have any response data, so ensure that no
// response data block is produced in the poll message.
HistoryEntry historyEntry =
persistResource(
new ContactHistory.Builder()
.setRegistrarId("NewRegistrar")
.setModificationTime(clock.nowUtc().minusDays(1))
.setType(HistoryEntry.Type.CONTACT_DELETE)
.setContact(contact)
.build());
persistResource(
new PollMessage.OneTime.Builder()
.setRegistrarId("NewRegistrar")
.setMsg("Deleted contact jd1234")
.setHistoryEntry(historyEntry)
.setEventTime(clock.nowUtc().minusDays(1))
.build());
assertMutatingFlow(false);
runFlowAssertResponse(loadFile("poll_response_contact_delete.xml"));
}
@Test
void testSuccess_hostDelete() throws Exception {
// Host delete poll messages do not have any response data, so ensure that no

View File

@@ -68,7 +68,10 @@ class EppInputTest {
assertThat(loginCommand.options.version).isEqualTo("1.0");
assertThat(loginCommand.options.language).isEqualTo("en");
assertThat(loginCommand.services.objectServices)
.containsExactly("urn:ietf:params:xml:ns:host-1.0", "urn:ietf:params:xml:ns:domain-1.0");
.containsExactly(
"urn:ietf:params:xml:ns:host-1.0",
"urn:ietf:params:xml:ns:domain-1.0",
"urn:ietf:params:xml:ns:contact-1.0");
assertThat(loginCommand.services.serviceExtensions)
.containsExactly("urn:ietf:params:xml:ns:launch-1.0", "urn:ietf:params:xml:ns:rgp-1.0");
}

View File

@@ -18,7 +18,6 @@ import static com.google.common.truth.Truth.assertThat;
import static google.registry.model.poll.PollMessageExternalKeyConverter.makePollMessageExternalId;
import static google.registry.model.poll.PollMessageExternalKeyConverter.parsePollMessageExternalId;
import static google.registry.testing.DatabaseHelper.createTld;
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.persistResource;
@@ -83,23 +82,6 @@ public class PollMessageExternalKeyConverterTest {
assertVKeysEqual(parsePollMessageExternalId("5-2007"), pollMessage.createVKey());
}
@Test
void testSuccess_contact() {
historyEntry =
persistResource(
DatabaseHelper.createHistoryEntryForEppResource(persistActiveContact("tim")));
PollMessage.OneTime pollMessage =
persistResource(
new PollMessage.OneTime.Builder()
.setRegistrarId("TheRegistrar")
.setEventTime(clock.nowUtc())
.setMsg("Test poll message")
.setHistoryEntry(historyEntry)
.build());
assertThat(makePollMessageExternalId(pollMessage)).isEqualTo("7-2007");
assertVKeysEqual(parsePollMessageExternalId("7-2007"), pollMessage.createVKey());
}
@Test
void testSuccess_host() {
historyEntry =

View File

@@ -18,7 +18,6 @@ import static com.google.common.truth.Truth.assertThat;
import static google.registry.model.ImmutableObjectSubject.assertAboutImmutableObjects;
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
import static google.registry.testing.DatabaseHelper.createTld;
import static google.registry.testing.DatabaseHelper.persistActiveContact;
import static google.registry.testing.DatabaseHelper.persistActiveDomain;
import static google.registry.testing.DatabaseHelper.persistResource;
import static java.nio.charset.StandardCharsets.UTF_8;
@@ -26,13 +25,12 @@ import static org.junit.Assert.assertThrows;
import com.google.common.collect.ImmutableSet;
import google.registry.model.EntityTestCase;
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.domain.Period;
import google.registry.model.eppcommon.Trid;
import google.registry.model.reporting.DomainTransactionRecord.TransactionReportField;
import google.registry.model.reporting.HistoryEntry.Type;
import org.joda.time.DateTime;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
@@ -40,14 +38,13 @@ import org.junit.jupiter.api.Test;
/** Unit tests for {@link HistoryEntry}. */
class HistoryEntryTest extends EntityTestCase {
private Domain domain;
private DomainHistory domainHistory;
private Contact contact;
@BeforeEach
void setUp() {
createTld("foobar");
Domain domain = persistActiveDomain("foo.foobar");
contact = persistActiveContact("someone");
domain = persistActiveDomain("foo.foobar");
DomainTransactionRecord transactionRecord =
new DomainTransactionRecord.Builder()
.setTld("foobar")
@@ -91,12 +88,12 @@ class HistoryEntryTest extends EntityTestCase {
assertThrows(
IllegalArgumentException.class,
() ->
new ContactHistory.Builder()
new DomainHistory.Builder()
.setRevisionId(5L)
.setModificationTime(DateTime.parse("1985-07-12T22:30:00Z"))
.setRegistrarId("TheRegistrar")
.setReason("Reason")
.setType(HistoryEntry.Type.CONTACT_CREATE)
.setType(Type.DOMAIN_CREATE)
.build());
assertThat(thrown).hasMessageThat().isEqualTo("EPP resource must be specified");
}
@@ -107,10 +104,10 @@ class HistoryEntryTest extends EntityTestCase {
assertThrows(
IllegalArgumentException.class,
() ->
new ContactHistory.Builder()
.setContact(contact)
new DomainHistory.Builder()
.setRevisionId(5L)
.setModificationTime(DateTime.parse("1985-07-12T22:30:00Z"))
.setDomain(domain)
.setModificationTime(DateTime.parse("1985-07-12T22:30.00Z"))
.setRegistrarId("TheRegistrar")
.setReason("Reason")
.build());
@@ -123,12 +120,12 @@ class HistoryEntryTest extends EntityTestCase {
assertThrows(
IllegalArgumentException.class,
() ->
new ContactHistory.Builder()
.setContact(contact)
new DomainHistory.Builder()
.setRevisionId(5L)
.setType(HistoryEntry.Type.CONTACT_CREATE)
.setDomain(domain)
.setType(Type.DOMAIN_CREATE)
.setRegistrarId("TheRegistrar")
.setReason("Reason")
.setReason("reason")
.build());
assertThat(thrown).hasMessageThat().isEqualTo("Modification time must be specified");
}
@@ -139,11 +136,11 @@ class HistoryEntryTest extends EntityTestCase {
assertThrows(
IllegalArgumentException.class,
() ->
new ContactHistory.Builder()
new DomainHistory.Builder()
.setRevisionId(5L)
.setContact(contact)
.setType(HistoryEntry.Type.CONTACT_CREATE)
.setModificationTime(DateTime.parse("1985-07-12T22:30:00Z"))
.setDomain(domain)
.setType(Type.DOMAIN_CREATE)
.setModificationTime(DateTime.parse("1985-07-12T22:30.00Z"))
.setReason("Reason")
.build());
assertThat(thrown).hasMessageThat().isEqualTo("Registrar ID must be specified");
@@ -155,11 +152,11 @@ class HistoryEntryTest extends EntityTestCase {
assertThrows(
IllegalArgumentException.class,
() ->
new ContactHistory.Builder()
.setContact(contact)
new DomainHistory.Builder()
.setRevisionId(5L)
.setType(HistoryEntry.Type.SYNTHETIC)
.setModificationTime(DateTime.parse("1985-07-12T22:30:00Z"))
.setDomain(domain)
.setType(Type.SYNTHETIC)
.setModificationTime(DateTime.parse("1985-07-12T22:30.00Z"))
.setRegistrarId("TheRegistrar")
.setReason("Reason")
.setRequestedByRegistrar(true)

View File

@@ -0,0 +1,96 @@
// 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.monitoring.whitebox;
import static com.google.common.truth.Truth.assertThat;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.monitoring.metrics.MetricRegistry;
import com.google.monitoring.metrics.MetricRegistryImpl;
import java.lang.management.MemoryMXBean;
import java.lang.management.MemoryUsage;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
/** Unit tests for {@link JvmMetrics}. */
class JvmMetricsTests {
private MetricRegistry registry;
private JvmMetrics jvmMetrics;
private MemoryMXBean mockMemoryMXBean = mock(MemoryMXBean.class);
private static final MemoryUsage HEAP_USAGE =
new MemoryUsage(/*init*/ 100, /*used*/ 200, /*commited*/ 500, /*max*/ 1000);
private static final MemoryUsage NON_HEAP_USAGE =
new MemoryUsage(/*init*/ 50, /*used*/ 100, /*commited*/ 250, /*max*/ 500);
@BeforeEach
public void setUp() {
MetricRegistryImpl.getDefault().unregisterAllMetrics();
registry = MetricRegistryImpl.getDefault();
when(mockMemoryMXBean.getHeapMemoryUsage()).thenReturn(HEAP_USAGE);
when(mockMemoryMXBean.getNonHeapMemoryUsage()).thenReturn(NON_HEAP_USAGE);
jvmMetrics = new JvmMetrics(mockMemoryMXBean);
}
@Test
public void metricsRegistered() {
jvmMetrics.register();
assertThat(registry.getRegisteredMetrics()).hasSize(3);
for (var metric : registry.getRegisteredMetrics()) {
assertThat(metric).isInstanceOf(com.google.monitoring.metrics.VirtualMetric.class);
}
}
@Test
public void testGetUsedMemory() {
jvmMetrics.register();
ImmutableMap<ImmutableList<String>, Long> values = jvmMetrics.getUsedMemory();
assertThat(values)
.containsExactly(
ImmutableList.of("heap"), 200L,
ImmutableList.of("non_heap"), 100L);
}
@Test
public void testGetCommittedMemory() {
jvmMetrics.register();
ImmutableMap<ImmutableList<String>, Long> values = jvmMetrics.getCommittedMemory();
assertThat(values)
.containsExactly(
ImmutableList.of("heap"), 500L,
ImmutableList.of("non_heap"), 250L);
}
@Test
public void testGetMaxMemory() {
jvmMetrics.register();
ImmutableMap<ImmutableList<String>, Long> values = jvmMetrics.getMaxMemory();
assertThat(values)
.containsExactly(
ImmutableList.of("heap"), 1000L,
ImmutableList.of("non_heap"), 500L);
}
}

View File

@@ -0,0 +1,236 @@
// 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 static com.google.common.truth.Truth.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import com.google.api.services.monitoring.v3.Monitoring;
import com.google.api.services.monitoring.v3.model.CreateTimeSeriesRequest;
import com.google.api.services.monitoring.v3.model.MetricDescriptor;
import com.google.api.services.monitoring.v3.model.TimeSeries;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import google.registry.mosapi.MosApiModels.ServiceStatus;
import google.registry.mosapi.MosApiModels.TldServiceState;
import google.registry.request.lock.LockHandler;
import google.registry.testing.FakeClock;
import java.io.IOException;
import java.util.List;
import java.util.concurrent.Callable;
import org.joda.time.DateTime;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;
/** Unit tests for {@link MosApiMetrics}. */
public class MosApiMetricsTest {
private static final String PROJECT_ID = "domain-registry-test";
private final Monitoring monitoringClient = mock(Monitoring.class);
private final Monitoring.Projects projects = mock(Monitoring.Projects.class);
private final LockHandler lockHandler = mock(LockHandler.class);
private final Monitoring.Projects.TimeSeries timeSeriesResource =
mock(Monitoring.Projects.TimeSeries.class);
private final Monitoring.Projects.TimeSeries.Create createRequest =
mock(Monitoring.Projects.TimeSeries.Create.class);
// Mocks for Metric Descriptors
private final Monitoring.Projects.MetricDescriptors metricDescriptorsResource =
mock(Monitoring.Projects.MetricDescriptors.class);
private final Monitoring.Projects.MetricDescriptors.Create createDescriptorRequest =
mock(Monitoring.Projects.MetricDescriptors.Create.class);
// Fixed Clock for deterministic testing
private final FakeClock clock = new FakeClock(DateTime.parse("2026-01-01T12:00:00Z"));
private MosApiMetrics mosApiMetrics;
@BeforeEach
void setUp() throws IOException, NoSuchFieldException, IllegalAccessException {
MosApiMetrics.isDescriptorInitialized.set(false);
when(monitoringClient.projects()).thenReturn(projects);
when(projects.timeSeries()).thenReturn(timeSeriesResource);
when(timeSeriesResource.create(anyString(), any(CreateTimeSeriesRequest.class)))
.thenReturn(createRequest);
// Setup for Metric Descriptors
when(projects.metricDescriptors()).thenReturn(metricDescriptorsResource);
when(metricDescriptorsResource.create(anyString(), any(MetricDescriptor.class)))
.thenReturn(createDescriptorRequest);
when(lockHandler.executeWithLocks(any(Callable.class), any(), any(), any()))
.thenAnswer(
invocation -> {
((Callable<?>) invocation.getArgument(0)).call();
return true;
});
mosApiMetrics = new MosApiMetrics(monitoringClient, PROJECT_ID, clock, lockHandler);
}
@Test
void testRecordStates_lazilyInitializesMetricDescriptors() throws IOException {
TldServiceState state = createTldState("test.tld", "UP", "UP");
mosApiMetrics.recordStates(ImmutableList.of(state));
ArgumentCaptor<MetricDescriptor> captor = ArgumentCaptor.forClass(MetricDescriptor.class);
verify(metricDescriptorsResource, times(3))
.create(eq("projects/" + PROJECT_ID), captor.capture());
List<MetricDescriptor> descriptors = captor.getAllValues();
// Verify TLD Status Descriptor
MetricDescriptor tldStatus =
descriptors.stream()
.filter(d -> d.getType().endsWith("tld_status"))
.findFirst()
.orElseThrow();
assertThat(tldStatus.getMetricKind()).isEqualTo("GAUGE");
assertThat(tldStatus.getValueType()).isEqualTo("INT64");
// Verify Service Status Descriptor
MetricDescriptor serviceStatus =
descriptors.stream()
.filter(d -> d.getType().endsWith("service_status"))
.findFirst()
.orElseThrow();
assertThat(serviceStatus.getMetricKind()).isEqualTo("GAUGE");
assertThat(serviceStatus.getValueType()).isEqualTo("INT64");
// Verify Emergency Usage Descriptor
MetricDescriptor emergencyUsage =
descriptors.stream()
.filter(d -> d.getType().endsWith("emergency_usage"))
.findFirst()
.orElseThrow();
assertThat(emergencyUsage.getMetricKind()).isEqualTo("GAUGE");
assertThat(emergencyUsage.getValueType()).isEqualTo("DOUBLE");
}
@Test
void testRecordStates_mapsStatusesToCorrectValues() throws IOException {
TldServiceState stateUp = createTldState("tld-up", "UP", "UP");
TldServiceState stateDown = createTldState("tld-down", "DOWN", "DOWN");
TldServiceState stateMaint = createTldState("tld-maint", "UP-INCONCLUSIVE", "DISABLED");
mosApiMetrics.recordStates(ImmutableList.of(stateUp, stateDown, stateMaint));
ArgumentCaptor<CreateTimeSeriesRequest> captor =
ArgumentCaptor.forClass(CreateTimeSeriesRequest.class);
verify(timeSeriesResource).create(eq("projects/" + PROJECT_ID), captor.capture());
List<TimeSeries> pushedSeries = captor.getValue().getTimeSeries();
// Verify TLD Status Mappings: 1 (UP), 0 (DOWN), 2 (UP-INCONCLUSIVE)
assertThat(getValueFor(pushedSeries, "tld-up", "tld_status")).isEqualTo(1);
assertThat(getValueFor(pushedSeries, "tld-down", "tld_status")).isEqualTo(0);
assertThat(getValueFor(pushedSeries, "tld-maint", "tld_status")).isEqualTo(2);
// Verify Service Status Mappings: UP -> 1, DOWN -> 0, DISABLED -> 2
assertThat(getValueFor(pushedSeries, "tld-up", "service_status")).isEqualTo(1);
assertThat(getValueFor(pushedSeries, "tld-down", "service_status")).isEqualTo(0);
assertThat(getValueFor(pushedSeries, "tld-maint", "service_status")).isEqualTo(2);
// 3. Verify Emergency Usage (DOUBLE)
assertThat(getValueFor(pushedSeries, "tld-up", "emergency_usage").doubleValue())
.isEqualTo(50.0);
assertThat(getValueFor(pushedSeries, "tld-down", "emergency_usage").doubleValue())
.isEqualTo(50.0);
assertThat(getValueFor(pushedSeries, "tld-maint", "emergency_usage").doubleValue())
.isEqualTo(50.0);
}
@Test
void testRecordStates_partitionsTimeSeries_atLimit() throws IOException {
ImmutableList<TldServiceState> largeBatch =
java.util.stream.IntStream.range(0, 70)
.mapToObj(i -> createTldState("tld-" + i, "UP", "UP"))
.collect(ImmutableList.toImmutableList());
mosApiMetrics.recordStates(largeBatch);
verify(timeSeriesResource, times(2))
.create(eq("projects/" + PROJECT_ID), any(CreateTimeSeriesRequest.class));
}
@Test
void testMetricStructure_containsExpectedLabelsAndResource() throws IOException {
TldServiceState state = createTldState("example.tld", "UP", "UP");
mosApiMetrics.recordStates(ImmutableList.of(state));
ArgumentCaptor<CreateTimeSeriesRequest> captor =
ArgumentCaptor.forClass(CreateTimeSeriesRequest.class);
verify(timeSeriesResource).create(anyString(), captor.capture());
TimeSeries ts = captor.getValue().getTimeSeries().get(0);
assertThat(ts.getMetric().getType()).startsWith("custom.googleapis.com/mosapi/");
assertThat(ts.getMetric().getLabels()).containsEntry("tld", "example.tld");
assertThat(ts.getResource().getType()).isEqualTo("global");
assertThat(ts.getResource().getLabels()).containsEntry("project_id", PROJECT_ID);
// Verify that the interval matches our fixed clock
assertThat(ts.getPoints().get(0).getInterval().getEndTime()).isEqualTo("2026-01-01T12:00:00Z");
}
/** Extracts the numeric value for a specific TLD and metric type from a list of TimeSeries. */
private Number getValueFor(List<TimeSeries> seriesList, String tld, String metricSuffix) {
String fullMetric = "custom.googleapis.com/mosapi/" + metricSuffix;
return seriesList.stream()
.filter(ts -> tld.equals(ts.getMetric().getLabels().get("tld")))
.filter(ts -> ts.getMetric().getType().equals(fullMetric))
.findFirst()
.map(
ts -> {
Double dVal = ts.getPoints().get(0).getValue().getDoubleValue();
if (dVal != null) {
return (Number) dVal;
}
// Fallback to Int64.
return (Number) ts.getPoints().get(0).getValue().getInt64Value();
})
.get();
}
@Test
void testRecordStates_skipsInitialization_ifLockNotAcquired() throws IOException {
when(lockHandler.executeWithLocks(any(Callable.class), any(), any(), any())).thenReturn(false);
TldServiceState state = createTldState("test.tld", "UP", "UP");
mosApiMetrics.recordStates(ImmutableList.of(state));
verify(metricDescriptorsResource, never()).create(anyString(), any());
}
/** Mocks a TldServiceState with a single service status. */
private TldServiceState createTldState(String tld, String tldStatus, String serviceStatus) {
ServiceStatus sStatus = mock(ServiceStatus.class);
when(sStatus.status()).thenReturn(serviceStatus);
when(sStatus.emergencyThreshold()).thenReturn(50.0);
TldServiceState state = mock(TldServiceState.class);
when(state.tld()).thenReturn(tld);
when(state.status()).thenReturn(tldStatus);
when(state.serviceStatuses()).thenReturn(ImmutableMap.of("dns", sStatus));
return state;
}
}

View File

@@ -169,4 +169,23 @@ class MosApiStateServiceTest {
&& states.stream()
.anyMatch(s -> s.tld().equals("tld1") && s.status().equals("Up"))));
}
@Test
void testTriggerMetrics_filtersOutInvalidContractStates() throws Exception {
// 1. Valid State
TldServiceState validState = new TldServiceState("tld1", 1L, "Up", ImmutableMap.of());
// 2. Invalid State (Status is NULL)
// We instantiate it directly to simulate a bad response object.
TldServiceState invalidState = new TldServiceState("tld2", 2L, null, ImmutableMap.of());
when(client.getTldServiceState("tld1")).thenReturn(validState);
when(client.getTldServiceState("tld2")).thenReturn(invalidState);
service.triggerMetricsForAllServiceStateSummaries();
// Verify: Only the valid state (tld1) is passed to recordStates
verify(metrics)
.recordStates(argThat(states -> states.size() == 1 && states.get(0).tld().equals("tld1")));
}
}

View File

@@ -28,17 +28,12 @@ import static google.registry.xjc.rgp.XjcRgpStatusValueType.TRANSFER_PERIOD;
import static java.nio.charset.StandardCharsets.UTF_8;
import static org.joda.money.CurrencyUnit.USD;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.common.net.InetAddresses;
import google.registry.model.billing.BillingBase.Flag;
import google.registry.model.billing.BillingBase.Reason;
import google.registry.model.billing.BillingEvent;
import google.registry.model.billing.BillingRecurrence;
import google.registry.model.contact.Contact;
import google.registry.model.contact.ContactAddress;
import google.registry.model.contact.ContactPhoneNumber;
import google.registry.model.contact.PostalInfo;
import google.registry.model.domain.DesignatedContact;
import google.registry.model.domain.Domain;
import google.registry.model.domain.DomainAuthInfo;
@@ -73,7 +68,6 @@ import google.registry.xjc.rgp.XjcRgpStatusType;
import google.registry.xjc.secdns.XjcSecdnsDsDataType;
import google.registry.xml.XmlException;
import java.io.ByteArrayOutputStream;
import java.util.Optional;
import org.joda.money.Money;
import org.joda.time.DateTime;
import org.junit.jupiter.api.BeforeEach;
@@ -109,7 +103,7 @@ public class DomainToXjcConverterTest {
assertThat(
bean.getContacts().stream()
.map(input -> String.format("%s %s", input.getType().toString(), input.getValue())))
.containsExactly("ADMIN 5372808-IRL", "TECH 5372808-TRL");
.containsExactly("ADMIN contact1234", "TECH contact1234");
assertThat(bean.getCrDate()).isEqualTo(DateTime.parse("1900-01-01T00:00:00Z"));
@@ -138,7 +132,7 @@ public class DomainToXjcConverterTest {
// name used to generate the IDN variant.
// TODO(b/26125498): bean.getOriginalName()
assertThat(bean.getRegistrant()).isEqualTo("5372808-ERL");
assertThat(bean.getRegistrant()).isEqualTo("contact1234");
// o Zero or more OPTIONAL <rgpStatus> element to represent
// "pendingDelete" sub-statuses, including "redemptionPeriod",
@@ -264,26 +258,6 @@ public class DomainToXjcConverterTest {
domain
.asBuilder()
.setAuthInfo(DomainAuthInfo.create(PasswordAuth.create("secret")))
.setContacts(
ImmutableSet.of(
DesignatedContact.create(
DesignatedContact.Type.ADMIN,
makeContact(
clock,
"10-Q9JYB4C",
"5372808-IRL",
"be that word our sign in parting",
"BOFH@cat.みんな")
.createVKey()),
DesignatedContact.create(
DesignatedContact.Type.TECH,
makeContact(
clock,
"11-Q9JYB4C",
"5372808-TRL",
"bird or fiend!? i shrieked upstarting",
"bog@cat.みんな")
.createVKey())))
.setCreationRegistrarId("TheRegistrar")
.setCreationTimeForTest(DateTime.parse("1900-01-01T00:00:00Z"))
.setPersistedCurrentSponsorRegistrarId("TheRegistrar")
@@ -298,15 +272,6 @@ public class DomainToXjcConverterTest {
makeHost(clock, "3-Q9JYB4C", "bird.or.devil.みんな", "1.2.3.4").createVKey(),
makeHost(clock, "4-Q9JYB4C", "ns2.cat.みんな", "bad:f00d:cafe::15:beef")
.createVKey()))
.setRegistrant(
Optional.of(
makeContact(
clock,
"12-Q9JYB4C",
"5372808-ERL",
"(◕‿◕) nevermore",
"prophet@evil.みんな")
.createVKey()))
.setRegistrationExpirationTime(DateTime.parse("1930-01-01T00:00:00Z"))
.setGracePeriods(
ImmutableSet.of(
@@ -404,37 +369,6 @@ public class DomainToXjcConverterTest {
return persistResource(domain);
}
private static Contact makeContact(
FakeClock clock, String repoId, String id, String name, String email) {
clock.advanceOneMilli();
return persistEppResource(
new Contact.Builder()
.setContactId(id)
.setEmailAddress(email)
.setPersistedCurrentSponsorRegistrarId("TheRegistrar")
.setCreationRegistrarId("TheRegistrar")
.setCreationTimeForTest(END_OF_TIME)
.setInternationalizedPostalInfo(
new PostalInfo.Builder()
.setType(PostalInfo.Type.INTERNATIONALIZED)
.setName(name)
.setOrg("SINNERS INCORPORATED")
.setAddress(
new ContactAddress.Builder()
.setStreet(ImmutableList.of("123 Example Boulevard"))
.setCity("KOKOMO")
.setState("BM")
.setZip("31337")
.setCountryCode("US")
.build())
.build())
.setRepoId(repoId)
.setVoiceNumber(
new ContactPhoneNumber.Builder().setPhoneNumber("+1.2126660420").build())
.setFaxNumber(new ContactPhoneNumber.Builder().setPhoneNumber("+1.2126660421").build())
.build());
}
private static Host makeHost(FakeClock clock, String repoId, String fqhn, String ip) {
clock.advanceOneMilli();
return persistEppResource(

View File

@@ -25,7 +25,6 @@ import static com.google.common.collect.MoreCollectors.onlyElement;
import static com.google.common.truth.Truth.assertThat;
import static com.google.common.truth.Truth.assertWithMessage;
import static google.registry.config.RegistryConfig.getContactAndHostRoidSuffix;
import static google.registry.config.RegistryConfig.getContactAutomaticTransferLength;
import static google.registry.model.EppResourceUtils.createDomainRepoId;
import static google.registry.model.EppResourceUtils.createRepoId;
import static google.registry.model.ImmutableObjectSubject.assertAboutImmutableObjects;
@@ -76,7 +75,6 @@ import google.registry.model.console.User;
import google.registry.model.console.UserRoles;
import google.registry.model.contact.Contact;
import google.registry.model.contact.ContactAuthInfo;
import google.registry.model.contact.ContactHistory;
import google.registry.model.domain.DesignatedContact;
import google.registry.model.domain.DesignatedContact.Type;
import google.registry.model.domain.Domain;
@@ -536,53 +534,6 @@ public final class DatabaseHelper {
.build();
}
public static Contact persistContactWithPendingTransfer(
Contact contact, DateTime requestTime, DateTime expirationTime, DateTime now) {
ContactHistory historyEntryContactTransfer =
persistResource(
new ContactHistory.Builder()
.setType(HistoryEntry.Type.CONTACT_TRANSFER_REQUEST)
.setContact(persistResource(contact))
.setModificationTime(now)
.setRegistrarId(contact.getCurrentSponsorRegistrarId())
.build());
return persistResource(
contact
.asBuilder()
.setPersistedCurrentSponsorRegistrarId("TheRegistrar")
.addStatusValue(StatusValue.PENDING_TRANSFER)
.setTransferData(
createContactTransferDataBuilder(requestTime, expirationTime)
.setPendingTransferExpirationTime(now.plus(getContactAutomaticTransferLength()))
.setServerApproveEntities(
historyEntryContactTransfer.getRepoId(),
historyEntryContactTransfer.getRevisionId(),
ImmutableSet.of(
// Pretend it's 3 days since the request
persistResource(
createPollMessageForImplicitTransfer(
contact,
historyEntryContactTransfer,
"NewRegistrar",
requestTime,
expirationTime,
null))
.createVKey(),
persistResource(
createPollMessageForImplicitTransfer(
contact,
historyEntryContactTransfer,
"TheRegistrar",
requestTime,
expirationTime,
null))
.createVKey()))
.setTransferRequestTrid(
Trid.create("transferClient-trid", "transferServer-trid"))
.build())
.build());
}
public static Domain persistDomainWithDependentResources(
String label,
String tld,

View File

@@ -6,11 +6,8 @@
</domain:check>
</check>
<extension>
<fee:check xmlns:fee="urn:ietf:params:xml:ns:fee-0.6">
<fee:domain>
<fee:name>rich.example</fee:name>
<fee:command>transfer</fee:command>
</fee:domain>
<fee:check xmlns:fee="urn:ietf:params:xml:ns:epp:fee-1.0">
<fee:command name="transfer"/>
</fee:check>
</extension>
<clTRID>ABC-12345</clTRID>

View File

@@ -0,0 +1,60 @@
<epp xmlns="urn:ietf:params:xml:ns:epp-1.0">
<response>
<result code="1000">
<msg>Command completed successfully</msg>
</result>
<resData>
<domain:chkData xmlns:domain="urn:ietf:params:xml:ns:domain-1.0">
<domain:cd>
<domain:name avail="0">rich.example</domain:name>
<domain:reason>In use</domain:reason>
</domain:cd>
</domain:chkData>
</resData>
<extension>
<fee:chkData xmlns:fee="urn:ietf:params:xml:ns:epp:fee-1.0">
<fee:currency>USD</fee:currency>
<fee:cd>
<fee:objID>rich.example</fee:objID>
<fee:class>premium</fee:class>
<fee:command name="create">
<fee:period unit="y">1</fee:period>
<fee:fee description="create">100.00</fee:fee>
</fee:command>
</fee:cd>
<fee:cd>
<fee:objID>rich.example</fee:objID>
<fee:command name="renew">
<fee:period unit="y">1</fee:period>
<fee:fee description="renew">%RENEWPRICE%</fee:fee>
</fee:command>
</fee:cd>
<fee:cd>
<fee:objID>rich.example</fee:objID>
<fee:command name="transfer">
<fee:period unit="y">1</fee:period>
<fee:fee description="renew">%RENEWPRICE%</fee:fee>
</fee:command>
</fee:cd>
<fee:cd>
<fee:objID>rich.example</fee:objID>
<fee:command name="restore">
<fee:period unit="y">1</fee:period>
<fee:fee description="restore">17.00</fee:fee>
</fee:command>
</fee:cd>
<fee:cd>
<fee:objID>rich.example</fee:objID>
<fee:command name="update">
<fee:period unit="y">1</fee:period>
<fee:fee description="update">0.00</fee:fee>
</fee:command>
</fee:cd>
</fee:chkData>
</extension>
<trID>
<clTRID>ABC-12345</clTRID>
<svTRID>server-trid</svTRID>
</trID>
</response>
</epp>

View File

@@ -12,13 +12,14 @@
</domain:chkData>
</resData>
<extension>
<fee:chkData xmlns:fee="urn:ietf:params:xml:ns:fee-0.6">
<fee:cd xmlns:fee="urn:ietf:params:xml:ns:fee-0.6">
<fee:name>rich.example</fee:name>
<fee:currency>USD</fee:currency>
<fee:command>renew</fee:command>
<fee:period unit="y">1</fee:period>
<fee:fee description="renew">%RENEWPRICE%</fee:fee>
<fee:chkData xmlns:fee="urn:ietf:params:xml:ns:epp:fee-1.0">
<fee:currency>USD</fee:currency>
<fee:cd>
<fee:objID>rich.example</fee:objID>
<fee:command name="renew">
<fee:period unit="y">1</fee:period>
<fee:fee description="renew">%RENEWPRICE%</fee:fee>
</fee:command>
</fee:cd>
</fee:chkData>
</extension>

View File

@@ -12,13 +12,14 @@
</domain:chkData>
</resData>
<extension>
<fee:chkData xmlns:fee="urn:ietf:params:xml:ns:fee-0.6">
<fee:cd xmlns:fee="urn:ietf:params:xml:ns:fee-0.6">
<fee:name>rich.example</fee:name>
<fee:currency>USD</fee:currency>
<fee:command>transfer</fee:command>
<fee:period unit="y">1</fee:period>
<fee:fee description="renew">%RENEWPRICE%</fee:fee>
<fee:chkData xmlns:fee="urn:ietf:params:xml:ns:epp:fee-1.0">
<fee:currency>USD</fee:currency>
<fee:cd>
<fee:objID>rich.example</fee:objID>
<fee:command name="transfer">
<fee:period unit="y">1</fee:period>
<fee:fee description="renew">%RENEWPRICE%</fee:fee>
</fee:command>
</fee:cd>
</fee:chkData>
</extension>

View File

@@ -1,52 +0,0 @@
<epp xmlns="urn:ietf:params:xml:ns:epp-1.0">
<response>
<result code="1000">
<msg>Command completed successfully</msg>
</result>
<resData>
<domain:chkData xmlns:domain="urn:ietf:params:xml:ns:domain-1.0">
<domain:cd>
<domain:name avail="0">rich.example</domain:name>
<domain:reason>In use</domain:reason>
</domain:cd>
</domain:chkData>
</resData>
<extension>
<fee:chkData xmlns:fee="urn:ietf:params:xml:ns:fee-0.6">
<fee:cd xmlns:fee="urn:ietf:params:xml:ns:fee-0.6">
<fee:name>rich.example</fee:name>
<fee:currency>USD</fee:currency>
<fee:command>create</fee:command>
<fee:period unit="y">1</fee:period>
<fee:fee description="create">100.00</fee:fee>
<fee:class>premium</fee:class>
</fee:cd>
<fee:cd xmlns:fee="urn:ietf:params:xml:ns:fee-0.6">
<fee:name>rich.example</fee:name>
<fee:currency>USD</fee:currency>
<fee:command>renew</fee:command>
<fee:period unit="y">1</fee:period>
<fee:fee description="renew">%RENEWPRICE%</fee:fee>
</fee:cd>
<fee:cd xmlns:fee="urn:ietf:params:xml:ns:fee-0.6">
<fee:name>rich.example</fee:name>
<fee:currency>USD</fee:currency>
<fee:command>transfer</fee:command>
<fee:period unit="y">1</fee:period>
<fee:fee description="renew">%RENEWPRICE%</fee:fee>
</fee:cd>
<fee:cd xmlns:fee="urn:ietf:params:xml:ns:fee-0.6">
<fee:name>rich.example</fee:name>
<fee:currency>USD</fee:currency>
<fee:command>restore</fee:command>
<fee:period unit="y">1</fee:period>
<fee:fee description="restore">17.00</fee:fee>
</fee:cd>
</fee:chkData>
</extension>
<trID>
<clTRID>ABC-12345</clTRID>
<svTRID>server-trid</svTRID>
</trID>
</response>
</epp>

View File

@@ -1,20 +0,0 @@
<epp xmlns="urn:ietf:params:xml:ns:epp-1.0">
<command>
<create>
<domain:create
xmlns:domain="urn:ietf:params:xml:ns:domain-1.0">
<domain:name>example.tld</domain:name>
<domain:period unit="y">2</domain:period>
<domain:ns>
<domain:hostObj>ns1.example.net</domain:hostObj>
<domain:hostObj>ns2.example.net</domain:hostObj>
</domain:ns>
<domain:contact>sh8013</domain:contact>
<domain:authInfo>
<domain:pw>2fooBAR</domain:pw>
</domain:authInfo>
</domain:create>
</create>
<clTRID>ABC-12345</clTRID>
</command>
</epp>

View File

@@ -6,7 +6,7 @@
<domain:name>example.tld</domain:name>
<domain:period unit="y">1</domain:period>
<domain:authInfo>
<domain:pw roid="JD1234-REP">2fooBAR</domain:pw>
<domain:pw>fooBAR</domain:pw>
</domain:authInfo>
</domain:transfer>
</transfer>

View File

@@ -6,7 +6,7 @@
<domain:name>example.tld</domain:name>
<domain:period unit="y">1</domain:period>
<domain:authInfo>
<domain:pw roid="JD1234-REP">2fooBAR</domain:pw>
<domain:pw>fooBAR</domain:pw>
</domain:authInfo>
</domain:transfer>
</transfer>

View File

@@ -0,0 +1,15 @@
<epp xmlns="urn:ietf:params:xml:ns:epp-1.0">
<command>
<transfer op="request">
<domain:transfer
xmlns:domain="urn:ietf:params:xml:ns:domain-1.0">
<domain:name>example.tld</domain:name>
<domain:period unit="y">1</domain:period>
<domain:authInfo>
<domain:pw roid="2-ROID">2fooBAR</domain:pw>
</domain:authInfo>
</domain:transfer>
</transfer>
<clTRID>ABC-12345</clTRID>
</command>
</epp>

View File

@@ -6,7 +6,7 @@
<domain:name>%DOMAIN%</domain:name>
<domain:period unit="y">%YEARS%</domain:period>
<domain:authInfo>
<domain:pw roid="JD1234-REP">2fooBAR</domain:pw>
<domain:pw>fooBAR</domain:pw>
</domain:authInfo>
</domain:transfer>
</transfer>

View File

@@ -6,7 +6,7 @@
<domain:name>example.tld</domain:name>
<domain:period unit="y">1</domain:period>
<domain:authInfo>
<domain:pw roid="JD1234-REP">2fooBAR</domain:pw>
<domain:pw>fooBAR</domain:pw>
</domain:authInfo>
</domain:transfer>
</transfer>

View File

@@ -6,7 +6,7 @@
<domain:name>example.tld</domain:name>
<domain:period unit="y">1</domain:period>
<domain:authInfo>
<domain:pw roid="JD1234-REP">2fooBAR</domain:pw>
<domain:pw>fooBAR</domain:pw>
</domain:authInfo>
</domain:transfer>
</transfer>

View File

@@ -6,7 +6,7 @@
<domain:name>example.tld</domain:name>
<domain:period unit="y">1</domain:period>
<domain:authInfo>
<domain:pw roid="JD1234-REP">2fooBAR</domain:pw>
<domain:pw>fooBAR</domain:pw>
</domain:authInfo>
</domain:transfer>
</transfer>

View File

@@ -6,7 +6,7 @@
<domain:name>example.tld</domain:name>
<domain:period unit="y">1</domain:period>
<domain:authInfo>
<domain:pw roid="JD1234-REP">2fooBAR</domain:pw>
<domain:pw>fooBAR</domain:pw>
</domain:authInfo>
</domain:transfer>
</transfer>

View File

@@ -6,7 +6,7 @@
<domain:name>example.tld</domain:name>
<domain:period unit="y">1</domain:period>
<domain:authInfo>
<domain:pw roid="JD1234-REP">2fooBAR</domain:pw>
<domain:pw>fooBAR</domain:pw>
</domain:authInfo>
</domain:transfer>
</transfer>

View File

@@ -5,7 +5,7 @@
xmlns:domain="urn:ietf:params:xml:ns:domain-1.0">
<domain:name>example.tld</domain:name>
<domain:authInfo>
<domain:pw roid="JD1234-REP">2fooBAR</domain:pw>
<domain:pw>fooBAR</domain:pw>
</domain:authInfo>
</domain:transfer>
</transfer>

View File

@@ -6,7 +6,7 @@
<domain:name>example.tld</domain:name>
<domain:period unit="m">1</domain:period>
<domain:authInfo>
<domain:pw roid="JD1234-REP">2fooBAR</domain:pw>
<domain:pw>fooBAR</domain:pw>
</domain:authInfo>
</domain:transfer>
</transfer>

View File

@@ -6,7 +6,7 @@
<domain:name>rich.example</domain:name>
<domain:period unit="y">1</domain:period>
<domain:authInfo>
<domain:pw roid="JD1234-REP">2fooBAR</domain:pw>
<domain:pw>fooBAR</domain:pw>
</domain:authInfo>
</domain:transfer>
</transfer>

View File

@@ -6,7 +6,7 @@
<domain:name>%DOMAIN%</domain:name>
<domain:period unit="y">%YEARS%</domain:period>
<domain:authInfo>
<domain:pw roid="JD1234-REP">2fooBAR</domain:pw>
<domain:pw>fooBAR</domain:pw>
</domain:authInfo>
</domain:transfer>
</transfer>

View File

@@ -6,11 +6,15 @@
<version>1.0</version>
<lang>en</lang>
<objURI>urn:ietf:params:xml:ns:host-1.0</objURI>
<objURI>urn:ietf:params:xml:ns:contact-1.0</objURI>
<objURI>urn:ietf:params:xml:ns:domain-1.0</objURI>
<svcExtension>
<extURI>urn:ietf:params:xml:ns:launch-1.0</extURI>
<extURI>urn:ietf:params:xml:ns:rgp-1.0</extURI>
<extURI>urn:ietf:params:xml:ns:secDNS-1.1</extURI>
<extURI>urn:ietf:params:xml:ns:fee-0.6</extURI>
<extURI>urn:ietf:params:xml:ns:fee-0.11</extURI>
<extURI>urn:ietf:params:xml:ns:fee-0.12</extURI>
<extURI>urn:ietf:params:xml:ns:epp:fee-1.0</extURI>
</svcExtension>
</svcMenu>

View File

@@ -10,6 +10,7 @@
<svcs>
<objURI>urn:ietf:params:xml:ns:host-1.0</objURI>
<objURI>urn:ietf:params:xml:ns:domain-1.0</objURI>
<objURI>urn:ietf:params:xml:ns:contact-1.0</objURI>
<svcExtension>
<extURI>urn:ietf:params:xml:ns:launch-1.0</extURI>
<extURI>urn:ietf:params:xml:ns:rgp-1.0</extURI>

View File

@@ -11,6 +11,7 @@
<svcs>
<objURI>urn:ietf:params:xml:ns:host-1.0</objURI>
<objURI>urn:ietf:params:xml:ns:domain-1.0</objURI>
<objURI>urn:ietf:params:xml:ns:contact-1.0</objURI>
<svcExtension>
<extURI>urn:ietf:params:xml:ns:launch-1.0</extURI>
<extURI>urn:ietf:params:xml:ns:rgp-1.0</extURI>

View File

@@ -10,6 +10,7 @@
<svcs>
<objURI>urn:ietf:params:xml:ns:host-1.0</objURI>
<objURI>urn:ietf:params:xml:ns:domain-1.0</objURI>
<objURI>urn:ietf:params:xml:ns:contact-1.0</objURI>
<svcExtension>
<extURI>urn:ietf:params:xml:ns:launch-1.0</extURI>
<extURI>urn:ietf:params:xml:ns:rgp-1.0</extURI>

View File

@@ -11,6 +11,7 @@
<svcs>
<objURI>urn:ietf:params:xml:ns:host-1.0</objURI>
<objURI>urn:ietf:params:xml:ns:domain-1.0</objURI>
<objURI>urn:ietf:params:xml:ns:contact-1.0</objURI>
<svcExtension>
<extURI>urn:ietf:params:xml:ns:launch-1.0</extURI>
<extURI>urn:ietf:params:xml:ns:rgp-1.0</extURI>

View File

@@ -6,11 +6,15 @@
<version>1.0</version>
<lang>en</lang>
<objURI>urn:ietf:params:xml:ns:host-1.0</objURI>
<objURI>urn:ietf:params:xml:ns:contact-1.0</objURI>
<objURI>urn:ietf:params:xml:ns:domain-1.0</objURI>
<svcExtension>
<extURI>urn:ietf:params:xml:ns:launch-1.0</extURI>
<extURI>urn:ietf:params:xml:ns:rgp-1.0</extURI>
<extURI>urn:ietf:params:xml:ns:secDNS-1.1</extURI>
<extURI>urn:ietf:params:xml:ns:fee-0.6</extURI>
<extURI>urn:ietf:params:xml:ns:fee-0.11</extURI>
<extURI>urn:ietf:params:xml:ns:fee-0.12</extURI>
<extURI>urn:ietf:params:xml:ns:epp:fee-1.0</extURI>
</svcExtension>
</svcMenu>

View File

@@ -10,6 +10,7 @@
<svcs>
<objURI>urn:ietf:params:xml:ns:host-1.0</objURI>
<objURI>urn:ietf:params:xml:ns:domain-1.0</objURI>
<objURI>urn:ietf:params:xml:ns:contact-1.0</objURI>
<svcExtension>
<extURI>http://custom/obj1ext-1.0</extURI>
</svcExtension>

View File

@@ -10,6 +10,7 @@
<svcs>
<objURI>urn:ietf:params:xml:ns:host-1.0</objURI>
<objURI>urn:ietf:params:xml:ns:domain-1.0</objURI>
<objURI>urn:ietf:params:xml:ns:contact-1.0</objURI>
</svcs>
</login>
<clTRID>ABC-12345</clTRID>

View File

@@ -10,6 +10,7 @@
<svcs>
<objURI>urn:ietf:params:xml:ns:host-1.0</objURI>
<objURI>urn:ietf:params:xml:ns:domain-1.0</objURI>
<objURI>urn:ietf:params:xml:ns:contact-1.0</objURI>
<objURI>urn:ietf:params:xml:ns:foo-1.0</objURI>
</svcs>
</login>

View File

@@ -10,6 +10,7 @@
<svcs>
<objURI>urn:ietf:params:xml:ns:host-1.0</objURI>
<objURI>urn:ietf:params:xml:ns:domain-1.0</objURI>
<objURI>urn:ietf:params:xml:ns:contact-1.0</objURI>
</svcs>
</login>
<clTRID>ABC-12345</clTRID>

View File

@@ -11,6 +11,7 @@
<svcs>
<objURI>urn:ietf:params:xml:ns:host-1.0</objURI>
<objURI>urn:ietf:params:xml:ns:domain-1.0</objURI>
<objURI>urn:ietf:params:xml:ns:contact-1.0</objURI>
</svcs>
</login>
<clTRID>ABC-12345</clTRID>

View File

@@ -10,6 +10,7 @@
<svcs>
<objURI>urn:ietf:params:xml:ns:host-1.0</objURI>
<objURI>urn:ietf:params:xml:ns:domain-1.0</objURI>
<objURI>urn:ietf:params:xml:ns:contact-1.0</objURI>
</svcs>
</login>
<clTRID>ABC-12345</clTRID>

View File

@@ -12,6 +12,7 @@ xsi:schemaLocation="urn:ietf:params:xml:ns:epp-1.0 epp-1.0.xsd">
</options>
<svcs>
<objURI>urn:ietf:params:xml:ns:domain-1.0</objURI>
<objURI>urn:ietf:params:xml:ns:contact-1.0</objURI>
<objURI>urn:ietf:params:xml:ns:host-1.0</objURI>
<svcExtension>
<extURI>urn:ietf:params:xml:ns:secDNS-1.1</extURI>

View File

@@ -10,6 +10,7 @@
<svcs>
<objURI>urn:ietf:params:xml:ns:host-1.0</objURI>
<objURI>urn:ietf:params:xml:ns:domain-1.0</objURI>
<objURI>urn:ietf:params:xml:ns:contact-1.0</objURI>
<svcExtension>
<extURI>urn:ietf:params:xml:ns:launch-1.0</extURI>
<extURI>urn:ietf:params:xml:ns:rgp-1.0</extURI>

View File

@@ -10,6 +10,7 @@
<svcs>
<objURI>urn:ietf:params:xml:ns:host-1.0</objURI>
<objURI>urn:ietf:params:xml:ns:domain-1.0</objURI>
<objURI>urn:ietf:params:xml:ns:contact-1.0</objURI>
<svcExtension>
<extURI>urn:ietf:params:xml:ns:launch-1.0</extURI>
<extURI>urn:ietf:params:xml:ns:rgp-1.0</extURI>

View File

@@ -46,7 +46,6 @@ BACKEND /_dr/task/readDnsRefreshRequests ReadDnsRefreshReques
BACKEND /_dr/task/refreshDnsForAllDomains RefreshDnsForAllDomainsAction GET n APP ADMIN
BACKEND /_dr/task/refreshDnsOnHostRename RefreshDnsOnHostRenameAction POST n APP ADMIN
BACKEND /_dr/task/relockDomain RelockDomainAction POST y APP ADMIN
BACKEND /_dr/task/removeAllDomainContacts RemoveAllDomainContactsAction POST n APP ADMIN
BACKEND /_dr/task/resaveAllEppResourcesPipeline ResaveAllEppResourcesPipelineAction GET n APP ADMIN
BACKEND /_dr/task/resaveEntity ResaveEntityAction POST n APP ADMIN
BACKEND /_dr/task/sendExpiringCertificateNotificationEmail SendExpiringCertificateNotificationEmailAction GET n APP ADMIN
@@ -58,7 +57,6 @@ BACKEND /_dr/task/tmchSmdrl TmchSmdrlAction
BACKEND /_dr/task/triggerMosApiServiceState TriggerServiceStateAction GET n APP ADMIN
BACKEND /_dr/task/updateRegistrarRdapBaseUrls UpdateRegistrarRdapBaseUrlsAction GET y APP ADMIN
BACKEND /_dr/task/uploadBsaUnavailableNames UploadBsaUnavailableDomainsAction GET,POST n APP ADMIN
BACKEND /_dr/task/wipeOutContactHistoryPii WipeOutContactHistoryPiiAction GET n APP ADMIN
PUBAPI /check CheckApiAction GET n NONE PUBLIC
PUBAPI /rdap/ RdapEmptyAction GET,HEAD n NONE PUBLIC
PUBAPI /rdap/autnum/(*) RdapAutnumAction GET,HEAD n NONE PUBLIC

View File

@@ -11,6 +11,7 @@
<svcs>
<objURI>urn:ietf:params:xml:ns:host-1.0</objURI>
<objURI>urn:ietf:params:xml:ns:domain-1.0</objURI>
<objURI>urn:ietf:params:xml:ns:contact-1.0</objURI>
</svcs>
</login>
<clTRID>epp-client-login-@@NOW@@-@@CHANNEL_NUMBER@@</clTRID>

View File

@@ -5,6 +5,7 @@
<svcMenu>
<version>1.0</version>
<lang>en</lang>
<objURI>urn:ietf:params:xml:ns:contact-1.0</objURI>
<objURI>urn:ietf:params:xml:ns:domain-1.0</objURI>
<objURI>urn:ietf:params:xml:ns:host-1.0</objURI>
</svcMenu>

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